@dxos/react-ui 0.9.0 → 0.9.1-main.c7dcc2e112

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 (81) hide show
  1. package/dist/lib/browser/{chunk-7JFD5ZHZ.mjs → chunk-F3PLXTDA.mjs} +21 -25
  2. package/dist/lib/browser/{chunk-7JFD5ZHZ.mjs.map → chunk-F3PLXTDA.mjs.map} +3 -3
  3. package/dist/lib/browser/{chunk-5SPBSIWS.mjs → chunk-HFJYTGHU.mjs} +11 -1
  4. package/dist/lib/browser/chunk-HFJYTGHU.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1042 -856
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +5 -5
  9. package/dist/lib/browser/testing/index.mjs.map +2 -2
  10. package/dist/lib/browser/translations.mjs +1 -1
  11. package/dist/lib/node-esm/{chunk-TTEL2FP2.mjs → chunk-LLVUYLAM.mjs} +21 -25
  12. package/dist/lib/node-esm/{chunk-TTEL2FP2.mjs.map → chunk-LLVUYLAM.mjs.map} +3 -3
  13. package/dist/lib/node-esm/{chunk-FPVTVXND.mjs → chunk-OJYUPUCY.mjs} +11 -1
  14. package/dist/lib/node-esm/chunk-OJYUPUCY.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1042 -856
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +5 -5
  19. package/dist/lib/node-esm/testing/index.mjs.map +2 -2
  20. package/dist/lib/node-esm/translations.mjs +1 -1
  21. package/dist/types/src/components/Button/Button.d.ts +2 -0
  22. package/dist/types/src/components/Button/Button.d.ts.map +1 -1
  23. package/dist/types/src/components/Button/IconButton.d.ts +0 -1
  24. package/dist/types/src/components/Button/IconButton.d.ts.map +1 -1
  25. package/dist/types/src/components/Button/IconButton.stories.d.ts +34 -0
  26. package/dist/types/src/components/Button/IconButton.stories.d.ts.map +1 -1
  27. package/dist/types/src/components/Button/SystemIconButton.d.ts +20 -0
  28. package/dist/types/src/components/Button/SystemIconButton.d.ts.map +1 -0
  29. package/dist/types/src/components/Button/ToggleIconButton.d.ts +15 -0
  30. package/dist/types/src/components/Button/ToggleIconButton.d.ts.map +1 -0
  31. package/dist/types/src/components/Button/index.d.ts +2 -0
  32. package/dist/types/src/components/Button/index.d.ts.map +1 -1
  33. package/dist/types/src/components/Card/Card.d.ts +25 -19
  34. package/dist/types/src/components/Card/Card.d.ts.map +1 -1
  35. package/dist/types/src/components/Card/Card.stories.d.ts +1 -1
  36. package/dist/types/src/components/Card/Card.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/Card/Card.theme.d.ts.map +1 -1
  38. package/dist/types/src/components/Clipboard/index.d.ts +1 -0
  39. package/dist/types/src/components/Clipboard/index.d.ts.map +1 -1
  40. package/dist/types/src/components/Column/Column.d.ts +19 -1
  41. package/dist/types/src/components/Column/Column.d.ts.map +1 -1
  42. package/dist/types/src/components/Column/Column.theme.d.ts +8 -0
  43. package/dist/types/src/components/Column/Column.theme.d.ts.map +1 -1
  44. package/dist/types/src/components/Input/constants.d.ts +3 -0
  45. package/dist/types/src/components/Input/constants.d.ts.map +1 -1
  46. package/dist/types/src/components/Message/Message.theme.d.ts.map +1 -1
  47. package/dist/types/src/components/Toast/Toast.d.ts.map +1 -1
  48. package/dist/types/src/components/Toast/Toast.theme.d.ts.map +1 -1
  49. package/dist/types/src/components/Tooltip/TextTooltip.d.ts +19 -0
  50. package/dist/types/src/components/Tooltip/TextTooltip.d.ts.map +1 -0
  51. package/dist/types/src/components/Tooltip/index.d.ts +1 -0
  52. package/dist/types/src/components/Tooltip/index.d.ts.map +1 -1
  53. package/dist/types/src/testing/Loading.d.ts.map +1 -1
  54. package/dist/types/src/translations.d.ts +10 -0
  55. package/dist/types/src/translations.d.ts.map +1 -1
  56. package/dist/types/tsconfig.tsbuildinfo +1 -1
  57. package/package.json +16 -16
  58. package/src/components/Button/Button.tsx +23 -2
  59. package/src/components/Button/IconButton.stories.tsx +81 -1
  60. package/src/components/Button/IconButton.tsx +2 -4
  61. package/src/components/Button/SystemIconButton.tsx +153 -0
  62. package/src/components/Button/ToggleIconButton.tsx +32 -0
  63. package/src/components/Button/index.ts +2 -0
  64. package/src/components/Card/Card.stories.tsx +69 -27
  65. package/src/components/Card/Card.theme.ts +27 -9
  66. package/src/components/Card/Card.tsx +165 -88
  67. package/src/components/Column/Column.theme.ts +32 -4
  68. package/src/components/Column/Column.tsx +36 -1
  69. package/src/components/Input/constants.ts +3 -0
  70. package/src/components/Message/Message.theme.ts +0 -10
  71. package/src/components/Message/Message.tsx +4 -4
  72. package/src/components/Popover/Popover.theme.ts +1 -1
  73. package/src/components/Toast/Toast.theme.ts +0 -6
  74. package/src/components/Toast/Toast.tsx +11 -10
  75. package/src/components/Tooltip/TextTooltip.tsx +54 -0
  76. package/src/components/Tooltip/index.ts +1 -0
  77. package/src/testing/Loading.tsx +2 -1
  78. package/src/testing/decorators/withLayout.tsx +2 -2
  79. package/src/translations.ts +10 -0
  80. package/dist/lib/browser/chunk-5SPBSIWS.mjs.map +0 -7
  81. package/dist/lib/node-esm/chunk-FPVTVXND.mjs.map +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui",
