@dxos/react-ui-stack 0.8.3-staging.0fa589b → 0.8.4-main.1da679c

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 (108) hide show
  1. package/dist/lib/browser/chunk-RG4VZFFO.mjs +1375 -0
  2. package/dist/lib/browser/chunk-RG4VZFFO.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +44 -1160
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/{node/testing/index.cjs → browser/playwright/index.mjs} +26 -33
  7. package/dist/lib/{node/testing/index.cjs.map → browser/playwright/index.mjs.map} +3 -3
  8. package/dist/lib/browser/testing/index.mjs +25 -51
  9. package/dist/lib/browser/testing/index.mjs.map +4 -4
  10. package/dist/lib/node-esm/chunk-CNA6O4G5.mjs +1377 -0
  11. package/dist/lib/node-esm/chunk-CNA6O4G5.mjs.map +7 -0
  12. package/dist/lib/node-esm/index.mjs +44 -1161
  13. package/dist/lib/node-esm/index.mjs.map +4 -4
  14. package/dist/lib/node-esm/meta.json +1 -1
  15. package/dist/lib/node-esm/playwright/index.mjs +76 -0
  16. package/dist/lib/node-esm/playwright/index.mjs.map +7 -0
  17. package/dist/lib/node-esm/testing/index.mjs +24 -51
  18. package/dist/lib/node-esm/testing/index.mjs.map +4 -4
  19. package/dist/types/src/components/Image/Image.d.ts +11 -0
  20. package/dist/types/src/components/Image/Image.d.ts.map +1 -0
  21. package/dist/types/src/components/Image/Image.stories.d.ts +31 -0
  22. package/dist/types/src/components/Image/Image.stories.d.ts.map +1 -0
  23. package/dist/types/src/components/Image/index.d.ts +2 -0
  24. package/dist/types/src/components/Image/index.d.ts.map +1 -0
  25. package/dist/types/src/components/Stack/Stack.d.ts +9 -2
  26. package/dist/types/src/components/Stack/Stack.d.ts.map +1 -1
  27. package/dist/types/src/components/Stack/Stack.stories.d.ts +12 -2
  28. package/dist/types/src/components/Stack/Stack.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/StackContext.d.ts +2 -1
  30. package/dist/types/src/components/StackContext.d.ts.map +1 -1
  31. package/dist/types/src/components/StackItem/StackItem.d.ts +4 -4
  32. package/dist/types/src/components/StackItem/StackItem.d.ts.map +1 -1
  33. package/dist/types/src/components/StackItem/StackItem.stories.d.ts +13 -4
  34. package/dist/types/src/components/StackItem/StackItem.stories.d.ts.map +1 -1
  35. package/dist/types/src/components/StackItem/StackItemContent.d.ts +2 -2
  36. package/dist/types/src/components/StackItem/StackItemContent.d.ts.map +1 -1
  37. package/dist/types/src/components/StackItem/StackItemHeading.d.ts +1 -1
  38. package/dist/types/src/components/StackItem/StackItemHeading.d.ts.map +1 -1
  39. package/dist/types/src/components/StackItem/StackItemResizeHandle.d.ts.map +1 -1
  40. package/dist/types/src/components/StackItem/StackItemSigil.d.ts.map +1 -1
  41. package/dist/types/src/components/index.d.ts +2 -1
  42. package/dist/types/src/components/index.d.ts.map +1 -1
  43. package/dist/types/src/exemplars/Card/Card.d.ts +7 -11
  44. package/dist/types/src/exemplars/Card/Card.d.ts.map +1 -1
  45. package/dist/types/src/exemplars/Card/Card.stories.d.ts +44 -0
  46. package/dist/types/src/exemplars/Card/Card.stories.d.ts.map +1 -0
  47. package/dist/types/src/exemplars/Card/fragments.d.ts +4 -4
  48. package/dist/types/src/exemplars/Card/fragments.d.ts.map +1 -1
  49. package/dist/types/src/exemplars/CardStack/CardStack.d.ts +7 -1
  50. package/dist/types/src/exemplars/CardStack/CardStack.d.ts.map +1 -1
  51. package/dist/types/src/exemplars/CardStack/CardStack.stories.d.ts +13 -0
  52. package/dist/types/src/exemplars/CardStack/CardStack.stories.d.ts.map +1 -0
  53. package/dist/types/src/hooks/useStackDropForElements.d.ts +1 -1
  54. package/dist/types/src/hooks/useStackDropForElements.d.ts.map +1 -1
  55. package/dist/types/src/index.d.ts +1 -1
  56. package/dist/types/src/index.d.ts.map +1 -1
  57. package/dist/types/src/playwright/index.d.ts +2 -0
  58. package/dist/types/src/playwright/index.d.ts.map +1 -0
  59. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  60. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  61. package/dist/types/src/playwright/stack-manager.d.ts.map +1 -0
  62. package/dist/types/src/testing/CardContainer.d.ts +6 -0
  63. package/dist/types/src/testing/CardContainer.d.ts.map +1 -0
  64. package/dist/types/src/testing/index.d.ts +1 -1
  65. package/dist/types/src/translations.d.ts +13 -14
  66. package/dist/types/src/translations.d.ts.map +1 -1
  67. package/dist/types/tsconfig.tsbuildinfo +1 -1
  68. package/package.json +30 -26
  69. package/src/components/Image/Image.stories.tsx +58 -0
  70. package/src/components/Image/Image.tsx +137 -0
  71. package/src/components/Image/index.ts +5 -0
  72. package/src/components/Stack/Stack.stories.tsx +7 -6
  73. package/src/components/Stack/Stack.tsx +160 -18
  74. package/src/components/StackContext.tsx +2 -1
  75. package/src/components/StackItem/StackItem.stories.tsx +15 -11
  76. package/src/components/StackItem/StackItem.tsx +15 -14
  77. package/src/components/StackItem/StackItemContent.tsx +4 -3
  78. package/src/components/StackItem/StackItemHeading.tsx +3 -3
  79. package/src/components/StackItem/StackItemResizeHandle.tsx +2 -1
  80. package/src/components/StackItem/StackItemSigil.tsx +2 -1
  81. package/src/components/index.ts +2 -1
  82. package/src/exemplars/Card/Card.stories.tsx +88 -0
  83. package/src/exemplars/Card/Card.tsx +42 -38
  84. package/src/exemplars/Card/CardDragPreview.tsx +2 -2
  85. package/src/exemplars/Card/fragments.ts +4 -5
  86. package/src/exemplars/CardStack/CardStack.stories.tsx +172 -0
  87. package/src/exemplars/CardStack/CardStack.tsx +19 -2
  88. package/src/hooks/useStackDropForElements.ts +5 -2
  89. package/src/index.ts +3 -4
  90. package/src/playwright/index.ts +5 -0
  91. package/src/playwright/playwright.config.ts +17 -0
  92. package/src/playwright/smoke.spec.ts +7 -5
  93. package/src/testing/CardContainer.tsx +37 -0
  94. package/src/testing/index.ts +1 -1
  95. package/src/translations.ts +5 -3
  96. package/dist/lib/node/index.cjs +0 -1220
  97. package/dist/lib/node/index.cjs.map +0 -7
  98. package/dist/lib/node/meta.json +0 -1
  99. package/dist/types/src/exemplars/Card/Card.stories-todo.d.ts +0 -1
  100. package/dist/types/src/exemplars/Card/Card.stories-todo.d.ts.map +0 -1
  101. package/dist/types/src/exemplars/CardStack/CardStack.stories-todo.d.ts +0 -1
  102. package/dist/types/src/exemplars/CardStack/CardStack.stories-todo.d.ts.map +0 -1
  103. package/dist/types/src/testing/stack-manager.d.ts.map +0 -1
  104. package/src/exemplars/Card/Card.stories-todo.tsx +0 -135
  105. package/src/exemplars/CardStack/CardStack.stories-todo.tsx +0 -80
  106. package/src/playwright/playwright.config.cts +0 -18
  107. /package/dist/types/src/{testing → playwright}/stack-manager.d.ts +0 -0
  108. /package/src/{testing → playwright}/stack-manager.ts +0 -0
