@dxos/react-ui-stack 0.8.4-main.1f223c7 → 0.8.4-main.2244d791bb

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 (112) hide show
  1. package/dist/lib/browser/index.mjs +921 -54
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/playwright/index.mjs +10 -23
  5. package/dist/lib/browser/playwright/index.mjs.map +2 -2
  6. package/dist/lib/node-esm/index.mjs +922 -54
  7. package/dist/lib/node-esm/index.mjs.map +4 -4
  8. package/dist/lib/node-esm/meta.json +1 -1
  9. package/dist/lib/node-esm/playwright/index.mjs +10 -23
  10. package/dist/lib/node-esm/playwright/index.mjs.map +2 -2
  11. package/dist/types/src/{exemplars → components}/CardStack/CardStack.d.ts +20 -13
  12. package/dist/types/src/components/CardStack/CardStack.d.ts.map +1 -0
  13. package/dist/types/src/{exemplars → components}/CardStack/CardStack.stories.d.ts +3 -1
  14. package/dist/types/src/components/CardStack/CardStack.stories.d.ts.map +1 -0
  15. package/dist/types/src/{exemplars → components}/CardStack/CardStackDragPreview.d.ts +4 -1
  16. package/dist/types/src/components/CardStack/CardStackDragPreview.d.ts.map +1 -0
  17. package/dist/types/src/components/CardStack/index.d.ts.map +1 -0
  18. package/dist/types/src/components/Stack/Stack.d.ts +7 -6
  19. package/dist/types/src/components/Stack/Stack.d.ts.map +1 -1
  20. package/dist/types/src/components/Stack/Stack.stories.d.ts +1 -2
  21. package/dist/types/src/components/Stack/Stack.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/StackContext.d.ts +1 -1
  23. package/dist/types/src/components/StackContext.d.ts.map +1 -1
  24. package/dist/types/src/components/StackItem/StackItem.d.ts +7 -10
  25. package/dist/types/src/components/StackItem/StackItem.d.ts.map +1 -1
  26. package/dist/types/src/components/StackItem/StackItem.stories.d.ts +0 -1
  27. package/dist/types/src/components/StackItem/StackItem.stories.d.ts.map +1 -1
  28. package/dist/types/src/components/StackItem/StackItemContent.d.ts +4 -37
  29. package/dist/types/src/components/StackItem/StackItemContent.d.ts.map +1 -1
  30. package/dist/types/src/components/StackItem/StackItemHeading.d.ts.map +1 -1
  31. package/dist/types/src/components/StackItem/StackItemSigil.d.ts +2 -2
  32. package/dist/types/src/components/StackItem/StackItemSigil.d.ts.map +1 -1
  33. package/dist/types/src/components/deprecated/LayoutControls.d.ts +3 -0
  34. package/dist/types/src/components/deprecated/LayoutControls.d.ts.map +1 -1
  35. package/dist/types/src/components/index.d.ts +2 -2
  36. package/dist/types/src/components/index.d.ts.map +1 -1
  37. package/dist/types/src/components/{defs.d.ts → types.d.ts} +1 -1
  38. package/dist/types/src/components/types.d.ts.map +1 -0
  39. package/dist/types/src/hooks/useStackDropForElements.d.ts +8 -6
  40. package/dist/types/src/hooks/useStackDropForElements.d.ts.map +1 -1
  41. package/dist/types/src/index.d.ts +0 -1
  42. package/dist/types/src/index.d.ts.map +1 -1
  43. package/dist/types/src/translations.d.ts +2 -2
  44. package/dist/types/src/translations.d.ts.map +1 -1
  45. package/dist/types/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +41 -38
  47. package/src/{exemplars → components}/CardStack/CardStack.stories.tsx +14 -13
  48. package/src/{exemplars → components}/CardStack/CardStack.tsx +113 -54
  49. package/src/{exemplars → components}/CardStack/CardStackDragPreview.tsx +12 -9
  50. package/src/components/Stack/Stack.stories.tsx +3 -5
  51. package/src/components/Stack/Stack.tsx +97 -40
  52. package/src/components/StackContext.tsx +1 -1
  53. package/src/components/StackItem/StackItem.stories.tsx +7 -7
  54. package/src/components/StackItem/StackItem.tsx +35 -16
  55. package/src/components/StackItem/StackItemContent.tsx +21 -41
  56. package/src/components/StackItem/StackItemHeading.tsx +2 -6
  57. package/src/components/StackItem/StackItemSigil.tsx +3 -3
  58. package/src/components/deprecated/LayoutControls.tsx +3 -0
  59. package/src/components/index.ts +2 -2
  60. package/src/hooks/useStackDropForElements.ts +58 -44
  61. package/src/index.ts +0 -3
  62. package/src/playwright/playwright.config.ts +1 -1
  63. package/src/translations.ts +1 -1
  64. package/dist/lib/browser/chunk-3V2YUQK5.mjs +0 -1375
  65. package/dist/lib/browser/chunk-3V2YUQK5.mjs.map +0 -7
  66. package/dist/lib/browser/testing/index.mjs +0 -31
  67. package/dist/lib/browser/testing/index.mjs.map +0 -7
  68. package/dist/lib/node-esm/chunk-HE3BRF7A.mjs +0 -1377
  69. package/dist/lib/node-esm/chunk-HE3BRF7A.mjs.map +0 -7
  70. package/dist/lib/node-esm/testing/index.mjs +0 -32
  71. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  72. package/dist/types/src/components/Image/Image.d.ts +0 -11
  73. package/dist/types/src/components/Image/Image.d.ts.map +0 -1
  74. package/dist/types/src/components/Image/Image.stories.d.ts +0 -31
  75. package/dist/types/src/components/Image/Image.stories.d.ts.map +0 -1
  76. package/dist/types/src/components/Image/index.d.ts +0 -2
  77. package/dist/types/src/components/Image/index.d.ts.map +0 -1
  78. package/dist/types/src/components/defs.d.ts.map +0 -1
  79. package/dist/types/src/exemplars/Card/Card.d.ts +0 -58
  80. package/dist/types/src/exemplars/Card/Card.d.ts.map +0 -1
  81. package/dist/types/src/exemplars/Card/Card.stories.d.ts +0 -44
  82. package/dist/types/src/exemplars/Card/Card.stories.d.ts.map +0 -1
  83. package/dist/types/src/exemplars/Card/CardDragPreview.d.ts +0 -6
  84. package/dist/types/src/exemplars/Card/CardDragPreview.d.ts.map +0 -1
  85. package/dist/types/src/exemplars/Card/fragments.d.ts +0 -13
  86. package/dist/types/src/exemplars/Card/fragments.d.ts.map +0 -1
  87. package/dist/types/src/exemplars/Card/index.d.ts +0 -4
  88. package/dist/types/src/exemplars/Card/index.d.ts.map +0 -1
  89. package/dist/types/src/exemplars/CardStack/CardStack.d.ts.map +0 -1
  90. package/dist/types/src/exemplars/CardStack/CardStack.stories.d.ts.map +0 -1
  91. package/dist/types/src/exemplars/CardStack/CardStackDragPreview.d.ts.map +0 -1
  92. package/dist/types/src/exemplars/CardStack/index.d.ts.map +0 -1
  93. package/dist/types/src/exemplars/index.d.ts +0 -3
  94. package/dist/types/src/exemplars/index.d.ts.map +0 -1
  95. package/dist/types/src/testing/CardContainer.d.ts +0 -6
  96. package/dist/types/src/testing/CardContainer.d.ts.map +0 -1
  97. package/dist/types/src/testing/index.d.ts +0 -2
  98. package/dist/types/src/testing/index.d.ts.map +0 -1
  99. package/src/components/Image/Image.stories.tsx +0 -58
  100. package/src/components/Image/Image.tsx +0 -137
  101. package/src/components/Image/index.ts +0 -5
  102. package/src/exemplars/Card/Card.stories.tsx +0 -88
  103. package/src/exemplars/Card/Card.tsx +0 -186
  104. package/src/exemplars/Card/CardDragPreview.tsx +0 -22
  105. package/src/exemplars/Card/fragments.ts +0 -24
  106. package/src/exemplars/Card/index.ts +0 -7
  107. package/src/exemplars/index.ts +0 -6
  108. package/src/testing/CardContainer.tsx +0 -37
  109. package/src/testing/index.ts +0 -5
  110. /package/dist/types/src/{exemplars → components}/CardStack/index.d.ts +0 -0
  111. /package/src/{exemplars → components}/CardStack/index.ts +0 -0
  112. /package/src/components/{defs.ts → types.ts} +0 -0