3
- "version": "0.9.0",
3
+ "version": "0.9.1-main.c7dcc2e112",
4
4
  "description": "Low-level React components for DXOS, applying a theme to a core group of primitives",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -85,17 +85,17 @@
85
85
  "react-error-boundary": "^4.0.13",
86
86
  "react-i18next": "^11.18.6",
87
87
  "react-remove-scroll": "^2.6.0",
88
- "@dxos/async": "0.9.0",
89
- "@dxos/invariant": "0.9.0",
90
- "@dxos/lit-ui": "0.9.0",
91
- "@dxos/react-error-boundary": "0.9.0",
92
- "@dxos/log": "0.9.0",
93
- "@dxos/react-hooks": "0.9.0",
94
- "@dxos/debug": "0.9.0",
95
- "@dxos/react-input": "0.9.0",
96
- "@dxos/ui-types": "0.9.0",
97
- "@dxos/react-list": "0.9.0",
98
- "@dxos/util": "0.9.0"
88
+ "@dxos/async": "0.9.1-main.c7dcc2e112",
89
+ "@dxos/debug": "0.9.1-main.c7dcc2e112",
90
+ "@dxos/lit-ui": "0.9.1-main.c7dcc2e112",
91
+ "@dxos/invariant": "0.9.1-main.c7dcc2e112",
92
+ "@dxos/log": "0.9.1-main.c7dcc2e112",
93
+ "@dxos/react-error-boundary": "0.9.1-main.c7dcc2e112",
94
+ "@dxos/react-input": "0.9.1-main.c7dcc2e112",
95
+ "@dxos/react-hooks": "0.9.1-main.c7dcc2e112",
96
+ "@dxos/react-list": "0.9.1-main.c7dcc2e112",
97
+ "@dxos/ui-types": "0.9.1-main.c7dcc2e112",
98
+ "@dxos/util": "0.9.1-main.c7dcc2e112"
99
99
  },
100
100
  "devDependencies": {
101
101
  "@dnd-kit/core": "^6.0.5",
@@ -109,14 +109,14 @@
109
109
  "react-dom": "~19.2.3",
110
110
  "tabster": "^8.5.5",
111
111
  "vite": "^8.0.16",
112
- "@dxos/random": "0.9.0",
113
- "@dxos/ui-theme": "0.9.0",
114
- "@dxos/util": "0.9.0"
112
+ "@dxos/ui-theme": "0.9.1-main.c7dcc2e112",
113
+ "@dxos/util": "0.9.1-main.c7dcc2e112",
114
+ "@dxos/random": "0.9.1-main.c7dcc2e112"
115
115
  },
116
116
  "peerDependencies": {
117
117
  "react": "~19.2.3",
118
118
  "react-dom": "~19.2.3",
119
- "@dxos/ui-theme": "0.9.0"
119
+ "@dxos/ui-theme": "0.9.1-main.c7dcc2e112"
120
120
  },
