@dxos/react-ui-stack 0.8.4-main.5ad4a44 → 0.8.4-main.66e292d
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-SM27YTH3.mjs → chunk-3F2KBXLP.mjs} +130 -66
- package/dist/lib/browser/chunk-3F2KBXLP.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/playwright/index.mjs +10 -23
- package/dist/lib/browser/playwright/index.mjs.map +2 -2
- package/dist/lib/browser/testing/index.mjs +1 -1
- package/dist/lib/node-esm/{chunk-MMAOXKOM.mjs → chunk-SYKFLQGK.mjs} +130 -66
- package/dist/lib/node-esm/chunk-SYKFLQGK.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1 -1
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/playwright/index.mjs +10 -23
- package/dist/lib/node-esm/playwright/index.mjs.map +2 -2
- package/dist/lib/node-esm/testing/index.mjs +1 -1
- package/dist/types/src/components/Image/Image.d.ts +5 -2
- package/dist/types/src/components/Image/Image.d.ts.map +1 -1
- package/dist/types/src/components/Image/Image.stories.d.ts +3 -0
- package/dist/types/src/components/Image/Image.stories.d.ts.map +1 -1
- package/dist/types/src/components/Stack/Stack.d.ts +6 -6
- package/dist/types/src/components/Stack/Stack.d.ts.map +1 -1
- package/dist/types/src/components/StackItem/StackItem.d.ts +1 -1
- package/dist/types/src/components/StackItem/StackItemContent.d.ts +10 -8
- package/dist/types/src/components/StackItem/StackItemContent.d.ts.map +1 -1
- package/dist/types/src/exemplars/Card/Card.d.ts +12 -8
- package/dist/types/src/exemplars/Card/Card.d.ts.map +1 -1
- package/dist/types/src/hooks/useStackDropForElements.d.ts +2 -2
- package/dist/types/src/hooks/useStackDropForElements.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +21 -21
- package/src/components/Image/Image.stories.tsx +33 -5
- package/src/components/Image/Image.tsx +158 -73
- package/src/components/Stack/Stack.tsx +14 -14
- package/src/components/StackItem/StackItem.stories.tsx +1 -1
- package/src/components/StackItem/StackItemContent.tsx +8 -8
- package/src/exemplars/Card/Card.tsx +25 -19
- package/src/hooks/useStackDropForElements.ts +42 -35
- package/dist/lib/browser/chunk-SM27YTH3.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-MMAOXKOM.mjs.map +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-stack",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.66e292d",
|
|
4
4
|
"description": "A stack component.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -54,36 +54,36 @@
|
|
|
54
54
|
"@radix-ui/react-slot": "1.1.2",
|
|
55
55
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
56
56
|
"react-resize-detector": "^11.0.1",
|
|
57
|
-
"@dxos/echo": "0.8.4-main.
|
|
58
|
-
"@dxos/
|
|
59
|
-
"@dxos/
|
|
60
|
-
"@dxos/
|
|
61
|
-
"@dxos/
|
|
62
|
-
"@dxos/
|
|
63
|
-
"@dxos/
|
|
57
|
+
"@dxos/echo": "0.8.4-main.66e292d",
|
|
58
|
+
"@dxos/live-object": "0.8.4-main.66e292d",
|
|
59
|
+
"@dxos/keyboard": "0.8.4-main.66e292d",
|
|
60
|
+
"@dxos/react-ui-attention": "0.8.4-main.66e292d",
|
|
61
|
+
"@dxos/react-ui-dnd": "0.8.4-main.66e292d",
|
|
62
|
+
"@dxos/storybook-utils": "0.8.4-main.66e292d",
|
|
63
|
+
"@dxos/util": "0.8.4-main.66e292d"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/react": "~19.2.2",
|
|
67
|
-
"@types/react-dom": "~19.2.
|
|
67
|
+
"@types/react-dom": "~19.2.2",
|
|
68
68
|
"react": "~19.2.0",
|
|
69
69
|
"react-dom": "~19.2.0",
|
|
70
70
|
"vite": "7.1.9",
|
|
71
|
-
"@dxos/app-graph": "0.8.4-main.
|
|
72
|
-
"@dxos/
|
|
73
|
-
"@dxos/
|
|
74
|
-
"@dxos/
|
|
75
|
-
"@dxos/
|
|
76
|
-
"@dxos/
|
|
77
|
-
"@dxos/
|
|
78
|
-
"@dxos/
|
|
71
|
+
"@dxos/app-graph": "0.8.4-main.66e292d",
|
|
72
|
+
"@dxos/random": "0.8.4-main.66e292d",
|
|
73
|
+
"@dxos/react-ui": "0.8.4-main.66e292d",
|
|
74
|
+
"@dxos/client": "0.8.4-main.66e292d",
|
|
75
|
+
"@dxos/react-ui-theme": "0.8.4-main.66e292d",
|
|
76
|
+
"@dxos/storybook-utils": "0.8.4-main.66e292d",
|
|
77
|
+
"@dxos/test-utils": "0.8.4-main.66e292d",
|
|
78
|
+
"@dxos/echo": "0.8.4-main.66e292d"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
81
|
"react": "^19.0.0",
|
|
82
82
|
"react-dom": "^19.0.0",
|
|
83
|
-
"@dxos/client": "0.8.4-main.
|
|
84
|
-
"@dxos/
|
|
85
|
-
"@dxos/react-ui-theme": "0.8.4-main.
|
|
86
|
-
"@dxos/
|
|
83
|
+
"@dxos/client": "0.8.4-main.66e292d",
|
|
84
|
+
"@dxos/random": "0.8.4-main.66e292d",
|
|
85
|
+
"@dxos/react-ui-theme": "0.8.4-main.66e292d",
|
|
86
|
+
"@dxos/react-ui": "0.8.4-main.66e292d"
|
|
87
87
|
},
|
|
88
88
|
"publishConfig": {
|
|
89
89
|
"access": "public"
|
|
@@ -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, { useMemo } from 'react';
|
|
7
7
|
|
|
8
8
|
import { faker } from '@dxos/random';
|
|
9
9
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
10
10
|
|
|
11
11
|
import { Image } from './Image';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
const seed = Math.random();
|
|
14
|
+
|
|
15
|
+
faker.seed(seed);
|
|
14
16
|
|
|
15
17
|
const meta = {
|
|
16
18
|
title: 'ui/react-ui-stack/Image',
|
|
@@ -22,7 +24,7 @@ const meta = {
|
|
|
22
24
|
),
|
|
23
25
|
decorators: [withTheme],
|
|
24
26
|
parameters: {
|
|
25
|
-
layout: '
|
|
27
|
+
layout: 'centered',
|
|
26
28
|
},
|
|
27
29
|
} satisfies Meta<typeof Image>;
|
|
28
30
|
|
|
@@ -44,13 +46,39 @@ export const Default: Story = {
|
|
|
44
46
|
export const Cors: Story = {
|
|
45
47
|
args: {
|
|
46
48
|
src: 'https://dxos.network/dxos-logotype-blue.png',
|
|
47
|
-
classNames: '
|
|
49
|
+
classNames: 'is-[20rem]',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Corners: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
src: 'https://media.licdn.com/dms/image/v2/D4D0BAQEY4OiENeMR4A/company-logo_200_200/company-logo_200_200/0/1728648673877/moonfire_logo?e=1763596800&v=beta&t=_Jmhg-vu5uqUR88YiTbDFOC4ShlUbjk63_7-JQpgK9A',
|
|
56
|
+
classNames: 'is-[20rem]',
|
|
48
57
|
},
|
|
49
58
|
};
|
|
50
59
|
|
|
51
60
|
export const SVG: Story = {
|
|
52
61
|
args: {
|
|
53
62
|
src: 'https://dxos.network/bg-kube.svg',
|
|
54
|
-
classNames: '
|
|
63
|
+
classNames: 'is-[20rem]',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const Many: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
src: 'https://dxos.network/bg-kube.svg',
|
|
70
|
+
},
|
|
71
|
+
render: () => {
|
|
72
|
+
const images = useMemo(
|
|
73
|
+
() => Array.from({ length: 9 }, (_, i) => `https://picsum.photos/seed/${seed + i}/500/500`),
|
|
74
|
+
[],
|
|
75
|
+
);
|
|
76
|
+
return (
|
|
77
|
+
<div className='is-[60rem] grid grid-cols-3 grid-rows-3 gap-8'>
|
|
78
|
+
{images.map((src, i) => (
|
|
79
|
+
<Image key={i} src={src} classNames='is-[18rem] bs-[12rem]' />
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
55
83
|
},
|
|
56
84
|
};
|
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, { type SyntheticEvent, useRef, useState } from 'react';
|
|
5
|
+
import React, { type SyntheticEvent, useCallback, useRef, useState } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
8
8
|
import { mx } from '@dxos/react-ui-theme';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
const cache = new Map<string, string>();
|
|
11
|
+
|
|
12
|
+
export type ImageProps = ThemedClassName<
|
|
13
|
+
{
|
|
14
|
+
src: string;
|
|
15
|
+
alt?: string;
|
|
16
|
+
crossOrigin?: 'anonymous' | 'use-credentials' | '';
|
|
17
|
+
} & ColorOptions
|
|
18
|
+
>;
|
|
17
19
|
|
|
18
20
|
export const Image = ({
|
|
19
21
|
classNames,
|
|
@@ -21,83 +23,47 @@ export const Image = ({
|
|
|
21
23
|
alt = '',
|
|
22
24
|
crossOrigin = 'anonymous',
|
|
23
25
|
sampleSize = 64,
|
|
24
|
-
contrast = 0.
|
|
26
|
+
contrast = 0.9,
|
|
25
27
|
}: ImageProps) => {
|
|
26
28
|
const [crossOriginState, setCrossOriginState] = useState<ImageProps['crossOrigin']>(crossOrigin);
|
|
27
29
|
const [dominantColor, setDominantColor] = useState<string | undefined>(undefined);
|
|
28
30
|
const [imageLoaded, setImageLoaded] = useState<boolean>(false);
|
|
29
31
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
30
32
|
|
|
31
|
-
//
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (!canvas || !ctx) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
33
|
+
// CORS not supported by server.
|
|
34
|
+
const handleImageError = (): void => {
|
|
35
|
+
setCrossOriginState(undefined);
|
|
36
|
+
};
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const imageData = ctx.getImageData(0, 0, sampleSize, sampleSize);
|
|
47
|
-
const pixels = imageData.data;
|
|
48
|
-
|
|
49
|
-
// Calculate average color with more weight to vibrant colors.
|
|
50
|
-
let r = 0;
|
|
51
|
-
let g = 0;
|
|
52
|
-
let b = 0;
|
|
53
|
-
let totalWeight = 0;
|
|
54
|
-
for (let i = 0; i < pixels.length; i += 4) {
|
|
55
|
-
const red = pixels[i];
|
|
56
|
-
const green = pixels[i + 1];
|
|
57
|
-
const blue = pixels[i + 2];
|
|
58
|
-
const alpha = pixels[i + 3];
|
|
59
|
-
|
|
60
|
-
// Skip transparent pixels.
|
|
61
|
-
if (alpha === 0) continue;
|
|
62
|
-
|
|
63
|
-
// Calculate saturation to weight vibrant colors more.
|
|
64
|
-
const max = Math.max(red, green, blue);
|
|
65
|
-
const min = Math.min(red, green, blue);
|
|
66
|
-
const saturation = max === 0 ? 0 : (max - min) / max;
|
|
67
|
-
const weight = 1 + saturation * 2; // Give more weight to saturated colors.
|
|
68
|
-
|
|
69
|
-
r += red * weight;
|
|
70
|
-
g += green * weight;
|
|
71
|
-
b += blue * weight;
|
|
72
|
-
totalWeight += weight;
|
|
38
|
+
const handleImageLoad = useCallback(
|
|
39
|
+
({ target }: SyntheticEvent<HTMLImageElement>): void => {
|
|
40
|
+
const rgb = cache.get(src);
|
|
41
|
+
if (rgb) {
|
|
42
|
+
setDominantColor(rgb);
|
|
43
|
+
setImageLoaded(true);
|
|
44
|
+
return;
|
|
73
45
|
}
|
|
74
46
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
b = Math.round(b / totalWeight);
|
|
79
|
-
|
|
80
|
-
// Slightly darken the color for better contrast.
|
|
81
|
-
r = Math.round(r * contrast);
|
|
82
|
-
g = Math.round(g * contrast);
|
|
83
|
-
b = Math.round(b * contrast);
|
|
84
|
-
setDominantColor(`rgb(${r}, ${g}, ${b})`);
|
|
47
|
+
const img = target as HTMLImageElement;
|
|
48
|
+
if (!canvasRef.current) {
|
|
49
|
+
return;
|
|
85
50
|
}
|
|
86
|
-
} catch {
|
|
87
|
-
setCrossOriginState(undefined);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
51
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
52
|
+
try {
|
|
53
|
+
const color = extractDominantColor(canvasRef.current, img, { sampleSize, contrast });
|
|
54
|
+
if (color) {
|
|
55
|
+
const rgb = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
|
|
56
|
+
cache.set(src, rgb);
|
|
57
|
+
setDominantColor(rgb);
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
setCrossOriginState(undefined);
|
|
61
|
+
}
|
|
95
62
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
};
|
|
63
|
+
setImageLoaded(true);
|
|
64
|
+
},
|
|
65
|
+
[sampleSize, contrast, src],
|
|
66
|
+
);
|
|
101
67
|
|
|
102
68
|
return (
|
|
103
69
|
<div
|
|
@@ -135,3 +101,122 @@ export const Image = ({
|
|
|
135
101
|
</div>
|
|
136
102
|
);
|
|
137
103
|
};
|
|
104
|
+
|
|
105
|
+
type ColorOptions = {
|
|
106
|
+
sampleSize?: number;
|
|
107
|
+
contrast?: number;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get dominant color from image (esp. from corners).
|
|
112
|
+
*/
|
|
113
|
+
const extractDominantColor = (
|
|
114
|
+
canvas: HTMLCanvasElement,
|
|
115
|
+
img: HTMLImageElement,
|
|
116
|
+
{ sampleSize = 64, contrast = 0.95 }: ColorOptions,
|
|
117
|
+
): [number, number, number] | null => {
|
|
118
|
+
const ctx = canvas.getContext('2d');
|
|
119
|
+
if (!ctx) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Draw the image scaled down.
|
|
124
|
+
canvas.width = sampleSize;
|
|
125
|
+
canvas.height = sampleSize;
|
|
126
|
+
ctx.drawImage(img, 0, 0, sampleSize, sampleSize);
|
|
127
|
+
|
|
128
|
+
// Get image data.
|
|
129
|
+
const imageData = ctx.getImageData(0, 0, sampleSize, sampleSize);
|
|
130
|
+
const pixels = imageData.data;
|
|
131
|
+
|
|
132
|
+
// Check for transparent background.
|
|
133
|
+
if (isTransparent(pixels, sampleSize)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let r = 0;
|
|
138
|
+
let g = 0;
|
|
139
|
+
let b = 0;
|
|
140
|
+
let totalWeight = 0;
|
|
141
|
+
|
|
142
|
+
// Define corner sampling areas (e.g., 25% of each dimension from each corner).
|
|
143
|
+
const cornerSize = Math.floor(sampleSize * 0.125);
|
|
144
|
+
|
|
145
|
+
// Sample only pixels in corner areas.
|
|
146
|
+
for (let y = 0; y < sampleSize; y++) {
|
|
147
|
+
for (let x = 0; x < sampleSize; x++) {
|
|
148
|
+
// Check if pixel is in any corner area.
|
|
149
|
+
const isInTopLeft = x < cornerSize && y < cornerSize;
|
|
150
|
+
const isInTopRight = x >= sampleSize - cornerSize && y < cornerSize;
|
|
151
|
+
const isInBottomLeft = x < cornerSize && y >= sampleSize - cornerSize;
|
|
152
|
+
const isInBottomRight = x >= sampleSize - cornerSize && y >= sampleSize - cornerSize;
|
|
153
|
+
if (!isInTopLeft && !isInTopRight && !isInBottomLeft && !isInBottomRight) {
|
|
154
|
+
continue; // Skip pixels not in corner areas.
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const i = (y * sampleSize + x) * 4;
|
|
158
|
+
const red = pixels[i];
|
|
159
|
+
const green = pixels[i + 1];
|
|
160
|
+
const blue = pixels[i + 2];
|
|
161
|
+
const alpha = pixels[i + 3];
|
|
162
|
+
|
|
163
|
+
// Skip transparent pixels.
|
|
164
|
+
if (alpha === 0) continue;
|
|
165
|
+
|
|
166
|
+
// Calculate saturation to weight vibrant colors more.
|
|
167
|
+
const max = Math.max(red, green, blue);
|
|
168
|
+
const min = Math.min(red, green, blue);
|
|
169
|
+
const saturation = max === 0 ? 0 : (max - min) / max;
|
|
170
|
+
const weight = 1 + saturation * 2;
|
|
171
|
+
|
|
172
|
+
r += red * weight;
|
|
173
|
+
g += green * weight;
|
|
174
|
+
b += blue * weight;
|
|
175
|
+
totalWeight += weight;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (totalWeight > 0) {
|
|
180
|
+
// Slightly darken the color for better contrast.
|
|
181
|
+
r = Math.round(Math.round(r / totalWeight) * contrast);
|
|
182
|
+
g = Math.round(Math.round(g / totalWeight) * contrast);
|
|
183
|
+
b = Math.round(Math.round(b / totalWeight) * contrast);
|
|
184
|
+
return [r, g, b];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Detects if an image has a transparent background by examining edge pixels.
|
|
192
|
+
* @param pixels - Image pixel data from canvas
|
|
193
|
+
* @param sampleSize - Size of the sampled image
|
|
194
|
+
* @param threshold - Percentage threshold for considering background transparent (default: 0.5)
|
|
195
|
+
* @returns True if the image has a transparent background
|
|
196
|
+
*/
|
|
197
|
+
const isTransparent = (pixels: Uint8ClampedArray, sampleSize: number, threshold: number = 0.5): boolean => {
|
|
198
|
+
let edgeTransparentPixels = 0;
|
|
199
|
+
const edgePixels = sampleSize * 4 - 4; // Perimeter minus corners counted twice.
|
|
200
|
+
|
|
201
|
+
for (let x = 0; x < sampleSize; x++) {
|
|
202
|
+
// Top edge.
|
|
203
|
+
const topIndex = x * 4;
|
|
204
|
+
if (pixels[topIndex + 3] === 0) edgeTransparentPixels++;
|
|
205
|
+
|
|
206
|
+
// Bottom edge.
|
|
207
|
+
const bottomIndex = ((sampleSize - 1) * sampleSize + x) * 4;
|
|
208
|
+
if (pixels[bottomIndex + 3] === 0) edgeTransparentPixels++;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (let y = 1; y < sampleSize - 1; y++) {
|
|
212
|
+
// Left edge.
|
|
213
|
+
const leftIndex = y * sampleSize * 4;
|
|
214
|
+
if (pixels[leftIndex + 3] === 0) edgeTransparentPixels++;
|
|
215
|
+
|
|
216
|
+
// Right edge.
|
|
217
|
+
const rightIndex = (y * sampleSize + sampleSize - 1) * 4;
|
|
218
|
+
if (pixels[rightIndex + 3] === 0) edgeTransparentPixels++;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return edgeTransparentPixels / edgePixels > threshold;
|
|
222
|
+
};
|
|
@@ -23,6 +23,7 @@ import { type StackContextValue } from '../defs';
|
|
|
23
23
|
import { StackContext } from '../StackContext';
|
|
24
24
|
|
|
25
25
|
export type Orientation = 'horizontal' | 'vertical';
|
|
26
|
+
|
|
26
27
|
/**
|
|
27
28
|
* Size is how Stack and its StackItems coordinate the dimensions of the items with the available space.
|
|
28
29
|
* - `intrinsic` signals to Stack and its StackItems to occupy their intrinsic size
|
|
@@ -32,14 +33,6 @@ export type Orientation = 'horizontal' | 'vertical';
|
|
|
32
33
|
*/
|
|
33
34
|
export type Size = 'intrinsic' | 'contain' | 'split';
|
|
34
35
|
|
|
35
|
-
export type StackProps = Omit<ThemedClassName<ComponentPropsWithRef<'div'>>, 'aria-orientation'> &
|
|
36
|
-
Partial<StackContextValue> & {
|
|
37
|
-
itemsCount?: number;
|
|
38
|
-
getDropElement?: (stackElement: HTMLDivElement) => HTMLDivElement;
|
|
39
|
-
separatorOnScroll?: number;
|
|
40
|
-
circularFocus?: boolean;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
36
|
export const railGridHorizontal = 'grid-rows-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
|
|
44
37
|
export const railGridVertical = 'grid-cols-[[rail-start]_var(--rail-size)_[content-start]_1fr_[content-end]]';
|
|
45
38
|
|
|
@@ -61,6 +54,14 @@ const scrollIntoViewAndFocus = (el: HTMLElement, orientation: StackProps['orient
|
|
|
61
54
|
return el.focus();
|
|
62
55
|
};
|
|
63
56
|
|
|
57
|
+
export type StackProps = Omit<ThemedClassName<ComponentPropsWithRef<'div'>>, 'aria-orientation'> &
|
|
58
|
+
Partial<StackContextValue> & {
|
|
59
|
+
itemsCount?: number;
|
|
60
|
+
getDropElement?: (stackElement: HTMLDivElement) => HTMLDivElement;
|
|
61
|
+
separatorOnScroll?: number;
|
|
62
|
+
circularFocus?: boolean;
|
|
63
|
+
};
|
|
64
|
+
|
|
64
65
|
export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
65
66
|
(
|
|
66
67
|
{
|
|
@@ -68,7 +69,7 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
68
69
|
classNames,
|
|
69
70
|
style,
|
|
70
71
|
orientation = 'vertical',
|
|
71
|
-
rail = true,
|
|
72
|
+
rail = true, // TODO(burdon): Change default to false.
|
|
72
73
|
size = 'intrinsic',
|
|
73
74
|
onRearrange,
|
|
74
75
|
itemsCount = Children.count(children),
|
|
@@ -135,9 +136,9 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
135
136
|
);
|
|
136
137
|
|
|
137
138
|
/**
|
|
138
|
-
* Handles moving focus using the arrow keys. Focus is only handled by the nearest stack;
|
|
139
|
-
* orientation, focus cycles between items, otherwise focus is passed to an adjacent stack item;
|
|
140
|
-
* such stack item, focus is passed to the adjacent empty stack if one can be found.
|
|
139
|
+
* Handles moving focus using the arrow keys. Focus is only handled by the nearest stack;
|
|
140
|
+
* if the arrow key matches the orientation, focus cycles between items, otherwise focus is passed to an adjacent stack item;
|
|
141
|
+
* or, if there is no such stack item, focus is passed to the adjacent empty stack if one can be found.
|
|
141
142
|
*/
|
|
142
143
|
const handleKeyDown = useCallback(
|
|
143
144
|
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
@@ -241,12 +242,10 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
241
242
|
closestStackOrientation === 'vertical' ? ownedItemRect.top : ownedItemRect.left;
|
|
242
243
|
|
|
243
244
|
let closestDistance = Infinity;
|
|
244
|
-
|
|
245
245
|
for (const item of adjacentStackItems) {
|
|
246
246
|
const itemRect = item.getBoundingClientRect();
|
|
247
247
|
const itemPosition = closestStackOrientation === 'vertical' ? itemRect.top : itemRect.left;
|
|
248
248
|
const distance = Math.abs(itemPosition - targetPosition);
|
|
249
|
-
|
|
250
249
|
if (distance < closestDistance) {
|
|
251
250
|
closestDistance = distance;
|
|
252
251
|
closestItem = item;
|
|
@@ -294,6 +293,7 @@ export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
|
294
293
|
if (!rail) {
|
|
295
294
|
return orientation === 'horizontal' ? 'grid-rows-1 pli-[--stack-gap]' : 'grid-cols-1 plb-[--stack-gap]';
|
|
296
295
|
}
|
|
296
|
+
|
|
297
297
|
if (orientation === 'horizontal') {
|
|
298
298
|
return railGridHorizontal;
|
|
299
299
|
} else {
|
|
@@ -12,7 +12,7 @@ import { StackItem, type StackItemRootProps } from './StackItem';
|
|
|
12
12
|
|
|
13
13
|
const DefaultStory = (props: StackItemRootProps) => {
|
|
14
14
|
return (
|
|
15
|
-
<StackItem.Root role='section' {...props} classNames='
|
|
15
|
+
<StackItem.Root role='section' {...props} classNames='is-[20rem] border border-separator'>
|
|
16
16
|
<StackItem.Heading>
|
|
17
17
|
<span className='sr-only'>Title</span>
|
|
18
18
|
<div role='none' className='sticky -block-start-px bg-[--sticky-bg] p-1 is-full'>
|
|
@@ -27,17 +27,17 @@ export type StackItemContentProps = ThemedClassName<Omit<ComponentPropsWithoutRe
|
|
|
27
27
|
*/
|
|
28
28
|
scrollable?: boolean;
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
* Whether the consumer intends to do something custom and typical affordances should not apply.
|
|
32
|
-
*/
|
|
33
|
-
// TODO(burdon): This is cryptic; can we remove (only used by plugin-inbox?) Normalize toolbar?
|
|
34
|
-
layoutManaged?: boolean;
|
|
35
|
-
|
|
36
30
|
/**
|
|
37
31
|
* Whether to set a certain aspect ratio on the content, including the toolbar and statusbar.
|
|
38
32
|
* This is provided for convenience and consistency; it can instead be specified by the `classNames` or `style` props as needed.
|
|
39
33
|
*/
|
|
40
34
|
size?: 'intrinsic' | 'video' | 'square';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether the consumer intends to do something custom and typical affordances should not apply.
|
|
38
|
+
* @deprecated Replace with override for gridTempateRows.
|
|
39
|
+
*/
|
|
40
|
+
layoutManaged?: boolean;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -71,13 +71,13 @@ export const StackItemContent = forwardRef<HTMLDivElement, StackItemContentProps
|
|
|
71
71
|
{...props}
|
|
72
72
|
className={mx(
|
|
73
73
|
'group grid grid-cols-[100%] density-coarse',
|
|
74
|
-
stackItemSize === 'contain' && 'min-bs-0 overflow-hidden',
|
|
75
74
|
size === 'video' ? 'aspect-video' : size === 'square' && 'aspect-square',
|
|
76
|
-
|
|
75
|
+
stackItemSize === 'contain' && 'min-bs-0 overflow-hidden',
|
|
77
76
|
scrollable ? 'min-bs-0 overflow-y-auto scrollbar-thin contain-layout' : 'overflow-hidden',
|
|
78
77
|
role === 'section' &&
|
|
79
78
|
toolbar &&
|
|
80
79
|
'[&_.dx-toolbar]:sticky [&_.dx-toolbar]:z-[1] [&_.dx-toolbar]:block-start-0 [&_.dx-toolbar]:-mbe-px [&_.dx-toolbar]:min-is-0',
|
|
80
|
+
toolbar && '[&>.dx-toolbar]:relative [&>.dx-toolbar]:border-be [&>.dx-toolbar]:border-subduedSeparator',
|
|
81
81
|
classNames,
|
|
82
82
|
)}
|
|
83
83
|
style={style}
|
|
@@ -13,31 +13,31 @@ import React, {
|
|
|
13
13
|
} from 'react';
|
|
14
14
|
|
|
15
15
|
import { Icon, IconButton, type ThemedClassName, Toolbar, type ToolbarRootProps, useTranslation } from '@dxos/react-ui';
|
|
16
|
-
import { hoverableControls, mx } from '@dxos/react-ui-theme';
|
|
16
|
+
import { cardMinInlineSize, hoverableControls, mx } from '@dxos/react-ui-theme';
|
|
17
17
|
|
|
18
18
|
import { Image, StackItem } from '../../components';
|
|
19
19
|
import { translationKey } from '../../translations';
|
|
20
20
|
|
|
21
21
|
import { cardChrome, cardHeading, cardRoot, cardSpacing, cardText } from './fragments';
|
|
22
22
|
|
|
23
|
-
type SharedCardProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> & { asChild?: boolean };
|
|
24
|
-
|
|
25
23
|
/**
|
|
26
24
|
* The default width of cards. It should be no larger than 320px per WCAG 2.1 SC 1.4.10.
|
|
27
25
|
*/
|
|
28
|
-
const cardDefaultInlineSize =
|
|
26
|
+
const cardDefaultInlineSize = cardMinInlineSize;
|
|
27
|
+
|
|
29
28
|
/**
|
|
30
|
-
* This is `cardDefaultInlineSize` plus 2 times the sum of the inner and outer spacing applied by CardStack on the
|
|
31
|
-
* inline axis.
|
|
29
|
+
* This is `cardDefaultInlineSize` plus 2 times the sum of the inner and outer spacing applied by CardStack on the inline axis.
|
|
32
30
|
*/
|
|
33
31
|
const cardStackDefaultInlineSizeRem = cardDefaultInlineSize + 2.125;
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
type SharedCardProps = ThemedClassName<ComponentPropsWithoutRef<'div'>> & { asChild?: boolean };
|
|
34
|
+
|
|
35
|
+
const CardStaticRoot = forwardRef<HTMLDivElement, SharedCardProps & { id?: string }>(
|
|
36
|
+
({ children, classNames, id, asChild, role = 'group', ...props }, forwardedRef) => {
|
|
37
37
|
const Root = asChild ? Slot : 'div';
|
|
38
38
|
const rootProps = asChild ? { classNames: [cardRoot, classNames] } : { className: mx(cardRoot, classNames), role };
|
|
39
39
|
return (
|
|
40
|
-
<Root {...props} {...rootProps} ref={forwardedRef}>
|
|
40
|
+
<Root {...(id && { 'data-object-id': id })} {...props} {...rootProps} ref={forwardedRef}>
|
|
41
41
|
{children}
|
|
42
42
|
</Root>
|
|
43
43
|
);
|
|
@@ -45,18 +45,19 @@ const CardStaticRoot = forwardRef<HTMLDivElement, SharedCardProps>(
|
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* This should be used by Surface fulfillments in cases where the content may or may not already be encapsulated (e.g.
|
|
49
|
-
*
|
|
50
|
-
* it will render a `div` primitive with the appropriate styling for specific handled situations.
|
|
48
|
+
* This should be used by Surface fulfillments in cases where the content may or may not already be encapsulated (e.g., in a Popover) and knows this based on the `role` it receives.
|
|
49
|
+
* This will render a `Card.StaticRoot` by default, otherwise it will render a `div` primitive with the appropriate styling for specific handled situations.
|
|
51
50
|
*/
|
|
52
51
|
const CardSurfaceRoot = ({
|
|
52
|
+
id,
|
|
53
53
|
role = 'never',
|
|
54
54
|
children,
|
|
55
55
|
classNames,
|
|
56
|
-
}: ThemedClassName<PropsWithChildren<{ role?: string }>>) => {
|
|
56
|
+
}: ThemedClassName<PropsWithChildren<{ id?: string; role?: string }>>) => {
|
|
57
57
|
if (['card--popover', 'card--intrinsic', 'card--extrinsic'].includes(role)) {
|
|
58
58
|
return (
|
|
59
59
|
<div
|
|
60
|
+
{...(id && { 'data-object-id': id })}
|
|
60
61
|
className={mx(
|
|
61
62
|
role === 'card--popover'
|
|
62
63
|
? 'popover-card-width'
|
|
@@ -72,6 +73,7 @@ const CardSurfaceRoot = ({
|
|
|
72
73
|
} else {
|
|
73
74
|
return (
|
|
74
75
|
<CardStaticRoot
|
|
76
|
+
id={id}
|
|
75
77
|
classNames={[
|
|
76
78
|
role === 'card--transclusion' && 'mlb-1',
|
|
77
79
|
role === 'card--transclusion' && hoverableControls,
|
|
@@ -128,22 +130,26 @@ const CardDragPreview = StackItem.DragPreview;
|
|
|
128
130
|
|
|
129
131
|
const CardMenu = Primitive.div as FC<ComponentPropsWithRef<'div'>>;
|
|
130
132
|
|
|
131
|
-
type CardPosterProps =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
type CardPosterProps = ThemedClassName<
|
|
134
|
+
{
|
|
135
|
+
alt: string;
|
|
136
|
+
aspect?: 'video' | 'auto';
|
|
137
|
+
} & Partial<{ image: string; icon: string }>
|
|
138
|
+
>;
|
|
135
139
|
|
|
136
140
|
const CardPoster = (props: CardPosterProps) => {
|
|
137
141
|
const aspect = props.aspect === 'auto' ? 'aspect-auto' : 'aspect-video';
|
|
138
142
|
if (props.image) {
|
|
139
|
-
return
|
|
143
|
+
return (
|
|
144
|
+
<Image classNames={[`dx-card__poster is-full`, aspect, props.classNames]} src={props.image} alt={props.alt} />
|
|
145
|
+
);
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
if (props.icon) {
|
|
143
149
|
return (
|
|
144
150
|
<div
|
|
145
151
|
role='image'
|
|
146
|
-
className={mx(`dx-card__poster grid place-items-center bg-inputSurface text-subdued`, aspect)}
|
|
152
|
+
className={mx(`dx-card__poster grid place-items-center bg-inputSurface text-subdued`, aspect, props.classNames)}
|
|
147
153
|
aria-label={props.alt}
|
|
148
154
|
>
|
|
149
155
|
<Icon icon={props.icon} size={10} />
|