@dxos/react-ui 0.8.4-main.cb12b3f963 → 0.8.4-main.d05539e30a
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/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/browser/chunk-A5QCIG5R.mjs +24 -0
- package/dist/lib/browser/chunk-A5QCIG5R.mjs.map +7 -0
- package/dist/lib/browser/{chunk-BDBC6H6V.mjs → chunk-LY5XDQR5.mjs} +6 -8
- package/dist/lib/browser/chunk-LY5XDQR5.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +601 -287
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +1 -7
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/translations.mjs +4 -13
- package/dist/lib/browser/translations.mjs.map +4 -4
- package/dist/lib/node-esm/{chunk-3JSJK2ZY.mjs → chunk-NGKLIKP3.mjs} +6 -8
- package/dist/lib/node-esm/chunk-NGKLIKP3.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-XCFLA74M.mjs +26 -0
- package/dist/lib/node-esm/chunk-XCFLA74M.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +601 -287
- 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 +1 -7
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/translations.mjs +4 -14
- package/dist/lib/node-esm/translations.mjs.map +4 -4
- package/dist/types/src/components/Card/Card.d.ts +2 -5
- package/dist/types/src/components/Card/Card.d.ts.map +1 -1
- package/dist/types/src/components/Carousel/Carousel.d.ts +106 -0
- package/dist/types/src/components/Carousel/Carousel.d.ts.map +1 -0
- package/dist/types/src/components/Carousel/index.d.ts +2 -0
- package/dist/types/src/components/Carousel/index.d.ts.map +1 -0
- package/dist/types/src/components/Icon/Icon.d.ts +1 -0
- package/dist/types/src/components/Icon/Icon.d.ts.map +1 -1
- package/dist/types/src/components/Link/Link.d.ts.map +1 -1
- package/dist/types/src/components/List/ListDropIndicator.d.ts.map +1 -1
- package/dist/types/src/components/MediaPlayer/MediaPlayer.d.ts +46 -0
- package/dist/types/src/components/MediaPlayer/MediaPlayer.d.ts.map +1 -0
- package/dist/types/src/components/MediaPlayer/MediaPlayer.stories.d.ts +16 -0
- package/dist/types/src/components/MediaPlayer/MediaPlayer.stories.d.ts.map +1 -0
- package/dist/types/src/components/MediaPlayer/index.d.ts +2 -0
- package/dist/types/src/components/MediaPlayer/index.d.ts.map +1 -0
- package/dist/types/src/components/ScrollArea/ScrollArea.stories.d.ts.map +1 -1
- package/dist/types/src/components/ScrollContainer/ScrollContainer.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Tooltip/Tooltip.d.ts +6 -6
- package/dist/types/src/components/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/exemplars/slot.stories.d.ts.map +1 -1
- package/dist/types/src/exemplars/virtualizer.stories.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +5 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -18
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +1 -1
- package/src/components/Button/IconButton.stories.tsx +1 -1
- package/src/components/Card/Card.stories.tsx +3 -3
- package/src/components/Card/Card.tsx +24 -17
- package/src/components/Carousel/Carousel.tsx +379 -0
- package/src/components/Carousel/index.ts +5 -0
- package/src/components/Clipboard/CopyButton.tsx +2 -2
- package/src/components/Icon/Icon.tsx +10 -3
- package/src/components/Link/Link.tsx +10 -2
- package/src/components/List/List.stories.tsx +1 -1
- package/src/components/List/List.tsx +1 -1
- package/src/components/List/ListDropIndicator.tsx +0 -1
- package/src/components/List/Tree.stories.tsx +1 -1
- package/src/components/MediaPlayer/MediaPlayer.stories.tsx +50 -0
- package/src/components/MediaPlayer/MediaPlayer.tsx +153 -0
- package/src/components/MediaPlayer/index.ts +5 -0
- package/src/components/Message/Message.tsx +2 -2
- package/src/components/ScrollArea/ScrollArea.stories.tsx +1 -5
- package/src/components/ScrollContainer/ScrollContainer.tsx +1 -3
- package/src/components/Toolbar/Toolbar.tsx +2 -1
- package/src/components/Tooltip/Tooltip.stories.tsx +1 -1
- package/src/components/Tooltip/Tooltip.tsx +14 -13
- package/src/components/index.ts +2 -0
- package/src/exemplars/slot.stories.tsx +2 -4
- package/src/exemplars/virtualizer.stories.tsx +0 -1
- package/src/testing/decorators/withLayout.tsx +6 -16
- package/src/translations.ts +5 -0
- package/dist/lib/browser/chunk-BDBC6H6V.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-3JSJK2ZY.mjs.map +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.d05539e30a",
|
|
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",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/dxos/dxos"
|
|
10
10
|
},
|
|
11
|
-
"license": "
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
12
12
|
"author": "DXOS.org",
|
|
13
13
|
"sideEffects": false,
|
|
14
14
|
"type": "module",
|
|
@@ -83,17 +83,17 @@
|
|
|
83
83
|
"react-error-boundary": "^4.0.13",
|
|
84
84
|
"react-i18next": "^11.18.6",
|
|
85
85
|
"react-remove-scroll": "^2.6.0",
|
|
86
|
-
"@dxos/
|
|
87
|
-
"@dxos/
|
|
88
|
-
"@dxos/
|
|
89
|
-
"@dxos/
|
|
90
|
-
"@dxos/
|
|
91
|
-
"@dxos/react-hooks": "0.8.4-main.
|
|
92
|
-
"@dxos/
|
|
93
|
-
"@dxos/react-
|
|
94
|
-
"@dxos/react-
|
|
95
|
-
"@dxos/util": "0.8.4-main.
|
|
96
|
-
"@dxos/ui-types": "0.8.4-main.
|
|
86
|
+
"@dxos/async": "0.8.4-main.d05539e30a",
|
|
87
|
+
"@dxos/invariant": "0.8.4-main.d05539e30a",
|
|
88
|
+
"@dxos/log": "0.8.4-main.d05539e30a",
|
|
89
|
+
"@dxos/lit-ui": "0.8.4-main.d05539e30a",
|
|
90
|
+
"@dxos/react-error-boundary": "0.8.4-main.d05539e30a",
|
|
91
|
+
"@dxos/react-hooks": "0.8.4-main.d05539e30a",
|
|
92
|
+
"@dxos/debug": "0.8.4-main.d05539e30a",
|
|
93
|
+
"@dxos/react-list": "0.8.4-main.d05539e30a",
|
|
94
|
+
"@dxos/react-input": "0.8.4-main.d05539e30a",
|
|
95
|
+
"@dxos/util": "0.8.4-main.d05539e30a",
|
|
96
|
+
"@dxos/ui-types": "0.8.4-main.d05539e30a"
|
|
97
97
|
},
|
|
98
98
|
"devDependencies": {
|
|
99
99
|
"@dnd-kit/core": "^6.0.5",
|
|
@@ -106,15 +106,15 @@
|
|
|
106
106
|
"react": "~19.2.3",
|
|
107
107
|
"react-dom": "~19.2.3",
|
|
108
108
|
"tabster": "^8.5.5",
|
|
109
|
-
"vite": "^8.0.
|
|
110
|
-
"@dxos/
|
|
111
|
-
"@dxos/
|
|
112
|
-
"@dxos/
|
|
109
|
+
"vite": "^8.0.13",
|
|
110
|
+
"@dxos/ui-theme": "0.8.4-main.d05539e30a",
|
|
111
|
+
"@dxos/random": "0.8.4-main.d05539e30a",
|
|
112
|
+
"@dxos/util": "0.8.4-main.d05539e30a"
|
|
113
113
|
},
|
|
114
114
|
"peerDependencies": {
|
|
115
115
|
"react": "~19.2.3",
|
|
116
116
|
"react-dom": "~19.2.3",
|
|
117
|
-
"@dxos/ui-theme": "0.8.4-main.
|
|
117
|
+
"@dxos/ui-theme": "0.8.4-main.d05539e30a"
|
|
118
118
|
},
|
|
119
119
|
"publishConfig": {
|
|
120
120
|
"access": "public"
|
|
@@ -15,7 +15,7 @@ const DefaultStory = (props: BreadcrumbRootProps) => {
|
|
|
15
15
|
<Breadcrumb.List>
|
|
16
16
|
<Breadcrumb.ListItem>
|
|
17
17
|
<Breadcrumb.Link asChild>
|
|
18
|
-
<Button variant='ghost' classNames='px-0 text-base-
|
|
18
|
+
<Button variant='ghost' classNames='px-0 text-base-foreground font-normal'>
|
|
19
19
|
Grocery
|
|
20
20
|
</Button>
|
|
21
21
|
</Breadcrumb.Link>
|
|
@@ -13,7 +13,7 @@ import { IconButton, type IconButtonProps } from './IconButton';
|
|
|
13
13
|
const DefaultStory = (props: IconButtonProps) => {
|
|
14
14
|
return (
|
|
15
15
|
<Tooltip.Provider>
|
|
16
|
-
<div
|
|
16
|
+
<div className='flex gap-4'>
|
|
17
17
|
<IconButton {...props} />
|
|
18
18
|
<IconButton iconOnly {...props} />
|
|
19
19
|
<Button>{props.label}</Button>
|
|
@@ -129,18 +129,18 @@ export const Description: Story = {
|
|
|
129
129
|
export const Mock = () => (
|
|
130
130
|
<div className='grid grid-cols-[2rem_1fr_2rem] w-full dx-card-min-width dx-card-max-width border border-separator rounded-xs'>
|
|
131
131
|
<div className='grid grid-cols-subgrid col-span-full'>
|
|
132
|
-
<div
|
|
132
|
+
<div className='grid h-[var(--dx-rail-item)] w-[var(--dx-rail-item)] place-items-center'>
|
|
133
133
|
<Icon icon='ph--dots-six-vertical--regular' />
|
|
134
134
|
</div>
|
|
135
135
|
<div className='p-1 whitespace-normal break-words text-description items-center'>
|
|
136
136
|
This line is very very long and it should wrap.
|
|
137
137
|
</div>
|
|
138
|
-
<div
|
|
138
|
+
<div className='grid h-[var(--dx-rail-item)] w-[var(--dx-rail-item)] place-items-center'>
|
|
139
139
|
<Icon icon='ph--x--regular' />
|
|
140
140
|
</div>
|
|
141
141
|
</div>
|
|
142
142
|
<div className='grid grid-cols-subgrid col-span-3'>
|
|
143
|
-
<div
|
|
143
|
+
<div className='grid h-[var(--dx-rail-item)] w-[var(--dx-rail-item)] place-items-center'>
|
|
144
144
|
<Icon icon='ph--dots-six-vertical--regular' />
|
|
145
145
|
</div>
|
|
146
146
|
<div className='p-1 text-description items-center col-span-2'>
|
|
@@ -52,7 +52,7 @@ const CardContext = createContext<CardContextValue>({});
|
|
|
52
52
|
|
|
53
53
|
const CARD_ROOT_NAME = 'Card.Root';
|
|
54
54
|
|
|
55
|
-
type
|
|
55
|
+
type CardRootProps = {
|
|
56
56
|
id?: string;
|
|
57
57
|
border?: boolean;
|
|
58
58
|
fullWidth?: boolean;
|
|
@@ -64,26 +64,33 @@ type CardRootOwnProps = {
|
|
|
64
64
|
'data-testid'?: string;
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
/**
|
|
68
|
+
* `Card.Root` does not support `asChild`. The Column grid is the root element
|
|
69
|
+
* (one `<div>` carrying both the `dx-card` and `dx-column-root` classes
|
|
70
|
+
* instead of the previous outer-card + inner-column pair), so caller-provided
|
|
71
|
+
* HTML attributes — `onClick`, `tabIndex`, `style`, `data-*`, `grid-template-rows`
|
|
72
|
+
* overrides via `classNames` — land directly on the grid container.
|
|
73
|
+
* Slot-parents (`Focus.Item asChild`, `Mosaic.Tile asChild`, etc.) continue to
|
|
74
|
+
* work because `composable()` preserves the COMPOSABLE marker that slottable parents
|
|
75
|
+
* check before warning, and Radix `Slot` merges the parent's props onto the inner
|
|
76
|
+
* `<div>` exactly the way `slottable`'s `Slot`/`Primitive.div` branch did.
|
|
77
|
+
*/
|
|
78
|
+
const CardRoot = composable<HTMLDivElement, CardRootProps>(
|
|
79
|
+
({ children, id, role, border = true, fullWidth, density, ...props }, forwardedRef) => {
|
|
71
80
|
const { className, ...rest } = composableProps(props);
|
|
72
|
-
const Comp = asChild ? Slot : Primitive.div;
|
|
73
81
|
const { tx } = useThemeContext();
|
|
74
82
|
|
|
75
83
|
return (
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
{
|
|
84
|
+
<Column.Root
|
|
85
|
+
asChild
|
|
86
|
+
gutter={density === 'coarse' ? 'lg' : 'md'}
|
|
87
|
+
classNames={tx('card.root', { border, fullWidth }, className)}
|
|
79
88
|
role={role ?? 'group'}
|
|
80
|
-
className={tx('card.root', { border, fullWidth }, className)}
|
|
81
|
-
ref={forwardedRef}
|
|
82
89
|
>
|
|
83
|
-
<
|
|
90
|
+
<div {...rest} {...(id && { 'data-object-id': id })} ref={forwardedRef}>
|
|
84
91
|
{children}
|
|
85
|
-
</
|
|
86
|
-
</
|
|
92
|
+
</div>
|
|
93
|
+
</Column.Root>
|
|
87
94
|
);
|
|
88
95
|
},
|
|
89
96
|
);
|
|
@@ -196,7 +203,7 @@ const CardIconBlock = forwardRef<HTMLDivElement, ThemedClassName<PropsWithChildr
|
|
|
196
203
|
const { tx } = useThemeContext();
|
|
197
204
|
|
|
198
205
|
return (
|
|
199
|
-
<div {...props}
|
|
206
|
+
<div {...props} className={tx('card.icon-block', { padding }, classNames)} ref={forwardedRef}>
|
|
200
207
|
{children}
|
|
201
208
|
</div>
|
|
202
209
|
);
|
|
@@ -253,6 +260,7 @@ const CARD_ROW_NAME = 'Card.Row';
|
|
|
253
260
|
|
|
254
261
|
type CardRowProps = { icon?: string; fullWidth?: boolean };
|
|
255
262
|
|
|
263
|
+
// TODO(burdon): fullWidth should mean no columns.
|
|
256
264
|
const CardRow = slottable<HTMLDivElement, CardRowProps>(({ children, asChild, icon, ...props }, forwardedRef) => {
|
|
257
265
|
const { className, ...rest } = composableProps(props);
|
|
258
266
|
const Comp = asChild ? Slot : Primitive.div;
|
|
@@ -365,7 +373,6 @@ const CardHtml = ({ html, variant = 'default', ...props }: CardHtmlProps & Theme
|
|
|
365
373
|
return (
|
|
366
374
|
<div
|
|
367
375
|
{...props}
|
|
368
|
-
role='none'
|
|
369
376
|
className={tx('card.text', { variant })}
|
|
370
377
|
// eslint-disable-next-line react/no-danger
|
|
371
378
|
dangerouslySetInnerHTML={{ __html: sanitized }}
|
|
@@ -401,7 +408,7 @@ const CardPoster = (props: CardPosterProps) => {
|
|
|
401
408
|
|
|
402
409
|
if (props.image) {
|
|
403
410
|
return (
|
|
404
|
-
<div
|
|
411
|
+
<div className='col-span-full'>
|
|
405
412
|
<Image
|
|
406
413
|
classNames={[tx('card.poster', {}), aspect, props.classNames]}
|
|
407
414
|
src={props.image}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useArrowNavigationGroup } from '@fluentui/react-tabster';
|
|
6
|
+
import React, {
|
|
7
|
+
createContext,
|
|
8
|
+
type KeyboardEvent,
|
|
9
|
+
type PropsWithChildren,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
useCallback,
|
|
12
|
+
useContext,
|
|
13
|
+
useEffect,
|
|
14
|
+
useMemo,
|
|
15
|
+
useState,
|
|
16
|
+
} from 'react';
|
|
17
|
+
|
|
18
|
+
import { mx } from '@dxos/ui-theme';
|
|
19
|
+
|
|
20
|
+
import { translationKey } from '../../translations';
|
|
21
|
+
import { type ThemedClassName } from '../../util';
|
|
22
|
+
import { IconButton } from '../Button';
|
|
23
|
+
import { MediaPlayer, type MediaKind } from '../MediaPlayer';
|
|
24
|
+
import { useTranslation } from '../ThemeProvider';
|
|
25
|
+
|
|
26
|
+
// TODO(burdon): Move per-element class strings to `@dxos/ui-theme` (theme tokens)
|
|
27
|
+
// so callers can re-theme via the same mechanism the rest of `react-ui` uses.
|
|
28
|
+
|
|
29
|
+
//
|
|
30
|
+
// Context
|
|
31
|
+
//
|
|
32
|
+
|
|
33
|
+
type CarouselContextValue = {
|
|
34
|
+
index: number;
|
|
35
|
+
count: number;
|
|
36
|
+
setIndex: (index: number) => void;
|
|
37
|
+
next: () => void;
|
|
38
|
+
prev: () => void;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const CarouselContext = createContext<CarouselContextValue | null>(null);
|
|
42
|
+
|
|
43
|
+
/** Returns the current carousel state. Must be used within {@link Carousel.Root}. */
|
|
44
|
+
export const useCarousel = (): CarouselContextValue => {
|
|
45
|
+
const context = useContext(CarouselContext);
|
|
46
|
+
if (!context) {
|
|
47
|
+
throw new Error('useCarousel must be used within Carousel.Root');
|
|
48
|
+
}
|
|
49
|
+
return context;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
//
|
|
53
|
+
// Root
|
|
54
|
+
//
|
|
55
|
+
|
|
56
|
+
export type CarouselRootProps = ThemedClassName<
|
|
57
|
+
PropsWithChildren<{
|
|
58
|
+
/** Total number of slides; drives auto-advance and indicator counts. */
|
|
59
|
+
count: number;
|
|
60
|
+
/** Whether to auto-advance slides on mount. Defaults to `false`. */
|
|
61
|
+
autorun?: boolean;
|
|
62
|
+
/** Auto-advance interval in milliseconds. Set 0 to disable. */
|
|
63
|
+
intervalMs?: number;
|
|
64
|
+
defaultIndex?: number;
|
|
65
|
+
}>
|
|
66
|
+
>;
|
|
67
|
+
|
|
68
|
+
const CarouselRoot = ({
|
|
69
|
+
classNames,
|
|
70
|
+
children,
|
|
71
|
+
count,
|
|
72
|
+
autorun = false,
|
|
73
|
+
intervalMs = 5_000,
|
|
74
|
+
defaultIndex = 0,
|
|
75
|
+
}: CarouselRootProps) => {
|
|
76
|
+
const [index, setIndexState] = useState(defaultIndex);
|
|
77
|
+
const [autoAdvance, setAutoAdvance] = useState(autorun);
|
|
78
|
+
|
|
79
|
+
// Reset to first slide if the slide count shrinks below the current index.
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (index >= count) {
|
|
82
|
+
setIndexState(0);
|
|
83
|
+
}
|
|
84
|
+
}, [count, index]);
|
|
85
|
+
|
|
86
|
+
// Auto-advance — stops permanently once the user interacts with any control.
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!autoAdvance || count <= 1 || intervalMs <= 0) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const handle = setInterval(() => setIndexState((i) => (i + 1) % count), intervalMs);
|
|
92
|
+
return () => clearInterval(handle);
|
|
93
|
+
}, [autoAdvance, count, intervalMs]);
|
|
94
|
+
|
|
95
|
+
const setIndex = useCallback((next: number) => {
|
|
96
|
+
setAutoAdvance(false);
|
|
97
|
+
setIndexState(next);
|
|
98
|
+
}, []);
|
|
99
|
+
const next = useCallback(() => {
|
|
100
|
+
setAutoAdvance(false);
|
|
101
|
+
setIndexState((i) => (i + 1) % count);
|
|
102
|
+
}, [count]);
|
|
103
|
+
const prev = useCallback(() => {
|
|
104
|
+
setAutoAdvance(false);
|
|
105
|
+
setIndexState((i) => (i - 1 + count) % count);
|
|
106
|
+
}, [count]);
|
|
107
|
+
|
|
108
|
+
const value = useMemo(() => ({ index, count, setIndex, next, prev }), [index, count, setIndex, next, prev]);
|
|
109
|
+
|
|
110
|
+
if (count === 0) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<CarouselContext.Provider value={value}>
|
|
116
|
+
{/*
|
|
117
|
+
* Rows are `[1fr, auto]`: row 1 (Previous|Viewport|Next) stretches when the parent
|
|
118
|
+
* gives the carousel a definite height, and row 2 (Indicators / Caption) sticks to
|
|
119
|
+
* its content height. With no parent height constraint, the `1fr` row simply tracks
|
|
120
|
+
* row-1 content — preserving the existing aspect-video behaviour for unbounded use.
|
|
121
|
+
*/}
|
|
122
|
+
{/* TODO(burdon): Move to ui-theme. */}
|
|
123
|
+
<div
|
|
124
|
+
className={mx(
|
|
125
|
+
'w-full grid grid-cols-[min-content_1fr_min-content] grid-rows-[minmax(0,1fr)_auto] gap-4 items-center',
|
|
126
|
+
classNames,
|
|
127
|
+
)}
|
|
128
|
+
>
|
|
129
|
+
{children}
|
|
130
|
+
</div>
|
|
131
|
+
</CarouselContext.Provider>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
CarouselRoot.displayName = 'Carousel.Root';
|
|
136
|
+
|
|
137
|
+
//
|
|
138
|
+
// Viewport
|
|
139
|
+
//
|
|
140
|
+
|
|
141
|
+
export type CarouselViewportProps = ThemedClassName<PropsWithChildren<{}>>;
|
|
142
|
+
|
|
143
|
+
const CarouselViewport = ({ children, classNames }: CarouselViewportProps) => {
|
|
144
|
+
const { t } = useTranslation(translationKey);
|
|
145
|
+
const { count, next, prev } = useCarousel();
|
|
146
|
+
const handleKeyDown = useCallback(
|
|
147
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
148
|
+
if (count <= 1) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (event.key === 'ArrowLeft') {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
prev();
|
|
154
|
+
} else if (event.key === 'ArrowRight') {
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
next();
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
[count, next, prev],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div
|
|
164
|
+
// TODO(burdon): Move to ui-theme.
|
|
165
|
+
className={mx(
|
|
166
|
+
'relative w-full aspect-video overflow-hidden',
|
|
167
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
168
|
+
classNames,
|
|
169
|
+
)}
|
|
170
|
+
tabIndex={0}
|
|
171
|
+
role='region'
|
|
172
|
+
aria-roledescription='carousel'
|
|
173
|
+
aria-label={t('carousel-viewport.label')}
|
|
174
|
+
onKeyDown={handleKeyDown}
|
|
175
|
+
>
|
|
176
|
+
{children}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
CarouselViewport.displayName = 'Carousel.Viewport';
|
|
182
|
+
|
|
183
|
+
//
|
|
184
|
+
// Slide
|
|
185
|
+
//
|
|
186
|
+
|
|
187
|
+
export type CarouselSlideProps = ThemedClassName<{
|
|
188
|
+
index: number;
|
|
189
|
+
/** Media source URL — rendered via the embedded {@link MediaPlayer}. */
|
|
190
|
+
src: string;
|
|
191
|
+
/** Override media auto-detection (`'video' | 'audio'`). */
|
|
192
|
+
kind?: MediaKind;
|
|
193
|
+
/** Accessible label / `<img alt>` fallback. */
|
|
194
|
+
alt?: string;
|
|
195
|
+
/** Class names forwarded to the inner `<img>` when MediaPlayer resolves to an image. */
|
|
196
|
+
imgClassNames?: string;
|
|
197
|
+
/** Class names forwarded to the inner `<video>` / `<audio>` / `<iframe>`. */
|
|
198
|
+
mediaClassNames?: string;
|
|
199
|
+
controls?: boolean;
|
|
200
|
+
autoPlay?: boolean;
|
|
201
|
+
loop?: boolean;
|
|
202
|
+
muted?: boolean;
|
|
203
|
+
crossOrigin?: 'anonymous' | 'use-credentials' | '';
|
|
204
|
+
}>;
|
|
205
|
+
|
|
206
|
+
const CarouselSlide = ({
|
|
207
|
+
index,
|
|
208
|
+
classNames,
|
|
209
|
+
src,
|
|
210
|
+
kind,
|
|
211
|
+
alt,
|
|
212
|
+
imgClassNames,
|
|
213
|
+
mediaClassNames,
|
|
214
|
+
controls,
|
|
215
|
+
autoPlay,
|
|
216
|
+
loop,
|
|
217
|
+
muted,
|
|
218
|
+
crossOrigin,
|
|
219
|
+
}: CarouselSlideProps) => {
|
|
220
|
+
const { index: active } = useCarousel();
|
|
221
|
+
if (active !== index) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div className={mx('absolute inset-0 w-full h-full bg-baseSurface', classNames)}>
|
|
227
|
+
<MediaPlayer
|
|
228
|
+
src={src}
|
|
229
|
+
kind={kind}
|
|
230
|
+
alt={alt}
|
|
231
|
+
classNames='w-full h-full'
|
|
232
|
+
imgClassNames={mx('object-cover', imgClassNames)}
|
|
233
|
+
mediaClassNames={mediaClassNames}
|
|
234
|
+
controls={controls}
|
|
235
|
+
autoPlay={autoPlay}
|
|
236
|
+
loop={loop}
|
|
237
|
+
muted={muted}
|
|
238
|
+
crossOrigin={crossOrigin}
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
CarouselSlide.displayName = 'Carousel.Slide';
|
|
245
|
+
|
|
246
|
+
//
|
|
247
|
+
// Previous / Next
|
|
248
|
+
//
|
|
249
|
+
|
|
250
|
+
export type CarouselButtonProps = ThemedClassName<{}>;
|
|
251
|
+
|
|
252
|
+
const CarouselPrevious = ({ classNames }: CarouselButtonProps) => {
|
|
253
|
+
const { t } = useTranslation(translationKey);
|
|
254
|
+
const { count, prev } = useCarousel();
|
|
255
|
+
if (count <= 1) {
|
|
256
|
+
return <div />;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<IconButton
|
|
261
|
+
classNames={mx('self-center', classNames)}
|
|
262
|
+
square
|
|
263
|
+
variant='ghost'
|
|
264
|
+
icon='ph--caret-left--regular'
|
|
265
|
+
iconOnly
|
|
266
|
+
label={t('carousel-prev.label')}
|
|
267
|
+
onClick={prev}
|
|
268
|
+
/>
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
CarouselPrevious.displayName = 'Carousel.Previous';
|
|
273
|
+
|
|
274
|
+
const CarouselNext = ({ classNames }: CarouselButtonProps) => {
|
|
275
|
+
const { t } = useTranslation(translationKey);
|
|
276
|
+
const { count, next } = useCarousel();
|
|
277
|
+
if (count <= 1) {
|
|
278
|
+
return <div />;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<IconButton
|
|
283
|
+
classNames={mx('self-center', classNames)}
|
|
284
|
+
square
|
|
285
|
+
variant='ghost'
|
|
286
|
+
icon='ph--caret-right--regular'
|
|
287
|
+
iconOnly
|
|
288
|
+
label={t('carousel-next.label')}
|
|
289
|
+
onClick={next}
|
|
290
|
+
/>
|
|
291
|
+
);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
CarouselNext.displayName = 'Carousel.Next';
|
|
295
|
+
|
|
296
|
+
//
|
|
297
|
+
// Indicators
|
|
298
|
+
//
|
|
299
|
+
|
|
300
|
+
export type CarouselIndicatorsProps = ThemedClassName<{}>;
|
|
301
|
+
|
|
302
|
+
/** Tab-strip of slide indicators. Sits in the centre column so it matches the viewport's width. */
|
|
303
|
+
const CarouselIndicators = ({ classNames }: CarouselIndicatorsProps) => {
|
|
304
|
+
const { t } = useTranslation(translationKey);
|
|
305
|
+
const { count, index, setIndex } = useCarousel();
|
|
306
|
+
const arrowNavigationAttrs = useArrowNavigationGroup({ axis: 'horizontal', memorizeCurrent: true });
|
|
307
|
+
if (count <= 1) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<div className='col-start-2 overflow-hidden'>
|
|
313
|
+
<div
|
|
314
|
+
{...arrowNavigationAttrs}
|
|
315
|
+
className={mx('flex items-center justify-center', classNames)}
|
|
316
|
+
role='tablist'
|
|
317
|
+
aria-label={t('carousel-indicators.label')}
|
|
318
|
+
>
|
|
319
|
+
{Array.from({ length: count }).map((_, i) => (
|
|
320
|
+
<IconButton
|
|
321
|
+
key={i}
|
|
322
|
+
role='tab'
|
|
323
|
+
aria-selected={i === index}
|
|
324
|
+
classNames={i === index ? 'text-primary-500' : 'text-description'}
|
|
325
|
+
icon={i === index ? 'ph--circle--fill' : 'ph--circle--regular'}
|
|
326
|
+
iconOnly
|
|
327
|
+
label={t('carousel-go-to.label', { index: i + 1 })}
|
|
328
|
+
onClick={() => setIndex(i)}
|
|
329
|
+
onFocus={() => setIndex(i)}
|
|
330
|
+
size={3}
|
|
331
|
+
variant='ghost'
|
|
332
|
+
/>
|
|
333
|
+
))}
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
CarouselIndicators.displayName = 'Carousel.Indicators';
|
|
340
|
+
|
|
341
|
+
//
|
|
342
|
+
// Caption
|
|
343
|
+
//
|
|
344
|
+
|
|
345
|
+
export type CarouselCaptionProps = ThemedClassName<{
|
|
346
|
+
/** Render prop receiving the active slide index. */
|
|
347
|
+
children: (index: number) => ReactNode;
|
|
348
|
+
}>;
|
|
349
|
+
|
|
350
|
+
/** Caption sized to the viewport's column. */
|
|
351
|
+
const CarouselCaption = ({ children, classNames }: CarouselCaptionProps) => {
|
|
352
|
+
const { index } = useCarousel();
|
|
353
|
+
const content = children(index);
|
|
354
|
+
if (content == null || content === false || content === '') {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
return (
|
|
358
|
+
// TODO(burdon): Move to ui-theme.
|
|
359
|
+
<div className='col-start-2'>
|
|
360
|
+
<p className={mx('text-center text-description', classNames)}>{content}</p>
|
|
361
|
+
</div>
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
CarouselCaption.displayName = 'Carousel.Caption';
|
|
366
|
+
|
|
367
|
+
//
|
|
368
|
+
// Carousel
|
|
369
|
+
//
|
|
370
|
+
|
|
371
|
+
export const Carousel = {
|
|
372
|
+
Root: CarouselRoot,
|
|
373
|
+
Viewport: CarouselViewport,
|
|
374
|
+
Slide: CarouselSlide,
|
|
375
|
+
Previous: CarouselPrevious,
|
|
376
|
+
Next: CarouselNext,
|
|
377
|
+
Indicators: CarouselIndicators,
|
|
378
|
+
Caption: CarouselCaption,
|
|
379
|
+
};
|
|
@@ -30,11 +30,11 @@ export const CopyButton = ({ classNames, value, size = 5, ...props }: CopyButton
|
|
|
30
30
|
onClick={() => setTextValue(value)}
|
|
31
31
|
data-testid='copy-invitation'
|
|
32
32
|
>
|
|
33
|
-
<div
|
|
33
|
+
<div className={mx('flex gap-1 items-center', isCopied && inactiveLabelStyles)}>
|
|
34
34
|
<span className='px-1'>{t('copy.label')}</span>
|
|
35
35
|
<Icon icon='ph--copy--regular' size={size} />
|
|
36
36
|
</div>
|
|
37
|
-
<div
|
|
37
|
+
<div className={mx('flex gap-1 items-center', !isCopied && inactiveLabelStyles)}>
|
|
38
38
|
<span className='px-1'>{t('copy-success.label')}</span>
|
|
39
39
|
<Icon icon='ph--check--regular' size={size} />
|
|
40
40
|
</div>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Primitive } from '@radix-ui/react-primitive';
|
|
6
|
-
import React, { type ComponentPropsWithRef, forwardRef, memo } from 'react';
|
|
6
|
+
import React, { type ComponentPropsWithRef, forwardRef, memo, useMemo } from 'react';
|
|
7
7
|
|
|
8
8
|
import { type Size } from '@dxos/ui-types';
|
|
9
9
|
|
|
@@ -13,18 +13,25 @@ import { type ThemedClassName } from '../../util';
|
|
|
13
13
|
export type IconProps = ThemedClassName<ComponentPropsWithRef<typeof Primitive.svg>> & {
|
|
14
14
|
icon: string;
|
|
15
15
|
size?: Size;
|
|
16
|
+
synchronized?: boolean;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* The Icon's size can be set directly or inherited from the `--dx-icon-size` CSS variable.
|
|
20
21
|
*/
|
|
21
22
|
export const Icon = memo(
|
|
22
|
-
forwardRef<SVGSVGElement, IconProps>(({
|
|
23
|
+
forwardRef<SVGSVGElement, IconProps>(({ classNames, icon, size, synchronized, style, ...props }, forwardedRef) => {
|
|
23
24
|
const { tx } = useThemeContext();
|
|
25
|
+
const spinDelay = useMemo(() => (synchronized ? `${-(Date.now() % 1_000)}ms` : undefined), [synchronized]);
|
|
24
26
|
const href = useIconHref(icon);
|
|
25
27
|
|
|
26
28
|
return (
|
|
27
|
-
<svg
|
|
29
|
+
<svg
|
|
30
|
+
{...props}
|
|
31
|
+
style={{ ...style, animationDelay: spinDelay }}
|
|
32
|
+
className={tx('icon.root', { size }, classNames)}
|
|
33
|
+
ref={forwardedRef}
|
|
34
|
+
>
|
|
28
35
|
<use href={href} />
|
|
29
36
|
</svg>
|
|
30
37
|
);
|
|
@@ -16,9 +16,17 @@ export type LinkProps = ThemedClassName<ComponentPropsWithRef<typeof Primitive.a
|
|
|
16
16
|
}>;
|
|
17
17
|
|
|
18
18
|
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
|
|
19
|
-
({ asChild, variant,
|
|
19
|
+
({ classNames, asChild, variant, target = '_blank', rel = 'noreferrer', ...props }, forwardedRef) => {
|
|
20
20
|
const { tx } = useThemeContext();
|
|
21
21
|
const Comp = asChild ? Slot : Primitive.a;
|
|
22
|
-
return
|
|
22
|
+
return (
|
|
23
|
+
<Comp
|
|
24
|
+
{...props}
|
|
25
|
+
target={target}
|
|
26
|
+
rel={rel}
|
|
27
|
+
className={tx('link.root', { variant }, classNames)}
|
|
28
|
+
ref={forwardedRef}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
23
31
|
},
|
|
24
32
|
);
|
|
@@ -176,7 +176,7 @@ export const Collapsible: Story = {
|
|
|
176
176
|
<List {...args}>
|
|
177
177
|
{items.map(({ id, text, body }, index) => (
|
|
178
178
|
<ListItem.Root key={id} id={id} collapsible={index !== 2} defaultOpen={index % 2 === 0}>
|
|
179
|
-
<div
|
|
179
|
+
<div className='grow flex'>
|
|
180
180
|
{index !== 2 ? <ListItem.OpenTrigger /> : <ListItem.MockOpenTrigger />}
|
|
181
181
|
<ListItem.Heading classNames='grow pt-2'>{text}</ListItem.Heading>
|
|
182
182
|
<ListItem.Endcap>
|
|
@@ -75,7 +75,7 @@ const MockListItemOpenTrigger = ({
|
|
|
75
75
|
}: ThemedClassName<Omit<ComponentPropsWithoutRef<'div'>, 'children'>>) => {
|
|
76
76
|
const density = useDensityContext();
|
|
77
77
|
const { tx } = useThemeContext();
|
|
78
|
-
return <div
|
|
78
|
+
return <div {...props} className={tx('list.item.openTrigger', { density }, classNames)} />;
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
type ListItemHeadingProps = ThemedClassName<ListPrimitiveItemHeadingProps>;
|