121
121
  "publishConfig": {
122
122
  "access": "public"
@@ -11,12 +11,15 @@ import { type Density, type Elevation } from '@dxos/ui-types';
11
11
 
12
12
  import { useDensityContext, useElevationContext, useThemeContext } from '../../hooks';
13
13
  import { type ThemedClassName } from '../../util';
14
+ import { Icon } from '../Icon';
14
15
 
15
16
  type ButtonProps = ThemedClassName<ComponentPropsWithRef<typeof Primitive.button>> & {
16
17
  variant?: 'default' | 'primary' | 'outline' | 'ghost' | 'destructive' | 'valence';
17
18
  density?: Density;
18
19
  elevation?: Elevation;
19
20
  asChild?: boolean;
21
+ /** Render a trailing caret indicating the button opens a menu. */
22
+ caretDown?: boolean;
20
23
  };
21
24
 
22
25
  type ButtonGroupContextValue = { inGroup?: boolean };
@@ -31,7 +34,16 @@ const [ButtonGroupProvider, useButtonGroupContext] = createContext<ButtonGroupCo
31
34
  const Button = memo(
32
35
  forwardRef<HTMLButtonElement, ButtonProps>(
33
36
  (
34
- { classNames, children, density: densityProp, elevation: elevationProp, variant = 'default', asChild, ...props },
37
+ {
38
+ classNames,
39
+ children,
40
+ density: densityProp,
41
+ elevation: elevationProp,
42
+ variant = 'default',
43
+ asChild,
44
+ caretDown,
45
+ ...props
46
+ },
35
47
  ref,
36
48
  ) => {
37
49
  const { inGroup } = useButtonGroupContext(BUTTON_NAME);
@@ -59,7 +71,16 @@ const Button = memo(
59
71
  )}
60
72
  {...(props.disabled && { disabled: true })}
61
73
  >
62
- {children}
74
+ {/* `asChild` forwards a single child via Slot (React.Children.only); only add the caret in
75
+ the non-`asChild` case so Slot still receives exactly one child. */}
76
+ {caretDown && !asChild ? (
77
+ <>
78
+ {children}
79
+ <Icon size={3} icon='ph--caret-down--bold' />
80
+ </>
81
+ ) : (
82
+ children
83
+ )}
63
84
  </Comp>
64
85
  );
65
86
  },
@@ -3,14 +3,16 @@
3
3
  //
4
4
 
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
- import React from 'react';
6
+ import React, { type ReactNode, useState } from 'react';
7
7
 
8
8
  import { type Density } from '@dxos/ui-types';
9
9
 
10
10
  import { withTheme } from '../../testing';
11
+ import { translations } from '../../translations';
11
12
  import { Tooltip } from '../Tooltip';
12
13
  import { Button } from './Button';
13
14
  import { IconButton, type IconButtonProps } from './IconButton';
15
+ import { SystemIconButton } from './SystemIconButton';
14
16
 
15
17
  const DefaultStory = (props: IconButtonProps) => {
16
18
  return (
@@ -60,6 +62,76 @@ const DensitiesStory = (props: Omit<IconButtonProps, 'density' | 'size'>) => {
60
62
  );
61
63
  };
62
64
 
65
+ type SystemPresetVariantProps = Partial<Pick<IconButtonProps, 'variant' | 'iconOnly'>>;
66
+
67
+ const SystemPresetRow = ({
68
+ name,
69
+ button,
70
+ }: {
71
+ name: string;
72
+ button: (props: SystemPresetVariantProps) => ReactNode;
73
+ }) => (
74
+ <div className='grid grid-cols-subgrid col-span-full gap-x-8 items-center'>
75
+ <div className='text-xs font-mono'>{name}</div>
76
+ <div>{button({})}</div>
77
+ <div>{button({ variant: 'ghost' })}</div>
78
+ <div>{button({ variant: 'ghost', iconOnly: true })}</div>
79
+ </div>
80
+ );
81
+
82
+ const SystemStory = () => {
83
+ const [state, setState] = useState<Record<string, boolean>>({});
84
+
85
+ return (
86
+ <Tooltip.Provider>
87
+ <div className='grid grid-cols-[auto_1fr_1fr_auto] gap-y-3 items-center'>
88
+ <div className='grid grid-cols-subgrid col-span-full gap-x-8'>
89
+ <div />
90
+ <div className='text-xs text-subdued uppercase'>default</div>
91
+ <div className='text-xs text-subdued uppercase'>ghost</div>
92
+ <div className='text-xs text-subdued uppercase'>iconOnly</div>
93
+ </div>
94
+
95
+ <SystemPresetRow
96
+ name='Star'
97
+ button={(props) => (
98
+ <SystemIconButton.Star
99
+ active={state.star}
100
+ onClick={() => setState((prev) => ({ ...prev, star: !prev.star }))}
101
+ {...props}
102
+ />
103
+ )}
104
+ />
105
+ <SystemPresetRow
106
+ name='Bookmark'
107
+ button={(props) => (
108
+ <SystemIconButton.Bookmark
109
+ active={state.bookmark}
110
+ onClick={() => setState((prev) => ({ ...prev, bookmark: !prev.bookmark }))}
111
+ {...props}
112
+ />
113
+ )}
114
+ />
115
+ <SystemPresetRow
116
+ name='Expander'
117
+ button={(props) => (
118
+ <SystemIconButton.Expander
119
+ active={state.expander}
120
+ onClick={() => setState((prev) => ({ ...prev, expander: !prev.expander }))}
121
+ {...props}
122
+ />
123
+ )}
124
+ />
125
+ <br />
126
+ <SystemPresetRow name='Add' button={(props) => <SystemIconButton.Add {...props} />} />
127
+ <SystemPresetRow name='Delete' button={(props) => <SystemIconButton.Delete {...props} />} />
128
+ <SystemPresetRow name='Edit' button={(props) => <SystemIconButton.Edit {...props} />} />
129
+ <SystemPresetRow name='Close' button={(props) => <SystemIconButton.Close {...props} />} />
130
+ </div>
131
+ </Tooltip.Provider>
132
+ );
133
+ };
134
+
63
135
  const meta = {
64
136
  title: 'ui/react-ui-core/components/IconButton',
65
137
  component: IconButton,
@@ -67,6 +139,7 @@ const meta = {
67
139
  decorators: [withTheme()],
68
140
  parameters: {
69
141
  layout: 'centered',
142
+ translations,
70
143
  },
71
144
  } satisfies Meta<typeof IconButton>;
72
145
 
@@ -90,3 +163,10 @@ export const Ghost: Story = {
90
163
  variant: 'ghost',
91
164
  },
92
165
  };
166
+
167
+ export const System: Story = {
168
+ render: SystemStory,
169
+ args: {
170
+ label: 'System',
171
+ },
172
+ };
@@ -14,9 +14,8 @@ type IconButtonProps = Omit<ButtonProps, 'children'> &
14
14
  Partial<Pick<IconProps, 'icon' | 'size'>> & {
15
15
  label: string;
16
16
  noTooltip?: boolean;
17
- caretDown?: boolean;
18
17
  iconOnly?: boolean;
19
- square?: boolean; // TODO(burdon): Handle more uniformly.
18
+ square?: boolean; // TODO(burdon): Should be automatic in style?
20
19
  iconEnd?: boolean;
21
20
  iconClassNames?: ThemedClassName<any>['classNames'];
22
21
  tooltipSide?: TooltipSide;
@@ -46,7 +45,7 @@ const IconOnlyButton = forwardRef<HTMLButtonElement, IconButtonProps>(
46
45
 
47
46
  const LabelledIconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
48
47
  (
49
- { size, icon, iconOnly, square, iconEnd, iconClassNames, label, caretDown, noTooltip: _, classNames, ...props },
48
+ { size, icon, iconOnly, square, iconEnd, iconClassNames, label, noTooltip: _, classNames, ...props },
50
49
  forwardedRef,
51
50
  ) => {
52
51
  const { tx } = useThemeContext();
@@ -55,7 +54,6 @@ const LabelledIconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
55
54
  {icon && !iconEnd && <Icon icon={icon} size={size} classNames={iconClassNames} />}
56
55
  <span className={iconOnly ? 'sr-only' : undefined}>{label}</span>
57
56
  {icon && iconEnd && <Icon icon={icon} size={size} classNames={iconClassNames} />}
58
- {caretDown && <Icon size={3} icon='ph--caret-down--bold' />}
59
57
  </Button>
60
58
  );
61
59
  },
@@ -0,0 +1,153 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React, { forwardRef } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+
8
+ import { translationKey } from '#translations';
9
+
10
+ import { IconButton, type IconButtonProps } from './IconButton';
11
+ import { ToggleIconButton, type ToggleIconButtonProps } from './ToggleIconButton';
12
+
13
+ // Static presets fix the icon and default the label; callers can still override `label`.
14
+ type StaticPresetProps = Omit<IconButtonProps, 'icon' | 'label'> & { label?: string };
15
+
16
+ // Stateful presets fix both icons so a call site cannot supply a conflicting glyph.
17
+ type TogglePresetProps = Omit<ToggleIconButtonProps, 'icon' | 'activeIcon' | 'label'> & { label?: string };
18
+
19
+ //
20
+ // Star
21
+ //
22
+
23
+ const StarIconButton = forwardRef<HTMLButtonElement, TogglePresetProps>(({ label, active, ...props }, forwardedRef) => {
24
+ const { t } = useTranslation(translationKey);
25
+ return (
26
+ <ToggleIconButton
27
+ {...props}
28
+ active={active}
29
+ icon='ph--star--regular'
30
+ activeIcon='ph--star--fill'
31
+ label={label ?? t(active ? 'system-button.unstar.label' : 'system-button.star.label')}
32
+ ref={forwardedRef}
33
+ />
34
+ );
35
+ });
36
+
37
+ StarIconButton.displayName = 'SystemIconButton.Star';
38
+
39
+ //
40
+ // Bookmark
41
+ //
42
+
43
+ const BookmarkIconButton = forwardRef<HTMLButtonElement, TogglePresetProps>(
44
+ ({ label, active, ...props }, forwardedRef) => {
45
+ const { t } = useTranslation(translationKey);
46
+ return (
47
+ <ToggleIconButton
48
+ {...props}
49
+ active={active}
50
+ icon='ph--bookmark-simple--regular'
51
+ activeIcon='ph--bookmark-simple--fill'
52
+ label={label ?? t(active ? 'system-button.unbookmark.label' : 'system-button.bookmark.label')}
53
+ ref={forwardedRef}
54
+ />
55
+ );
56
+ },
57
+ );
58
+
59
+ BookmarkIconButton.displayName = 'SystemIconButton.Bookmark';
60
+
61
+ //
62
+ // Expander
63
+ //
64
+
65
+ const ExpanderIconButton = forwardRef<HTMLButtonElement, TogglePresetProps>(
66
+ ({ label, active, ...props }, forwardedRef) => {
67
+ const { t } = useTranslation(translationKey);
68
+ return (
69
+ <ToggleIconButton
70
+ {...props}
71
+ active={active}
72
+ icon='ph--caret-right--regular'
73
+ label={label ?? t(active ? 'system-button.collapse.label' : 'system-button.expand.label')}
74
+ ref={forwardedRef}
75
+ />
76
+ );
77
+ },
78
+ );
79
+
80
+ ExpanderIconButton.displayName = 'SystemIconButton.Expander';
81
+
82
+ //
83
+ // Add
84
+ //
85
+
86
+ const AddIconButton = forwardRef<HTMLButtonElement, StaticPresetProps>(({ label, ...props }, forwardedRef) => {
87
+ const { t } = useTranslation(translationKey);
88
+ return (
89
+ <IconButton {...props} icon='ph--plus--regular' label={label ?? t('system-button.add.label')} ref={forwardedRef} />
90
+ );
91
+ });
92
+
93
+ AddIconButton.displayName = 'SystemIconButton.Add';
94
+
95
+ //
96
+ // Delete
97
+ //
98
+
99
+ const DeleteIconButton = forwardRef<HTMLButtonElement, StaticPresetProps>(({ label, ...props }, forwardedRef) => {
100
+ const { t } = useTranslation(translationKey);
101
+ return (
102
+ <IconButton
103
+ {...props}
104
+ icon='ph--trash--regular'
105
+ label={label ?? t('system-button.delete.label')}
106
+ ref={forwardedRef}
107
+ />
108
+ );
109
+ });
110
+
111
+ DeleteIconButton.displayName = 'SystemIconButton.Delete';
112
+
113
+ //
114
+ // Edit
115
+ //
116
+
117
+ const EditIconButton = forwardRef<HTMLButtonElement, StaticPresetProps>(({ label, ...props }, forwardedRef) => {
118
+ const { t } = useTranslation(translationKey);
119
+ return (
120
+ <IconButton {...props} icon='ph--pen--regular' label={label ?? t('system-button.edit.label')} ref={forwardedRef} />
121
+ );
122
+ });
123
+
124
+ EditIconButton.displayName = 'SystemIconButton.Edit';
125
+
126
+ //
127
+ // Close
128
+ //
129
+
130
+ const CloseIconButton = forwardRef<HTMLButtonElement, StaticPresetProps>(({ label, ...props }, forwardedRef) => {
131
+ const { t } = useTranslation(translationKey);
132
+ return (
133
+ <IconButton {...props} icon='ph--x--regular' label={label ?? t('system-button.close.label')} ref={forwardedRef} />
134
+ );
135
+ });
136
+
137
+ CloseIconButton.displayName = 'SystemIconButton.Close';
138
+
139
+ //
140
+ // Namespace
141
+ //
142
+
143
+ export const SystemIconButton = {
144
+ Star: StarIconButton,
145
+ Bookmark: BookmarkIconButton,
146
+ Expander: ExpanderIconButton,
147
+ Add: AddIconButton,
148
+ Delete: DeleteIconButton,
149
+ Edit: EditIconButton,
150
+ Close: CloseIconButton,
151
+ };
152
+
153
+ export type { StaticPresetProps, TogglePresetProps };
@@ -0,0 +1,32 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React, { forwardRef } from 'react';
6
+
7
+ import { mx } from '@dxos/ui-theme';
8
+
9
+ import { IconButton, type IconButtonProps } from './IconButton';
10
+
11
+ export type ToggleIconButtonProps = Omit<IconButtonProps, 'icon'> & {
12
+ active?: boolean;
13
+ /** Icon for the inactive state; rotated 90° in the active state when `activeIcon` is omitted. */
14
+ icon: NonNullable<IconButtonProps['icon']>;
15
+ /** Icon for the active state. When omitted, `icon` rotates 90° (disclosure-style). */
16
+ activeIcon?: NonNullable<IconButtonProps['icon']>;
17
+ };
18
+
19
+ /**
20
+ * Icon button bound to a boolean toggle. With `activeIcon` it swaps icons between states;
21
+ * otherwise it rotates `icon` 90° to signal the active (e.g. expanded) state.
22
+ */
23
+ export const ToggleIconButton = forwardRef<HTMLButtonElement, ToggleIconButtonProps>(
24
+ ({ active, icon, activeIcon, iconClassNames, ...props }, forwardedRef) => (
25
+ <IconButton
26
+ {...props}
27
+ ref={forwardedRef}
28
+ icon={active && activeIcon ? activeIcon : icon}
29
+ iconClassNames={activeIcon ? iconClassNames : mx('transition-transform', active && 'rotate-90', iconClassNames)}
30
+ />
31
+ ),
32
+ );
@@ -4,5 +4,7 @@
4
4
 
5
5
  export * from './Button';
6
6
  export * from './IconButton';
7
+ export * from './SystemIconButton';
7
8
  export * from './Toggle';
8
9
  export * from './ToggleGroup';
10
+ export * from './ToggleIconButton';
@@ -6,7 +6,7 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React, { useRef } from 'react';
7
7
 
8
8
  import { random } from '@dxos/random';
9
- import { Icon } from '@dxos/react-ui';
9
+ import { Icon, IconButton } from '@dxos/react-ui';
10
10
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
11
 
12
12
  import { Card } from './Card';
@@ -32,10 +32,16 @@ const DefaultStory = ({ title, description, image, fullWidth }: DefaultStoryProp
32
32
  </Card.Header>
33
33
  <Card.Body>
34
34
  <Card.Poster alt='Card.Poster' image={image} />
35
- <Card.Row icon='ph--dot-outline--regular'>
35
+ <Card.Row>
36
+ <Card.Block>
37
+ <Icon icon='ph--dot-outline--regular' />
38
+ </Card.Block>
36
39
  <Card.Text>Card.Text (default)</Card.Text>
37
40
  </Card.Row>
38
- <Card.Row icon='ph--dot-outline--regular'>
41
+ <Card.Row>
42
+ <Card.Block>
43
+ <Icon icon='ph--dot-outline--regular' />
44
+ </Card.Block>
39
45
  <Card.Text variant='description'>
40
46
  Card.Text (description)
41
47
  <br />
@@ -52,7 +58,13 @@ const DefaultStory = ({ title, description, image, fullWidth }: DefaultStoryProp
52
58
  const meta = {
53
59
  title: 'ui/react-ui-core/components/Card',
54
60
  render: DefaultStory,
55
- decorators: [withTheme(), withLayout({ layout: 'centered', classNames: 'grid w-[30rem] place-items-center' })],
61
+ decorators: [
62
+ withTheme(),
63
+ withLayout({
64
+ layout: 'centered',
65
+ classNames: 'grid w-[30rem] place-items-center bg-transparent',
66
+ }),
67
+ ],
56
68
  } satisfies Meta<typeof DefaultStory>;
57
69
 
58
70
  export default meta;
@@ -147,26 +159,56 @@ export const Description: Story = {
147
159
  },
148
160
  };
149
161
 
150
- export const Mock = () => (
151
- <div className='grid grid-cols-[2rem_1fr_2rem] w-full dx-card-min-width dx-card-max-width border border-separator rounded-xs'>
152
- <div className='grid grid-cols-subgrid col-span-full'>
153
- <div className='grid h-[var(--dx-rail-item)] w-[var(--dx-rail-item)] place-items-center'>
154
- <Icon icon='ph--dots-six-vertical--regular' />
155
- </div>
156
- <div className='p-1 whitespace-normal break-words text-description items-center'>
157
- This line is very very long and it should wrap.
158
- </div>
159
- <div className='grid h-[var(--dx-rail-item)] w-[var(--dx-rail-item)] place-items-center'>
160
- <Icon icon='ph--x--regular' />
161
- </div>
162
- </div>
163
- <div className='grid grid-cols-subgrid col-span-3'>
164
- <div className='grid h-[var(--dx-rail-item)] w-[var(--dx-rail-item)] place-items-center'>
165
- <Icon icon='ph--dots-six-vertical--regular' />
166
- </div>
167
- <div className='p-1 text-description items-center col-span-2'>
168
- This line is very very long and it should wrap.
169
- </div>
170
- </div>
171
- </div>
172
- );
162
+ export const Slots: Story = {
163
+ args: {
164
+ title: random.commerce.productName(),
165
+ },
166
+ render: ({ title }) => {
167
+ const showLeading = true;
168
+ return (
169
+ <Card.Root>
170
+ <Card.Header>
171
+ <Card.Title>{title}</Card.Title>
172
+ <Card.ActionIconButton action='close' onClick={() => console.log('close')} />
173
+ </Card.Header>
174
+ <Card.Body>
175
+ {/* Leading passive icon. */}
176
+ <Card.Row>
177
+ <Card.Block>
178
+ <Icon icon='ph--user--regular' />
179
+ </Card.Block>
180
+ <Card.Text>Start slot passive Icon</Card.Text>
181
+ </Card.Row>
182
+ {/* Passive icon + trailing IconButton: the two gutters align to the pixel. */}
183
+ <Card.Row>
184
+ <Card.Block>
185
+ <Icon icon='ph--at--regular' />
186
+ </Card.Block>
187
+ <Card.Text>Start Icon + end IconButton (aligned)</Card.Text>
188
+ <Card.Block end>
189
+ <IconButton iconOnly variant='ghost' icon='ph--x--regular' label='Remove' onClick={() => {}} />
190
+ </Card.Block>
191
+ </Card.Row>
192
+ {/* Conditional leading slot: when falsy, content stays in the center track. */}
193
+ <Card.Row>
194
+ {showLeading && (
195
+ <Card.Block>
196
+ <Icon icon='ph--calendar--regular' />
197
+ </Card.Block>
198
+ )}
199
+ <Card.Text>Conditional start slot</Card.Text>
200
+ </Card.Row>
201
+ {/* asChild: the anchor itself becomes the trailing gutter cell. */}
202
+ <Card.Row>
203
+ <Card.Text>Trailing link via asChild</Card.Text>
204
+ <Card.Block end asChild>
205
+ <a href='https://dxos.org' target='_blank' rel='noreferrer'>
206
+ <Icon icon='ph--arrow-square-out--regular' />
207
+ </a>
208
+ </Card.Block>
209
+ </Card.Row>
210
+ </Card.Body>
211
+ </Card.Root>
212
+ );
213
+ },
214
+ };
@@ -17,8 +17,7 @@ export type CardStyleProps = {
17
17
 
18
18
  const root: ComponentFunction<CardStyleProps> = ({ border, fullWidth }, ...etc) =>
19
19
  mx(
20
- 'dx-card dx-card-min-width dx-card-max-width min-h-(--dx-rail-item) group/card relative overflow-hidden',
21
- 'bg-card-surface',
20
+ 'dx-card dx-card-surface dx-card-min-width dx-card-max-width min-h-(--dx-rail-item) group/card relative overflow-hidden',
22
21
  border && 'border border-subdued-separator rounded-sm dx-focus-ring-group-y-indicator',
23
22
  fullWidth && 'max-w-none!',
24
23
  ...etc,
@@ -26,13 +25,16 @@ const root: ComponentFunction<CardStyleProps> = ({ border, fullWidth }, ...etc)
26
25
 
27
26
  const header: ComponentFunction<CardStyleProps> = (_, ...etc) =>
28
27
  mx(
29
- 'dx-card__header dx-density-md bg-transparent p-0! gap-0! col-span-3 grid! grid-cols-subgrid! shadow-none [contain:none]',
28
+ 'dx-card__header col-span-3 grid grid-cols-subgrid items-center',
29
+ '[&>*:not(.dx-gutter)]:col-start-2 [&>*:not(.dx-gutter)>*]:col-start-2',
30
30
  ...etc,
31
31
  );
32
32
 
33
33
  const title: ComponentFunction<CardStyleProps> = (_props, ...etc) => mx('dx-card__title grow truncate', ...etc);
34
34
 
35
- const body: ComponentFunction<CardStyleProps> = (_props, ...etc) => mx('dx-card__body contents pb-1 last:pb-0', ...etc);
35
+ const body: ComponentFunction<CardStyleProps> = (_props, ...etc) => mx('dx-card__body contents', ...etc);
36
+
37
+ const block: ComponentFunction<CardStyleProps> = (_props, ...etc) => mx('dx-card__block', ...etc);
36
38
 
37
39
  const text: ComponentFunction<CardStyleProps> = ({ variant = 'default', truncate: _truncate }, ...etc) =>
38
40
  mx(
@@ -46,25 +48,40 @@ const textSpan: ComponentFunction<CardStyleProps> = ({ variant = 'default', trun
46
48
  mx(variant === 'description' && 'text-sm text-description line-clamp-3', truncate && 'truncate', ...etc);
47
49
 
48
50
  const poster: ComponentFunction<CardStyleProps> = (_props, ...etc) =>
49
- mx('dx-card__poster col-span-3 max-h-[200px]', ...etc);
51
+ mx('dx-card__poster col-span-3 max-h-[200px] select-none pointer-events-none', ...etc);
50
52
 
51
53
  const posterIcon: ComponentFunction<CardStyleProps> = (_props, ...etc) =>
52
54
  mx('dx-card__poster-icon col-span-3 grid place-items-center bg-input-surface text-subdued max-h-[200px]', ...etc);
53
55
 
54
56
  const action: ComponentFunction<CardStyleProps> = (_props, ...etc) =>
55
- mx('dx-card__acztion col-span-3 !grid grid-cols-subgrid p-0! w-full text-start overflow-hidden', ...etc);
57
+ mx(
58
+ 'dx-card__action col-span-3 grid grid-cols-subgrid [&>*:not(.dx-gutter)]:col-start-2 [&>*:not(.dx-gutter)>*]:col-start-2 p-0! gap-0! w-full text-start overflow-hidden',
59
+ ...etc,
60
+ );
56
61
 
57
62
  const actionLabel: ComponentFunction<CardStyleProps> = (_props, ...etc) =>
58
63
  mx('dx-card__action-label min-w-0 flex-1 truncate', ...etc);
59
64
 
60
65
  const link: ComponentFunction<CardStyleProps> = (_props, ...etc) =>
61
- mx('dx-card__link col-span-3 !grid grid-cols-subgrid group p-0! dx-button dx-focus-ring min-h-1!', ...etc);
66
+ mx(
67
+ 'dx-card__link col-span-3 grid grid-cols-subgrid [&>*:not(.dx-gutter)]:col-start-2 [&>*:not(.dx-gutter)>*]:col-start-2 group p-0! dx-button dx-focus-ring min-h-1!',
68
+ ...etc,
69
+ );
62
70
 
63
71
  const linkLabel: ComponentFunction<CardStyleProps> = (_props, ...etc) =>
64
- mx('dx-card__link-label min-w-0 flex-1 truncate', ...etc);
72
+ mx('dx-card__link-label min-w-0 flex-1 truncate text-sm!', ...etc);
65
73
 
66
74
  const row: ComponentFunction<CardStyleProps> = ({ fullWidth }, ...etc) =>
67
- mx('dx-card__row', fullWidth ? 'col-span-full' : 'col-span-3 grid grid-cols-subgrid', ...etc);
75
+ mx(
76
+ 'dx-card__row overflow-hidden',
77
+ fullWidth
78
+ ? 'col-span-full'
79
+ : // The `>*` selector reaches the real grid item when a content child is `display: contents`
80
+ // (e.g. `dx-avatar`), which the direct-child selector cannot target. It is inert for normal
81
+ // block children, whose inner nodes are not grid items of this row.
82
+ 'col-span-3 grid grid-cols-subgrid [&>*:not(.dx-gutter)]:col-start-2 [&>*:not(.dx-gutter)>*]:col-start-2',
83
+ ...etc,
84
+ );
68
85
 
69
86
  // NOTE: Direct children that lack an explicit `col-*` utility default to the
70
87
  // Column.Root center track (via `--dx-col`); see `ui-theme`'s `css/components/card.css`.
@@ -79,6 +96,7 @@ export const cardTheme: Theme<CardStyleProps> = {
79
96
  header,
80
97
  title,
81
98
  body,
99
+ block,
82
100
  row,
83
101
  section,
84
102
  'section-title': sectionTitle,