@@ -7,6 +7,7 @@ import React, {
7
7
  type CSSProperties,
8
8
  Children,
9
9
  type ComponentPropsWithRef,
10
+ type FocusEvent,
10
11
  type KeyboardEvent,
11
12
  forwardRef,
12
13
  useCallback,
@@ -16,13 +17,14 @@ import React, {
16
17
  } from 'react';
17
18
 
18
19
  import { ListItem, type ThemedClassName, useId } from '@dxos/react-ui';
19
- import { mx } from '@dxos/react-ui-theme';
20
+ import { mx } from '@dxos/ui-theme';
20
21
 
21
22
  import { useStackDropForElements } from '../../hooks';
22
- import { type StackContextValue } from '../defs';
23
23
  import { StackContext } from '../StackContext';
24
+ import { type StackContextValue } from '../types';
24
25
 
25
26
  export type Orientation = 'horizontal' | 'vertical';
27
+
26
28
  /**
27
29
  * Size is how Stack and its StackItems coordinate the dimensions of the items with the available space.
28
30
  * - `intrinsic` signals to Stack and its StackItems to occupy their intrinsic size
@@ -32,13 +34,6 @@ export type Orientation = 'horizontal' | 'vertical';
32
34
  */
33
35
  export type Size = 'intrinsic' | 'contain' | 'split';
34
36
 
35
- export type StackProps = Omit<ThemedClassName<ComponentPropsWithRef<'div'>>, 'aria-orientation'> &
36
- Partial<StackContextValue> & {
37
- itemsCount?: number;
38
- getDropElement?: (stackElement: HTMLDivElement) => HTMLDivElement;
39
- separatorOnScroll?: number;
40
- };
41
-
42
37
  export const railGridHorizontal = 'grid-rows-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
43
38
  export const railGridVertical = 'grid-cols-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
44
39
 
@@ -60,6 +55,14 @@ const scrollIntoViewAndFocus = (el: HTMLElement, orientation: StackProps['orient
60
55
  return el.focus();
61
56
  };
62
57
 
58
+ export type StackProps = Omit<ThemedClassName<ComponentPropsWithRef<'div'>>, 'aria-orientation'> &
59
+ Partial<StackContextValue> & {
60
+ itemsCount?: number;
61
+ getDropElement?: (stackElement: HTMLDivElement) => HTMLDivElement;
62
+ separatorOnScroll?: number;
63
+ circularFocus?: boolean;
64
+ };
65
+
63
66
  export const Stack = forwardRef<HTMLDivElement, StackProps>(
64
67
  (
65
68
  {
@@ -67,18 +70,20 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
67
70
  classNames,
68
71
  style,
69
72
  orientation = 'vertical',
70
- rail = true,
73
+ rail = true, // TODO(burdon): Change default to false.
71
74
  size = 'intrinsic',
72
75
  onRearrange,
73
76
  itemsCount = Children.count(children),
74
77
  getDropElement,
75
78
  separatorOnScroll,
79
+ circularFocus,
76
80
  ...props
77
81
  },
78
82
  forwardedRef,
79
83
  ) => {
80
84
  const stackId = useId('stack', props.id);
81
85
  const [stackElement, stackRef] = useState<HTMLDivElement | null>(null);
86
+ const [lastFocusedItem, setLastFocusedItem] = useState<string>();
82
87
  const composedItemRef = composeRefs<HTMLDivElement>(stackRef, forwardedRef);
83
88
 
84
89
  const styles: CSSProperties = {
@@ -115,9 +120,26 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
115
120
  }, [stackElement, separatorOnScroll, orientation]);
116
121
 
117
122
  /**
118
- * Handles moving focus using the arrow keys. Focus is only handled by the nearest stack; if the arrow key matches the
119
- * orientation, focus cycles between items, otherwise focus is passed to an adjacent stack item; or, if there is no
120
- * such stack item, focus is passed to the adjacent empty stack if one can be found.
123
+ * Handles blur events to track the last focused item within this stack.
124
+ */
125
+ const handleBlur = useCallback(
126
+ (event: FocusEvent<HTMLDivElement>) => {
127
+ if (event.target) {
128
+ const target = event.target as HTMLElement;
129
+ const closestStackItem = target.closest(`[data-dx-item-id]`) as HTMLElement | null;
130
+ if (closestStackItem?.closest(`[data-dx-stack="${stackId}"]`)) {
131
+ setLastFocusedItem(closestStackItem?.getAttribute('data-dx-item-id') ?? undefined);
132
+ }
133
+ }
134
+ props.onBlur?.(event);
135
+ },
136
+ [stackId, props.onBlur],
137
+ );
138
+
139
+ /**
140
+ * Handles moving focus using the arrow keys. Focus is only handled by the nearest stack;
141
+ * if the arrow key matches the orientation, focus cycles between items, otherwise focus is passed to an adjacent stack item;
142
+ * or, if there is no such stack item, focus is passed to the adjacent empty stack if one can be found.
121
143
  */
122
144
  const handleKeyDown = useCallback(
123
145
  (event: KeyboardEvent<HTMLDivElement>) => {
@@ -152,15 +174,28 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
152
174
  ? 1
153
175
  : 0;
154
176
  if (parallelDelta !== 0) {
155
- const adjacentItem = closestStackItems[
156
- (closestStackItems.indexOf(closestOwnedItem) + parallelDelta + closestStackItems.length) %
157
- closestStackItems.length
158
- ] as HTMLElement | undefined;
177
+ const currentIndex = closestStackItems.indexOf(closestOwnedItem);
178
+ const nextIndex = currentIndex + parallelDelta;
179
+ let adjacentItem: HTMLElement | undefined;
180
+
181
+ if (circularFocus) {
182
+ // Circular navigation: wrap around using modulo.
183
+ adjacentItem = closestStackItems[(nextIndex + closestStackItems.length) % closestStackItems.length] as
184
+ | HTMLElement
185
+ | undefined;
186
+ } else {
187
+ // Non-circular navigation: only move if within bounds.
188
+ if (nextIndex >= 0 && nextIndex < closestStackItems.length) {
189
+ adjacentItem = closestStackItems[nextIndex] as HTMLElement | undefined;
190
+ }
191
+ }
192
+
159
193
  if (adjacentItem) {
160
194
  event.preventDefault();
161
195
  scrollIntoViewAndFocus(adjacentItem, closestStackOrientation);
162
196
  }
163
197
  }
198
+
164
199
  if (perpendicularDelta !== 0) {
165
200
  if (ancestorStack && ancestorOrientation !== closestStackOrientation) {
166
201
  const siblingStacks = Array.from(
@@ -168,10 +203,21 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
168
203
  `[data-dx-stack-item="${ancestorStack.getAttribute('data-dx-stack')}"] [data-dx-stack]`,
169
204
  ),
170
205
  ) as HTMLElement[];
171
- const adjacentStack = siblingStacks[
172
- (siblingStacks.indexOf(closestStack) + perpendicularDelta + siblingStacks.length) %
173
- siblingStacks.length
174
- ] as HTMLElement | undefined;
206
+ const currentStackIndex = siblingStacks.indexOf(closestStack);
207
+ const nextStackIndex = currentStackIndex + perpendicularDelta;
208
+ let adjacentStack: HTMLElement | undefined;
209
+
210
+ if (ancestorStack.getAttribute('data-dx-stack-circular-focus') === 'true') {
211
+ // Circular navigation: wrap around using modulo.
212
+ adjacentStack = siblingStacks[(nextStackIndex + siblingStacks.length) % siblingStacks.length] as
213
+ | HTMLElement
214
+ | undefined;
215
+ } else {
216
+ // Non-circular navigation: only move if within bounds.
217
+ if (nextStackIndex >= 0 && nextStackIndex < siblingStacks.length) {
218
+ adjacentStack = siblingStacks[nextStackIndex] as HTMLElement | undefined;
219
+ }
220
+ }
175
221
  const adjacentStackSelfItem = adjacentStack?.closest(
176
222
  `[data-dx-stack-item=${ancestorStack.getAttribute('data-dx-stack')}]`,
177
223
  ) as HTMLElement | undefined;
@@ -182,26 +228,33 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
182
228
  ),
183
229
  ) as HTMLElement[])
184
230
  : [];
185
- if (adjacentStackItems.length > 0) {
186
- // Find the closest item by position
187
- const ownedItemRect = closestOwnedItem.getBoundingClientRect();
188
- const targetPosition =
189
- closestStackOrientation === 'vertical' ? ownedItemRect.top : ownedItemRect.left;
190
-
231
+ if (adjacentStack && adjacentStackItems.length > 0) {
232
+ // Check if the adjacent stack has a last focused item recorded, otherwise find the closest item by position.
191
233
  let closestItem = adjacentStackItems[0];
192
- let closestDistance = Infinity;
193
-
194
- for (const item of adjacentStackItems) {
195
- const itemRect = item.getBoundingClientRect();
196
- const itemPosition = closestStackOrientation === 'vertical' ? itemRect.top : itemRect.left;
197
- const distance = Math.abs(itemPosition - targetPosition);
234
+ // Try to find an item with matching data-dx-stack-item value.
235
+ const lastFocusedItem = adjacentStack.querySelector(
236
+ `[data-dx-item-id="${adjacentStack.getAttribute('data-dx-last-focused-item') ?? 'never'}"]`,
237
+ );
238
+ if (lastFocusedItem) {
239
+ closestItem = lastFocusedItem as HTMLElement;
240
+ } else {
241
+ // Fall back to positional calculation
242
+ const ownedItemRect = closestOwnedItem.getBoundingClientRect();
243
+ const targetPosition =
244
+ closestStackOrientation === 'vertical' ? ownedItemRect.top : ownedItemRect.left;
198
245
 
199
- if (distance < closestDistance) {
200
- closestDistance = distance;
201
- closestItem = item;
202
- }
203
- if (closestDistance <= PERPENDICULAR_FOCUS_THRESHHOLD) {
204
- break;
246
+ let closestDistance = Infinity;
247
+ for (const item of adjacentStackItems) {
248
+ const itemRect = item.getBoundingClientRect();
249
+ const itemPosition = closestStackOrientation === 'vertical' ? itemRect.top : itemRect.left;
250
+ const distance = Math.abs(itemPosition - targetPosition);
251
+ if (distance < closestDistance) {
252
+ closestDistance = distance;
253
+ closestItem = item;
254
+ }
255
+ if (closestDistance <= PERPENDICULAR_FOCUS_THRESHHOLD) {
256
+ break;
257
+ }
205
258
  }
206
259
  }
207
260
 
@@ -235,13 +288,14 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
235
288
  }
236
289
  props.onKeyDown?.(event);
237
290
  },
238
- [props.onKeyDown, stackId],
291
+ [props.onKeyDown, stackId, circularFocus],
239
292
  );
240
293
 
241
294
  const gridClasses = useMemo(() => {
242
295
  if (!rail) {
243
296
  return orientation === 'horizontal' ? 'grid-rows-1 pli-[--stack-gap]' : 'grid-cols-1 plb-[--stack-gap]';
244
297
  }
298
+
245
299
  if (orientation === 'horizontal') {
246
300
  return railGridHorizontal;
247
301
  } else {
@@ -279,7 +333,10 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
279
333
  classNames,
280
334
  )}
281
335
  onKeyDown={handleKeyDown}
336
+ onBlur={handleBlur}
282
337
  data-dx-stack={stackId}
338
+ data-dx-stack-circular-focus={circularFocus}
339
+ data-dx-last-focused-item={lastFocusedItem}
283
340
  data-rail={rail}
284
341
  aria-orientation={orientation}
285
342
  style={styles}
@@ -4,8 +4,8 @@
4
4
 
5
5
  import { createContext, useContext } from 'react';
6
6
 
7
- import { type StackItemRearrangeHandler, type StackItemSize } from './defs';
8
7
  import { type Orientation, type Size } from './Stack';
8
+ import { type StackItemRearrangeHandler, type StackItemSize } from './types';
9
9
 
10
10
  export type StackContextValue = {
11
11
  orientation: Orientation;
@@ -2,20 +2,18 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
6
  import React from 'react';
9
7
 
10
8
  import { DropdownMenu, Icon } from '@dxos/react-ui';
11
- import { withTheme } from '@dxos/storybook-utils';
9
+ import { withTheme } from '@dxos/react-ui/testing';
12
10
 
13
11
  import { StackItem, type StackItemRootProps } from './StackItem';
14
12
 
15
13
  const DefaultStory = (props: StackItemRootProps) => {
16
14
  return (
17
- <StackItem.Root role='section' {...props} classNames='w-[20rem] border border-separator'>
18
- <StackItem.Heading>
15
+ <StackItem.Root role='section' {...props} classNames='is-[20rem] border border-separator'>
16
+ <StackItem.Heading classNames='is-full border-be border-separator'>
19
17
  <span className='sr-only'>Title</span>
20
18
  <div role='none' className='sticky -block-start-px bg-[--sticky-bg] p-1 is-full'>
21
19
  <DropdownMenu.Root>
@@ -27,7 +25,9 @@ const DefaultStory = (props: StackItemRootProps) => {
27
25
  </DropdownMenu.Root>
28
26
  </div>
29
27
  </StackItem.Heading>
30
- <StackItem.Content classNames='p-2'>Content</StackItem.Content>
28
+ <StackItem.Content>
29
+ <div className='p-4 text-center'>Content</div>
30
+ </StackItem.Content>
31
31
  </StackItem.Root>
32
32
  );
33
33
  };
@@ -36,7 +36,7 @@ const meta = {
36
36
  title: 'ui/react-ui-stack/StackItem',
37
37
  component: StackItem.Root as any,
38
38
  render: DefaultStory,
39
- decorators: [withTheme],
39
+ decorators: [withTheme()],
40
40
  parameters: {
41
41
  layout: 'centered',
42
42
  },
@@ -26,10 +26,10 @@ import { createPortal } from 'react-dom';
26
26
 
27
27
  import { ListItem, type ThemedClassName } from '@dxos/react-ui';
28
28
  import { resizeAttributes, sizeStyle } from '@dxos/react-ui-dnd';
29
- import { mx } from '@dxos/react-ui-theme';
29
+ import { mx } from '@dxos/ui-theme';
30
30
 
31
- import { type StackItemData, type StackItemSize } from '../defs';
32
31
  import { type ItemDragState, StackItemContext, idle, useStack, useStackItem } from '../StackContext';
32
+ import { type StackItemData, type StackItemSize } from '../types';
33
33
 
34
34
  import { StackItemContent, type StackItemContentProps } from './StackItemContent';
35
35
  import { StackItemDragHandle, type StackItemDragHandleProps } from './StackItemDragHandle';
@@ -54,6 +54,10 @@ export const DEFAULT_HORIZONTAL_SIZE = 48 satisfies StackItemSize;
54
54
  export const DEFAULT_VERTICAL_SIZE = 'min-content' satisfies StackItemSize;
55
55
  export const DEFAULT_EXTRINSIC_SIZE = DEFAULT_HORIZONTAL_SIZE satisfies StackItemSize;
56
56
 
57
+ //
58
+ // StackItemRoot
59
+ //
60
+
57
61
  type StackItemRootProps = ThemedClassName<ComponentPropsWithRef<'div'>> & {
58
62
  item: Omit<StackItemData, 'type'>;
59
63
  order?: number;
@@ -63,7 +67,7 @@ type StackItemRootProps = ThemedClassName<ComponentPropsWithRef<'div'>> & {
63
67
  onSizeChange?: (nextSize: StackItemSize) => void;
64
68
  role?: 'article' | 'section';
65
69
  disableRearrange?: boolean;
66
- focusIndicatorVariant?: 'over-all' | 'group';
70
+ focusIndicatorVariant?: 'over-all' | 'group' | 'over-all-always' | 'group-always';
67
71
  };
68
72
 
69
73
  const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
@@ -86,6 +90,7 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
86
90
  forwardedRef,
87
91
  ) => {
88
92
  const [itemElement, itemRef] = useState<HTMLDivElement | null>(null);
93
+ const composedItemRef = composeRefs<HTMLDivElement>(itemRef, forwardedRef);
89
94
  const [selfDragHandleElement, selfDragHandleRef] = useState<HTMLDivElement | null>(null);
90
95
  const [closestEdge, setEdge] = useState<Edge | null>(null);
91
96
  const [sourceId, setSourceId] = useState<string | null>(null);
@@ -96,8 +101,6 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
96
101
 
97
102
  const Root = role ?? 'div';
98
103
 
99
- const composedItemRef = composeRefs<HTMLDivElement>(itemRef, forwardedRef);
100
-
101
104
  const setSize = useCallback(
102
105
  (nextSize: StackItemSize, commit?: boolean) => {
103
106
  setInternalSize(nextSize);
@@ -110,6 +113,7 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
110
113
 
111
114
  const type = orientation === 'horizontal' ? 'column' : 'card';
112
115
 
116
+ // TODO(burdon): Factor out?
113
117
  useLayoutEffect(() => {
114
118
  if (!itemElement || !onRearrange || disableRearrange) {
115
119
  return;
@@ -183,18 +187,18 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
183
187
 
184
188
  const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited' });
185
189
 
186
- // Determine if the drop would result in any changes
190
+ // Determine if the drop would result in any changes.
187
191
  const shouldShowDropIndicator = () => {
188
192
  if (!closestEdge || !sourceId) {
189
193
  return false;
190
194
  }
191
195
 
192
- // Don't show indicator when dragged item is over itself
196
+ // Don't show indicator when dragged item is over itself.
193
197
  if (sourceId === item.id) {
194
198
  return false;
195
199
  }
196
200
 
197
- // Don't show indicator when dragged item is over the trailing edge of its previous sibling
201
+ // Don't show indicator when dragged item is over the trailing edge of its previous sibling.
198
202
  const isTrailingEdgeOfPrevSibling =
199
203
  prevSiblingId !== undefined &&
200
204
  sourceId === prevSiblingId &&
@@ -232,15 +236,22 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
232
236
  'group/stack-item grid relative',
233
237
  focusIndicatorVariant === 'over-all'
234
238
  ? 'dx-focus-ring-inset-over-all'
235
- : orientation === 'horizontal'
236
- ? 'dx-focus-ring-group-x'
237
- : 'dx-focus-ring-group-y',
239
+ : focusIndicatorVariant === 'over-all-always'
240
+ ? 'dx-focus-ring-inset-over-all-always'
241
+ : orientation === 'horizontal'
242
+ ? focusIndicatorVariant === 'group-always'
243
+ ? 'dx-focus-ring-group-x-always'
244
+ : 'dx-focus-ring-group-x'
245
+ : focusIndicatorVariant === 'group-always'
246
+ ? 'dx-focus-ring-group-y-always'
247
+ : 'dx-focus-ring-group-y',
238
248
  orientation === 'horizontal' ? 'grid-rows-subgrid' : 'grid-cols-subgrid',
239
249
  rail && (orientation === 'horizontal' ? 'row-span-2' : 'col-span-2'),
240
250
  role === 'section' && orientation !== 'horizontal' && 'border-be border-subduedSeparator',
241
251
  classNames,
242
252
  )}
243
253
  data-dx-stack-item={stackId}
254
+ data-dx-item-id={item.id}
244
255
  {...resizeAttributes}
245
256
  style={{
246
257
  ...(stackSize !== 'split' && sizeStyle(size, orientation)),
@@ -261,37 +272,45 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
261
272
  },
262
273
  );
263
274
 
275
+ //
276
+ // StackItemDragPreview
277
+ //
278
+
264
279
  type StackItemDragPreviewProps = {
265
280
  children: ({ item }: { item: any }) => ReactNode;
266
281
  };
267
282
 
268
- export const StackItemDragPreview = ({ children }: StackItemDragPreviewProps) => {
283
+ const StackItemDragPreview = ({ children }: StackItemDragPreviewProps) => {
269
284
  const { state } = useStackItem();
270
285
  return state?.type === 'preview' ? createPortal(children({ item: state.item }), state.container) : null;
271
286
  };
272
287
 
288
+ //
289
+ // StackItem
290
+ //
291
+
273
292
  export const StackItem = {
274
293
  Root: StackItemRoot,
275
294
  Content: StackItemContent,
295
+ DragHandle: StackItemDragHandle,
296
+ DragPreview: StackItemDragPreview,
276
297
  Heading: StackItemHeading,
277
298
  HeadingLabel: StackItemHeadingLabel,
278
299
  HeadingStickyContent: StackItemHeadingStickyContent,
279
300
  ResizeHandle: StackItemResizeHandle,
280
- DragHandle: StackItemDragHandle,
281
301
  Sigil: StackItemSigil,
282
302
  SigilButton: StackItemSigilButton,
283
- DragPreview: StackItemDragPreview,
284
303
  };
285
304
 
286
305
  export type {
287
306
  StackItemRootProps,
288
307
  StackItemContentProps,
308
+ StackItemDragHandleProps,
309
+ StackItemDragPreviewProps,
289
310
  StackItemHeadingProps,
290
311
  StackItemHeadingLabelProps,
291
312
  StackItemResizeHandleProps,
292
- StackItemDragHandleProps,
293
313
  StackItemSigilProps,
294
314
  StackItemSigilButtonProps,
295
315
  StackItemSigilAction,
296
- StackItemDragPreviewProps,
297
316
  };
@@ -5,71 +5,49 @@
5
5
  import React, { type ComponentPropsWithoutRef, forwardRef, useMemo } from 'react';
6
6
 
7
7
  import { type ThemedClassName } from '@dxos/react-ui';
8
- import { mx } from '@dxos/react-ui-theme';
8
+ import { mx } from '@dxos/ui-theme';
9
9
 
10
10
  import { useStack, useStackItem } from '../StackContext';
11
11
 
12
- export type StackItemContentProps = ThemedClassName<Omit<ComponentPropsWithoutRef<'div'>, 'role'>> & {
13
- /**
14
- * This flag is required in order to clarify a developer experience that seemed like it needed extra boilerplate
15
- * (`row-span-2`) or was buggy. See the description of the StackItem.Content component itself for more information.
16
- */
12
+ export type StackItemContentProps = ThemedClassName<Omit<ComponentPropsWithoutRef<'div'>, 'role' | 'scrollable'>> & {
17
13
  toolbar?: boolean;
18
-
19
- /**
20
- * Whether to provide for the layout of a statusbar after the content.
21
- */
22
14
  statusbar?: boolean;
23
-
24
- /**
25
- * Whether the consumer intends to do something custom and typical affordances should not apply
26
- */
27
- layoutManaged?: boolean;
28
-
29
- /**
30
- * Whether to set a certain aspect ratio on the content, including the toolbar and statusbar. This is provided for
31
- * convenience and consistency; it can instead be specified by the `classNames` or `style` props as needed.
32
- */
33
- size?: 'intrinsic' | 'video' | 'square';
34
15
  };
35
16
 
36
17
  /**
37
- * This component should be used by plugins for rendering content within a stack item, a.k.a. a “plank” or “section”.
38
- * The `toolbar` flag must be provided since this component provides for the layout of content with the toolbar.
18
+ * This component should be used by plugins for rendering content within a stack item (i.e., a “plank” or “section”).
39
19
  */
40
20
  export const StackItemContent = forwardRef<HTMLDivElement, StackItemContentProps>(
41
- ({ children, toolbar, statusbar, layoutManaged, classNames, size = 'intrinsic', ...props }, forwardedRef) => {
21
+ ({ classNames, children, toolbar, statusbar, ...props }, forwardedRef) => {
42
22
  const { size: stackItemSize } = useStack();
43
23
  const { role } = useStackItem();
44
24
  const style = useMemo(
45
- () =>
46
- layoutManaged
47
- ? {}
48
- : {
49
- gridTemplateRows: [
50
- ...(toolbar ? [role === 'section' ? 'calc(var(--toolbar-size) - 1px)' : 'var(--toolbar-size)'] : []),
51
- '1fr',
52
- ...(statusbar ? ['var(--statusbar-size)'] : []),
53
- ].join(' '),
54
- },
55
- [toolbar, statusbar, layoutManaged],
25
+ () => ({
26
+ gridTemplateRows: [
27
+ toolbar && role === 'section' ? 'calc(var(--toolbar-size) - 1px)' : 'var(--toolbar-size)',
28
+ '1fr',
29
+ statusbar && 'var(--statusbar-size)',
30
+ ]
31
+ .filter(Boolean)
32
+ .join(' '),
33
+ }),
34
+ [toolbar, statusbar],
56
35
  );
57
36
 
58
37
  return (
59
38
  <div
60
- role='none'
61
39
  {...props}
40
+ role='none'
41
+ style={style}
62
42
  className={mx(
63
43
  'group grid grid-cols-[100%] density-coarse',
64
44
  stackItemSize === 'contain' && 'min-bs-0 overflow-hidden',
65
- size === 'video' ? 'aspect-video' : size === 'square' && 'aspect-square',
66
- toolbar && '[&>.dx-toolbar]:relative [&>.dx-toolbar]:border-be [&>.dx-toolbar]:border-subduedSeparator',
67
- role === 'section' &&
68
- toolbar &&
45
+ toolbar &&
46
+ role === 'section' &&
69
47
  '[&_.dx-toolbar]:sticky [&_.dx-toolbar]:z-[1] [&_.dx-toolbar]:block-start-0 [&_.dx-toolbar]:-mbe-px [&_.dx-toolbar]:min-is-0',
48
+ toolbar && '[&>.dx-toolbar]:relative [&>.dx-toolbar]:border-be [&>.dx-toolbar]:border-subduedSeparator',
70
49
  classNames,
71
50
  )}
72
- style={style}
73
51
  data-popover-collision-boundary={true}
74
52
  ref={forwardedRef}
75
53
  >
@@ -78,3 +56,5 @@ export const StackItemContent = forwardRef<HTMLDivElement, StackItemContentProps
78
56
  );
79
57
  },
80
58
  );
59
+
60
+ StackItemContent.displayName = 'StackItemContent';
@@ -2,7 +2,6 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { useFocusableGroup } from '@fluentui/react-tabster';
6
5
  import { Slot } from '@radix-ui/react-slot';
7
6
  import React, {
8
7
  type ComponentPropsWithRef,
@@ -13,7 +12,7 @@ import React, {
13
12
 
14
13
  import { type ThemedClassName } from '@dxos/react-ui';
15
14
  import { type AttendableId, type Related, useAttention } from '@dxos/react-ui-attention';
16
- import { mx } from '@dxos/react-ui-theme';
15
+ import { mx } from '@dxos/ui-theme';
17
16
 
18
17
  import { useStack } from '../StackContext';
19
18
 
@@ -30,7 +29,6 @@ export const StackItemHeading = ({
30
29
  ...props
31
30
  }: StackItemHeadingProps) => {
32
31
  const { orientation } = useStack();
33
- const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited' });
34
32
 
35
33
  const Root = asChild ? Slot : 'div';
36
34
 
@@ -38,10 +36,8 @@ export const StackItemHeading = ({
38
36
  <Root
39
37
  role='heading'
40
38
  {...props}
41
- tabIndex={0}
42
- {...focusableGroupAttrs}
43
39
  className={mx(
44
- 'flex items-center dx-focus-ring-inset-over-all relative !border-is-0 bg-headerSurface',
40
+ 'flex items-center !border-is-0 bg-headerSurface',
45
41
  separateOnScroll
46
42
  ? 'border-transparent [[data-scroll-separator="true"]_&]:border-subduedSeparator'
47
43
  : 'border-subduedSeparator',
@@ -4,11 +4,11 @@
4
4
 
5
5
  import React, { Fragment, type PropsWithChildren, forwardRef, useState } from 'react';
6
6
 
7
- import { type ActionLike } from '@dxos/app-graph';
7
+ import { type Node } from '@dxos/app-graph';
8
8
  import { keySymbols } from '@dxos/keyboard';
9
9
  import { Button, type ButtonProps, DropdownMenu, Icon, toLocalizedString, useTranslation } from '@dxos/react-ui';
10
10
  import { type AttendableId, type Related, useAttention } from '@dxos/react-ui-attention';
11
- import { descriptionText, mx } from '@dxos/react-ui-theme';
11
+ import { descriptionText, mx } from '@dxos/ui-theme';
12
12
  import { getHostPlatform } from '@dxos/util';
13
13
 
14
14
  import { translationKey } from '../../translations';
@@ -23,7 +23,7 @@ export type KeyBinding = {
23
23
  unknown?: string;
24
24
  };
25
25
 
26
- export type StackItemSigilAction = Pick<ActionLike, 'id' | 'properties' | 'data'>;
26
+ export type StackItemSigilAction = Pick<Node.ActionLike, 'id' | 'properties' | 'data'>;
27
27
 
28
28
  export type StackItemSigilButtonProps = Omit<ButtonProps, 'variant'> &
29
29
  AttendableId &
@@ -30,6 +30,9 @@ const LayoutControl = ({ icon, label, ...props }: Omit<ButtonProps, 'children'>
30
30
  return <IconButton iconOnly icon={icon} label={label} tooltipSide='bottom' variant='ghost' {...props} />;
31
31
  };
32
32
 
33
+ /**
34
+ * @deprecated
35
+ */
33
36
  export const LayoutControls = forwardRef<HTMLDivElement, LayoutControlsProps>(
34
37
  (
35
38
  { onClick, variant = 'default', capabilities: can, isSolo, pin, close = false, children, ...props },
@@ -2,8 +2,8 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export type * from './defs';
5
+ export type * from './types';
6
6
 
7
- export * from './Image';
7
+ export * from './CardStack';
8
8
  export * from './Stack';
9
9
  export * from './StackItem';