@@ -7,46 +7,47 @@ import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-d
7
7
  import { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source';
8
8
  import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
9
9
  import {
10
+ type Edge,
10
11
  attachClosestEdge,
11
12
  extractClosestEdge,
12
- type Edge,
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
16
  import React, {
17
- forwardRef,
18
- useLayoutEffect,
19
- useState,
20
17
  type ComponentPropsWithRef,
21
- useCallback,
22
18
  type ReactNode,
19
+ forwardRef,
20
+ useCallback,
21
+ useLayoutEffect,
23
22
  useMemo,
23
+ useState,
24
24
  } from 'react';
25
25
  import { createPortal } from 'react-dom';
26
26
 
27
- import { type ThemedClassName, ListItem } from '@dxos/react-ui';
27
+ import { ListItem, type ThemedClassName } from '@dxos/react-ui';
28
28
  import { resizeAttributes, sizeStyle } from '@dxos/react-ui-dnd';
29
29
  import { mx } from '@dxos/react-ui-theme';
30
30
 
31
+ import { type StackItemData, type StackItemSize } from '../defs';
32
+ import { type ItemDragState, StackItemContext, idle, useStack, useStackItem } from '../StackContext';
33
+
31
34
  import { StackItemContent, type StackItemContentProps } from './StackItemContent';
32
35
  import { StackItemDragHandle, type StackItemDragHandleProps } from './StackItemDragHandle';
33
36
  import {
34
37
  StackItemHeading,
35
38
  StackItemHeadingLabel,
36
- type StackItemHeadingProps,
37
39
  type StackItemHeadingLabelProps,
40
+ type StackItemHeadingProps,
38
41
  StackItemHeadingStickyContent,
39
42
  } from './StackItemHeading';
40
43
  import { StackItemResizeHandle, type StackItemResizeHandleProps } from './StackItemResizeHandle';
41
44
  import {
42
45
  StackItemSigil,
43
- type StackItemSigilProps,
44
46
  type StackItemSigilAction,
45
- type StackItemSigilButtonProps,
46
47
  StackItemSigilButton,
48
+ type StackItemSigilButtonProps,
49
+ type StackItemSigilProps,
47
50
  } from './StackItemSigil';
48
- import { useStack, StackItemContext, idle, type ItemDragState, useStackItem } from '../StackContext';
49
- import { type StackItemSize, type StackItemData } from '../defs';
50
51
 
51
52
  // NOTE: 48rem fills the screen on a MacbookPro with the sidebars closed.
52
53
  export const DEFAULT_HORIZONTAL_SIZE = 48 satisfies StackItemSize;
@@ -89,7 +90,7 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
89
90
  const [closestEdge, setEdge] = useState<Edge | null>(null);
90
91
  const [sourceId, setSourceId] = useState<string | null>(null);
91
92
  const [dragState, setDragState] = useState<ItemDragState>(idle);
92
- const { orientation, rail, onRearrange } = useStack();
93
+ const { orientation, rail, onRearrange, size: stackSize, stackId } = useStack();
93
94
  const [size = orientation === 'horizontal' ? DEFAULT_HORIZONTAL_SIZE : DEFAULT_VERTICAL_SIZE, setInternalSize] =
94
95
  useState(propsSize);
95
96
 
@@ -239,10 +240,10 @@ const StackItemRoot = forwardRef<HTMLDivElement, StackItemRootProps>(
239
240
  role === 'section' && orientation !== 'horizontal' && 'border-be border-subduedSeparator',
240
241
  classNames,
241
242
  )}
242
- data-dx-stack-item
243
+ data-dx-stack-item={stackId}
243
244
  {...resizeAttributes}
244
245
  style={{
245
- ...sizeStyle(size, orientation),
246
+ ...(stackSize !== 'split' && sizeStyle(size, orientation)),
246
247
  ...(Number.isFinite(order) && {
247
248
  [orientation === 'horizontal' ? 'gridColumn' : 'gridRow']: `${order}`,
248
249
  }),
@@ -9,7 +9,7 @@ import { mx } from '@dxos/react-ui-theme';
9
9
 
10
10
  import { useStack, useStackItem } from '../StackContext';
11
11
 
12
- export type StackItemContentProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> & {
12
+ export type StackItemContentProps = ThemedClassName<Omit<ComponentPropsWithoutRef<'div'>, 'role'>> & {
13
13
  /**
14
14
  * This flag is required in order to clarify a developer experience that seemed like it needed extra boilerplate
15
15
  * (`row-span-2`) or was buggy. See the description of the StackItem.Content component itself for more information.
@@ -54,15 +54,16 @@ export const StackItemContent = forwardRef<HTMLDivElement, StackItemContentProps
54
54
  },
55
55
  [toolbar, statusbar, layoutManaged],
56
56
  );
57
+
57
58
  return (
58
59
  <div
59
60
  role='none'
60
61
  {...props}
61
62
  className={mx(
62
- 'group grid grid-cols-[100%]',
63
+ 'group grid grid-cols-[100%] density-coarse',
63
64
  stackItemSize === 'contain' && 'min-bs-0 overflow-hidden',
64
65
  size === 'video' ? 'aspect-video' : size === 'square' && 'aspect-square',
65
- toolbar && '[&_.dx-toolbar]:relative [&_.dx-toolbar]:border-be [&_.dx-toolbar]:border-subduedSeparator',
66
+ toolbar && '[&>.dx-toolbar]:relative [&>.dx-toolbar]:border-be [&>.dx-toolbar]:border-subduedSeparator',
66
67
  role === 'section' &&
67
68
  toolbar &&
68
69
  '[&_.dx-toolbar]:sticky [&_.dx-toolbar]:z-[1] [&_.dx-toolbar]:block-start-0 [&_.dx-toolbar]:-mbe-px [&_.dx-toolbar]:min-is-0',
@@ -5,14 +5,14 @@
5
5
  import { useFocusableGroup } from '@fluentui/react-tabster';
6
6
  import { Slot } from '@radix-ui/react-slot';
7
7
  import React, {
8
- type ComponentPropsWithoutRef,
9
8
  type ComponentPropsWithRef,
10
- forwardRef,
9
+ type ComponentPropsWithoutRef,
11
10
  type PropsWithChildren,
11
+ forwardRef,
12
12
  } from 'react';
13
13
 
14
14
  import { type ThemedClassName } from '@dxos/react-ui';
15
- import { useAttention, type AttendableId, type Related } from '@dxos/react-ui-attention';
15
+ import { type AttendableId, type Related, useAttention } from '@dxos/react-ui-attention';
16
16
  import { mx } from '@dxos/react-ui-theme';
17
17
 
18
18
  import { useStack } from '../StackContext';
@@ -6,9 +6,10 @@ import React from 'react';
6
6
 
7
7
  import { ResizeHandle } from '@dxos/react-ui-dnd';
8
8
 
9
- import { DEFAULT_EXTRINSIC_SIZE } from './StackItem';
10
9
  import { useStack, useStackItem } from '../StackContext';
11
10
 
11
+ import { DEFAULT_EXTRINSIC_SIZE } from './StackItem';
12
+
12
13
  const MIN_WIDTH = 20;
13
14
  const MIN_HEIGHT = 3;
14
15
 
@@ -11,9 +11,10 @@ import { type AttendableId, type Related, useAttention } from '@dxos/react-ui-at
11
11
  import { descriptionText, mx } from '@dxos/react-ui-theme';
12
12
  import { getHostPlatform } from '@dxos/util';
13
13
 
14
- import { MenuSignifierHorizontal } from './MenuSignifier';
15
14
  import { translationKey } from '../../translations';
16
15
 
16
+ import { MenuSignifierHorizontal } from './MenuSignifier';
17
+
17
18
  export type KeyBinding = {
18
19
  windows?: string;
19
20
  macos?: string;
@@ -2,7 +2,8 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './defs';
5
+ export type * from './defs';
6
6
 
7
+ export * from './Image';
7
8
  export * from './Stack';
8
9
  export * from './StackItem';
@@ -0,0 +1,88 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
8
+ import React from 'react';
9
+
10
+ import { faker } from '@dxos/random';
11
+ import { withTheme } from '@dxos/storybook-utils';
12
+
13
+ import { Card } from './Card';
14
+
15
+ faker.seed(0);
16
+
17
+ type CardStoryProps = {
18
+ title: string;
19
+ description: string;
20
+ image: string;
21
+ showImage: boolean;
22
+ showIcon: boolean;
23
+ };
24
+
25
+ const DefaultStory = ({ title, description, image, showImage, showIcon }: CardStoryProps) => {
26
+ return (
27
+ <div className='max-is-md'>
28
+ <Card.StaticRoot>
29
+ <Card.Toolbar>
30
+ <Card.DragHandle toolbarItem />
31
+ <Card.ToolbarSeparator variant='gap' />
32
+ <Card.ToolbarIconButton iconOnly variant='ghost' icon='ph--x--regular' label={'remove card label'} />
33
+ </Card.Toolbar>
34
+ {showImage && <Card.Poster alt={title} image={image} />}
35
+ {!showImage && showIcon && <Card.Poster alt={title} icon='ph--building-office--regular' />}
36
+ <Card.Heading>{title}</Card.Heading>
37
+ {description && <Card.Text classNames='line-clamp-2'>{description}</Card.Text>}
38
+ </Card.StaticRoot>
39
+ </div>
40
+ );
41
+ };
42
+
43
+ const meta = {
44
+ title: 'ui/react-ui-stack/Card',
45
+ render: DefaultStory,
46
+ decorators: [withTheme],
47
+ parameters: {
48
+ layout: 'centered',
49
+ },
50
+ argTypes: {
51
+ title: {
52
+ control: 'text',
53
+ description: 'Card title',
54
+ },
55
+ description: {
56
+ control: 'text',
57
+ description: 'Card description',
58
+ },
59
+ image: {
60
+ control: 'text',
61
+ description: 'URL for the poster image',
62
+ },
63
+ showImage: {
64
+ control: 'boolean',
65
+ description: 'Whether to show the image',
66
+ },
67
+ showIcon: {
68
+ control: 'boolean',
69
+ description: 'Whether to show an icon (when image is not shown)',
70
+ },
71
+ },
72
+ } satisfies Meta<typeof DefaultStory>;
73
+
74
+ export default meta;
75
+
76
+ type Story = StoryObj<typeof meta>;
77
+
78
+ const image = faker.image.url();
79
+
80
+ export const Default: Story = {
81
+ args: {
82
+ title: faker.commerce.productName(),
83
+ description: faker.lorem.paragraph(),
84
+ image,
85
+ showImage: true,
86
+ showIcon: true,
87
+ },
88
+ };
@@ -5,40 +5,27 @@
5
5
  import { Primitive } from '@radix-ui/react-primitive';
6
6
  import { Slot } from '@radix-ui/react-slot';
7
7
  import React, {
8
- type ComponentPropsWithoutRef,
9
8
  type ComponentPropsWithRef,
9
+ type ComponentPropsWithoutRef,
10
10
  type FC,
11
- forwardRef,
12
11
  type PropsWithChildren,
12
+ forwardRef,
13
13
  } from 'react';
14
14
 
15
15
  import { Icon, IconButton, type ThemedClassName, Toolbar, type ToolbarRootProps, useTranslation } from '@dxos/react-ui';
16
16
  import { hoverableControls, mx } from '@dxos/react-ui-theme';
17
17
 
18
- import { cardChrome, cardContent, cardHeading, cardRoot, cardText, cardSpacing } from './fragments';
19
- import { StackItem } from '../../components';
18
+ import { Image, StackItem } from '../../components';
20
19
  import { translationKey } from '../../translations';
21
20
 
22
- type SharedCardProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> & { asChild?: boolean };
21
+ import { cardChrome, cardHeading, cardRoot, cardSpacing, cardText } from './fragments';
23
22
 
24
- const CardRoot = forwardRef<HTMLDivElement, SharedCardProps>(
25
- ({ children, classNames, asChild, role = 'none', ...props }, forwardedRef) => {
26
- const Root = asChild ? Slot : 'div';
27
- const rootProps = asChild ? { classNames: [cardRoot, classNames] } : { className: mx(cardRoot, classNames), role };
28
- return (
29
- <Root {...props} {...rootProps} ref={forwardedRef}>
30
- {children}
31
- </Root>
32
- );
33
- },
34
- );
23
+ type SharedCardProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> & { asChild?: boolean };
35
24
 
36
- const CardContent = forwardRef<HTMLDivElement, SharedCardProps>(
25
+ const CardStaticRoot = forwardRef<HTMLDivElement, SharedCardProps>(
37
26
  ({ children, classNames, asChild, role = 'group', ...props }, forwardedRef) => {
38
27
  const Root = asChild ? Slot : 'div';
39
- const rootProps = asChild
40
- ? { classNames: [cardContent, classNames] }
41
- : { className: mx(cardContent, classNames), role };
28
+ const rootProps = asChild ? { classNames: [cardRoot, classNames] } : { className: mx(cardRoot, classNames), role };
42
29
  return (
43
30
  <Root {...props} {...rootProps} ref={forwardedRef}>
44
31
  {children}
@@ -49,21 +36,40 @@ const CardContent = forwardRef<HTMLDivElement, SharedCardProps>(
49
36
 
50
37
  /**
51
38
  * This should be used by Surface fulfillments in cases where the content may or may not already be encapsulated (e.g.
52
- * in a Popover) and knows this based on the `role` it receives. This will render a `Card.Content` by default, otherwise
39
+ * in a Popover) and knows this based on the `role` it receives. This will render a `Card.StaticRoot` by default, otherwise
53
40
  * it will render a `div` primitive with the appropriate styling for specific handled situations.
54
41
  */
55
- const CardConditionalContent = ({ role, children }: PropsWithChildren<{ role?: string }>) => {
56
- if (['popover', 'card--kanban'].includes(role ?? 'never')) {
42
+ const CardSurfaceRoot = ({
43
+ role = 'never',
44
+ children,
45
+ classNames,
46
+ }: ThemedClassName<PropsWithChildren<{ role?: string }>>) => {
47
+ if (['card--popover', 'card--intrinsic', 'card--extrinsic'].includes(role)) {
57
48
  return (
58
- <div className={role === 'popover' ? 'popover-card-width' : role === 'card--kanban' ? 'contents' : ''}>
49
+ <div
50
+ className={mx(
51
+ role === 'card--popover'
52
+ ? 'popover-card-width'
53
+ : ['card--intrinsic', 'card--extrinsic'].includes(role)
54
+ ? 'contents'
55
+ : '',
56
+ classNames,
57
+ )}
58
+ >
59
59
  {children}
60
60
  </div>
61
61
  );
62
62
  } else {
63
63
  return (
64
- <CardContent {...(role === 'card--document' && { classNames: ['mlb-[1em]', hoverableControls] })}>
64
+ <CardStaticRoot
65
+ classNames={[
66
+ role === 'card--transclusion' && 'mlb-1',
67
+ role === 'card--transclusion' && hoverableControls,
68
+ classNames,
69
+ ]}
70
+ >
65
71
  {children}
66
- </CardContent>
72
+ </CardStaticRoot>
67
73
  );
68
74
  }
69
75
  };
@@ -84,7 +90,7 @@ const CardHeading = forwardRef<HTMLDivElement, SharedCardProps>(
84
90
 
85
91
  const CardToolbar = forwardRef<HTMLDivElement, ToolbarRootProps>(({ children, classNames, ...props }, forwardedRef) => {
86
92
  return (
87
- <Toolbar.Root {...props} classNames={['bg-transparent', classNames]} ref={forwardedRef}>
93
+ <Toolbar.Root {...props} classNames={['bg-transparent density-coarse', classNames]} ref={forwardedRef}>
88
94
  {children}
89
95
  </Toolbar.Root>
90
96
  );
@@ -101,7 +107,7 @@ const CardDragHandle = forwardRef<HTMLButtonElement, { toolbarItem?: boolean }>(
101
107
  iconOnly
102
108
  icon='ph--dots-six-vertical--regular'
103
109
  variant='ghost'
104
- label={t('card drag handle label')}
110
+ label={t('drag handle label')}
105
111
  classNames='pli-2'
106
112
  ref={forwardedRef}
107
113
  />
@@ -120,15 +126,14 @@ type CardPosterProps = {
120
126
  const CardPoster = (props: CardPosterProps) => {
121
127
  const aspect = props.aspect === 'auto' ? 'aspect-auto' : 'aspect-video';
122
128
  if (props.image) {
123
- return (
124
- <img className={`dx-card__poster ${aspect} object-cover is-full bs-auto`} src={props.image} alt={props.alt} />
125
- );
129
+ return <Image classNames={[`dx-card__poster is-full __bs-auto`, aspect]} src={props.image} alt={props.alt} />;
126
130
  }
131
+
127
132
  if (props.icon) {
128
133
  return (
129
134
  <div
130
135
  role='image'
131
- className={`dx-card__poster grid ${aspect} place-items-center bg-inputSurface text-subdued`}
136
+ className={mx(`dx-card__poster grid place-items-center bg-inputSurface text-subdued`, aspect)}
132
137
  aria-label={props.alt}
133
138
  >
134
139
  <Icon icon={props.icon} size={10} />
@@ -151,9 +156,9 @@ const CardChrome = forwardRef<HTMLDivElement, SharedCardProps>(
151
156
  },
152
157
  );
153
158
 
154
- const CardText = forwardRef<HTMLParagraphElement, SharedCardProps>(
159
+ const CardText = forwardRef<HTMLDivElement, SharedCardProps>(
155
160
  ({ children, classNames, asChild, role = 'none', ...props }, forwardedRef) => {
156
- const Root = asChild ? Slot : 'p';
161
+ const Root = asChild ? Slot : 'div';
157
162
  const rootProps = asChild ? { classNames: [cardText, classNames] } : { className: mx(cardText, classNames), role };
158
163
  return (
159
164
  <Root {...props} {...rootProps} ref={forwardedRef}>
@@ -164,9 +169,8 @@ const CardText = forwardRef<HTMLParagraphElement, SharedCardProps>(
164
169
  );
165
170
 
166
171
  export const Card = {
167
- Root: CardRoot,
168
- Content: CardContent,
169
- Container: CardConditionalContent,
172
+ StaticRoot: CardStaticRoot,
173
+ SurfaceRoot: CardSurfaceRoot,
170
174
  Heading: CardHeading,
171
175
  Toolbar: CardToolbar,
172
176
  ToolbarIconButton: CardToolbarIconButton,
@@ -179,4 +183,4 @@ export const Card = {
179
183
  Text: CardText,
180
184
  };
181
185
 
182
- export { cardRoot, cardContent, cardHeading, cardText, cardChrome, cardSpacing };
186
+ export { cardRoot, cardHeading, cardText, cardChrome, cardSpacing };
@@ -6,14 +6,14 @@ import React, { type PropsWithChildren } from 'react';
6
6
 
7
7
  import { mx } from '@dxos/react-ui-theme';
8
8
 
9
- import { cardContent } from './fragments';
9
+ import { cardRoot } from './fragments';
10
10
 
11
11
  const CardDragPreviewRoot = ({ children }: PropsWithChildren<{}>) => {
12
12
  return <div className='p-2'>{children}</div>;
13
13
  };
14
14
 
15
15
  const CardDragPreviewContent = ({ children }: PropsWithChildren<{}>) => {
16
- return <div className={mx(cardContent, 'ring-focusLine ring-neutralFocusIndicator')}>{children}</div>;
16
+ return <div className={mx(cardRoot, 'ring-focusLine ring-neutralFocusIndicator')}>{children}</div>;
17
17
  };
18
18
 
19
19
  export const CardDragPreview = {
@@ -2,12 +2,11 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export const cardRoot = 'contain-layout pli-2 plb-1 first-of-type:pbs-0 last-of-type:pbe-0';
6
-
7
- export const cardContent =
5
+ export const cardRoot =
8
6
  'rounded overflow-hidden bg-cardSurface border border-separator dark:border-subduedSeparator dx-focus-ring-group-y-indicator relative min-bs-[--rail-item] group/card';
9
7
 
10
8
  export const cardSpacing = 'pli-cardSpacingInline mlb-cardSpacingBlock';
9
+ export const cardNoSpacing = 'pli-0 mlb-0';
11
10
  export const labelSpacing = 'mbs-inputSpacingBlock mbe-labelSpacingBlock';
12
11
 
13
12
  export const cardDialogContent = 'p-0 bs-content min-bs-[8rem] max-bs-full md:max-is-[32rem] overflow-hidden';
@@ -19,7 +18,7 @@ export const cardDialogSearchListRoot =
19
18
 
20
19
  export const cardText = cardSpacing;
21
20
 
22
- export const cardHeading = 'text-lg font-medium line-clamp-2';
21
+ export const cardHeading = 'text-lg font-medium line-clamp-2 grow';
23
22
 
24
23
  export const cardChrome =
25
- 'pli-[--dx-cardSpacingChrome] mlb-[--dx-cardSpacingChrome] [&_.dx-button]:text-start [&_.dx-button]:is-full';
24
+ 'pli-[--dx-cardSpacingChrome] mlb-[--dx-cardSpacingChrome] [&_.dx-button]:text-start [&_.dx-button]:is-full [&_.dx-button]:pis-[calc(var(--dx-cardSpacingInline)-var(--dx-cardSpacingChrome))]';
@@ -0,0 +1,172 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
8
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
9
+ import React, { useCallback, useState } from 'react';
10
+
11
+ import { faker } from '@dxos/random';
12
+ import { IconButton } from '@dxos/react-ui';
13
+ import { withLayout, withTheme } from '@dxos/storybook-utils';
14
+
15
+ import { StackItem } from '../../components';
16
+ import { Card, CardDragPreview } from '../Card';
17
+
18
+ import { CardStack } from './CardStack';
19
+
20
+ faker.seed(0);
21
+
22
+ type CardItem = {
23
+ id: string;
24
+ title: string;
25
+ description: string;
26
+ image: string;
27
+ };
28
+
29
+ type StackItemData = {
30
+ id: string;
31
+ type?: 'column' | 'card';
32
+ };
33
+
34
+ const CardStackStory = () => {
35
+ const [column, setColumn] = useState<CardItem[]>(
36
+ faker.helpers.multiple(
37
+ () => ({
38
+ id: faker.string.uuid(),
39
+ title: faker.commerce.productName(),
40
+ description: faker.lorem.paragraph(),
41
+ image: faker.image.url(),
42
+ }),
43
+ { count: 12 },
44
+ ),
45
+ );
46
+
47
+ const handleRearrange = useCallback((source: StackItemData, target: StackItemData, closestEdge: Edge | null) => {
48
+ setColumn((prevColumn) => {
49
+ const newColumns = [...prevColumn];
50
+ // Reordering cards within a column
51
+ const sourceCardIndex = prevColumn.findIndex((card) => card.id === source.id);
52
+ const targetCardIndex = prevColumn.findIndex((card) => card.id === target.id);
53
+
54
+ if (typeof sourceCardIndex === 'number' && typeof targetCardIndex === 'number') {
55
+ const [movedCard] = newColumns.splice(sourceCardIndex, 1);
56
+
57
+ let insertIndex;
58
+ if (sourceCardIndex < targetCardIndex) {
59
+ insertIndex = closestEdge === 'bottom' ? targetCardIndex : targetCardIndex - 1;
60
+ } else {
61
+ insertIndex = closestEdge === 'bottom' ? targetCardIndex + 1 : targetCardIndex;
62
+ }
63
+ newColumns.splice(insertIndex, 0, movedCard);
64
+ }
65
+ return newColumns;
66
+ });
67
+ }, []);
68
+
69
+ const handleAddCard = useCallback(() => {
70
+ setColumn((prevColumn) => {
71
+ const newColumn = [...prevColumn];
72
+ const newCard = {
73
+ id: faker.string.uuid(),
74
+ title: faker.commerce.productName(),
75
+ description: faker.lorem.paragraph(),
76
+ image: faker.image.url(),
77
+ } satisfies CardItem;
78
+ newColumn.push(newCard);
79
+ console.log('[add card]', prevColumn.length, newColumn.length);
80
+ return newColumn;
81
+ });
82
+ }, []);
83
+
84
+ const handleRemoveCard = useCallback((cardId: string) => {
85
+ setColumn((prevColumn) => {
86
+ const newColumn = [...prevColumn];
87
+
88
+ const cardIndex = prevColumn.findIndex((card) => card.id === cardId);
89
+ if (cardIndex !== -1) {
90
+ newColumn.splice(cardIndex, 1);
91
+ }
92
+
93
+ return newColumn;
94
+ });
95
+ }, []);
96
+
97
+ return (
98
+ <CardStack.Root classNames='is-96'>
99
+ <CardStack.Content>
100
+ <CardStack.Stack id='story column' onRearrange={handleRearrange} itemsCount={column.length}>
101
+ {column.map((card, cardIndex, cardsArray) => {
102
+ const cardItem = { id: card.id, type: 'card' as const };
103
+ const prevCardId = cardIndex > 0 ? cardsArray[cardIndex - 1].id : undefined;
104
+ const nextCardId = cardIndex < cardsArray.length - 1 ? cardsArray[cardIndex + 1].id : undefined;
105
+
106
+ return (
107
+ <CardStack.Item asChild key={card.id}>
108
+ <StackItem.Root
109
+ item={cardItem}
110
+ focusIndicatorVariant='group'
111
+ prevSiblingId={prevCardId}
112
+ nextSiblingId={nextCardId}
113
+ >
114
+ <Card.StaticRoot>
115
+ <Card.Toolbar>
116
+ <StackItem.DragHandle asChild>
117
+ <Card.DragHandle toolbarItem />
118
+ </StackItem.DragHandle>
119
+ <Card.ToolbarSeparator variant='gap' />
120
+ <Card.ToolbarIconButton
121
+ iconOnly
122
+ variant='ghost'
123
+ icon='ph--x--regular'
124
+ label='Remove card'
125
+ onClick={() => handleRemoveCard(card.id)}
126
+ />
127
+ </Card.Toolbar>
128
+ <Card.Poster alt={card.title} image={card.image} />
129
+ <Card.Heading>{card.title}</Card.Heading>
130
+ <Card.Text classNames='line-clamp-2'>{card.description}</Card.Text>
131
+ </Card.StaticRoot>
132
+ <StackItem.DragPreview>
133
+ {() => (
134
+ <CardDragPreview.Root>
135
+ <CardDragPreview.Content>
136
+ <Card.Toolbar>
137
+ <Card.DragHandle toolbarItem />
138
+ </Card.Toolbar>
139
+ <Card.Poster alt={card.title} image={card.image} />
140
+ <Card.Heading>{card.title}</Card.Heading>
141
+ <Card.Text classNames='line-clamp-2'>{card.description}</Card.Text>
142
+ </CardDragPreview.Content>
143
+ </CardDragPreview.Root>
144
+ )}
145
+ </StackItem.DragPreview>
146
+ </StackItem.Root>
147
+ </CardStack.Item>
148
+ );
149
+ })}
150
+ </CardStack.Stack>
151
+
152
+ <CardStack.Footer>
153
+ <IconButton icon='ph--plus--regular' label='Add card' onClick={handleAddCard} classNames='is-full' />
154
+ </CardStack.Footer>
155
+
156
+ <CardStack.Heading>{faker.company.name()}</CardStack.Heading>
157
+ </CardStack.Content>
158
+ </CardStack.Root>
159
+ );
160
+ };
161
+
162
+ const meta = {
163
+ title: 'ui/react-ui-stack/CardStack',
164
+ component: CardStackStory,
165
+ decorators: [withTheme, withLayout({ fullscreen: true })],
166
+ } satisfies Meta<typeof CardStackStory>;
167
+
168
+ export default meta;
169
+
170
+ type Story = StoryObj<typeof CardStackStory>;
171
+
172
+ export const Default: Story = {};
@@ -8,7 +8,7 @@ import React, { type ComponentPropsWithoutRef, forwardRef } from 'react';
8
8
  import type { ThemedClassName } from '@dxos/react-ui';
9
9
  import { mx } from '@dxos/react-ui-theme';
10
10
 
11
- import { railGridHorizontalContainFitContent, Stack, type StackProps } from '../../components';
11
+ import { Stack, type StackProps, railGridHorizontalContainFitContent } from '../../components';
12
12
  import { Card } from '../Card';
13
13
 
14
14
  type SharedCardStackProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> & { asChild?: boolean };
@@ -107,6 +107,22 @@ const CardStackRoot = forwardRef<HTMLDivElement, SharedCardStackProps>(
107
107
  },
108
108
  );
109
109
 
110
+ const cardStackItem = 'contain-layout pli-2 plb-1 first-of-type:pbs-0 last-of-type:pbe-0';
111
+
112
+ const CardStackItem = forwardRef<HTMLDivElement, SharedCardStackProps>(
113
+ ({ children, classNames, asChild, role = 'none', ...props }, forwardedRef) => {
114
+ const Root = asChild ? Slot : 'div';
115
+ const rootProps = asChild
116
+ ? { classNames: [cardStackItem, classNames] }
117
+ : { className: mx(cardStackItem, classNames), role };
118
+ return (
119
+ <Root {...props} {...rootProps} ref={forwardedRef}>
120
+ {children}
121
+ </Root>
122
+ );
123
+ },
124
+ );
125
+
110
126
  export const CardStack = {
111
127
  Root: CardStackRoot,
112
128
  Content: CardStackContent,
@@ -114,6 +130,7 @@ export const CardStack = {
114
130
  Heading: CardStackHeading,
115
131
  Footer: CardStackFooter,
116
132
  DragHandle: CardStackDragHandle,
133
+ Item: CardStackItem,
117
134
  };
118
135
 
119
- export { cardStackRoot, cardStackFooter, cardStackHeading, cardStackContent };
136
+ export { cardStackRoot, cardStackFooter, cardStackHeading, cardStackContent, cardStackItem };