@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.
- package/dist/lib/browser/{chunk-7JFD5ZHZ.mjs → chunk-F3PLXTDA.mjs} +21 -25
- package/dist/lib/browser/{chunk-7JFD5ZHZ.mjs.map → chunk-F3PLXTDA.mjs.map} +3 -3
- package/dist/lib/browser/{chunk-5SPBSIWS.mjs → chunk-HFJYTGHU.mjs} +11 -1
- package/dist/lib/browser/chunk-HFJYTGHU.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1042 -856
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +5 -5
- package/dist/lib/browser/testing/index.mjs.map +2 -2
- package/dist/lib/browser/translations.mjs +1 -1
- package/dist/lib/node-esm/{chunk-TTEL2FP2.mjs → chunk-LLVUYLAM.mjs} +21 -25
- package/dist/lib/node-esm/{chunk-TTEL2FP2.mjs.map → chunk-LLVUYLAM.mjs.map} +3 -3
- package/dist/lib/node-esm/{chunk-FPVTVXND.mjs → chunk-OJYUPUCY.mjs} +11 -1
- package/dist/lib/node-esm/chunk-OJYUPUCY.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1042 -856
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +5 -5
- package/dist/lib/node-esm/testing/index.mjs.map +2 -2
- package/dist/lib/node-esm/translations.mjs +1 -1
- package/dist/types/src/components/Button/Button.d.ts +2 -0
- package/dist/types/src/components/Button/Button.d.ts.map +1 -1
- package/dist/types/src/components/Button/IconButton.d.ts +0 -1
- package/dist/types/src/components/Button/IconButton.d.ts.map +1 -1
- package/dist/types/src/components/Button/IconButton.stories.d.ts +34 -0
- package/dist/types/src/components/Button/IconButton.stories.d.ts.map +1 -1
- package/dist/types/src/components/Button/SystemIconButton.d.ts +20 -0
- package/dist/types/src/components/Button/SystemIconButton.d.ts.map +1 -0
- package/dist/types/src/components/Button/ToggleIconButton.d.ts +15 -0
- package/dist/types/src/components/Button/ToggleIconButton.d.ts.map +1 -0
- package/dist/types/src/components/Button/index.d.ts +2 -0
- package/dist/types/src/components/Button/index.d.ts.map +1 -1
- package/dist/types/src/components/Card/Card.d.ts +25 -19
- package/dist/types/src/components/Card/Card.d.ts.map +1 -1
- package/dist/types/src/components/Card/Card.stories.d.ts +1 -1
- package/dist/types/src/components/Card/Card.stories.d.ts.map +1 -1
- package/dist/types/src/components/Card/Card.theme.d.ts.map +1 -1
- package/dist/types/src/components/Clipboard/index.d.ts +1 -0
- package/dist/types/src/components/Clipboard/index.d.ts.map +1 -1
- package/dist/types/src/components/Column/Column.d.ts +19 -1
- package/dist/types/src/components/Column/Column.d.ts.map +1 -1
- package/dist/types/src/components/Column/Column.theme.d.ts +8 -0
- package/dist/types/src/components/Column/Column.theme.d.ts.map +1 -1
- package/dist/types/src/components/Input/constants.d.ts +3 -0
- package/dist/types/src/components/Input/constants.d.ts.map +1 -1
- package/dist/types/src/components/Message/Message.theme.d.ts.map +1 -1
- package/dist/types/src/components/Toast/Toast.d.ts.map +1 -1
- package/dist/types/src/components/Toast/Toast.theme.d.ts.map +1 -1
- package/dist/types/src/components/Tooltip/TextTooltip.d.ts +19 -0
- package/dist/types/src/components/Tooltip/TextTooltip.d.ts.map +1 -0
- package/dist/types/src/components/Tooltip/index.d.ts +1 -0
- package/dist/types/src/components/Tooltip/index.d.ts.map +1 -1
- package/dist/types/src/testing/Loading.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +10 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -16
- package/src/components/Button/Button.tsx +23 -2
- package/src/components/Button/IconButton.stories.tsx +81 -1
- package/src/components/Button/IconButton.tsx +2 -4
- package/src/components/Button/SystemIconButton.tsx +153 -0
- package/src/components/Button/ToggleIconButton.tsx +32 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Card/Card.stories.tsx +69 -27
- package/src/components/Card/Card.theme.ts +27 -9
- package/src/components/Card/Card.tsx +165 -88
- package/src/components/Column/Column.theme.ts +32 -4
- package/src/components/Column/Column.tsx +36 -1
- package/src/components/Input/constants.ts +3 -0
- package/src/components/Message/Message.theme.ts +0 -10
- package/src/components/Message/Message.tsx +4 -4
- package/src/components/Popover/Popover.theme.ts +1 -1
- package/src/components/Toast/Toast.theme.ts +0 -6
- package/src/components/Toast/Toast.tsx +11 -10
- package/src/components/Tooltip/TextTooltip.tsx +54 -0
- package/src/components/Tooltip/index.ts +1 -0
- package/src/testing/Loading.tsx +2 -1
- package/src/testing/decorators/withLayout.tsx +2 -2
- package/src/translations.ts +10 -0
- package/dist/lib/browser/chunk-5SPBSIWS.mjs.map +0 -7
- 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.
|
|
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.
|
|
89
|
-
"@dxos/
|
|
90
|
-
"@dxos/lit-ui": "0.9.
|
|
91
|
-
"@dxos/
|
|
92
|
-
"@dxos/log": "0.9.
|
|
93
|
-
"@dxos/react-
|
|
94
|
-
"@dxos/
|
|
95
|
-
"@dxos/react-
|
|
96
|
-
"@dxos/
|
|
97
|
-
"@dxos/
|
|
98
|
-
"@dxos/util": "0.9.
|
|
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/
|
|
113
|
-
"@dxos/
|
|
114
|
-
"@dxos/
|
|
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.
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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):
|
|
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,
|
|
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
|
+
);
|
|
@@ -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
|
|
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
|
|
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: [
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|