From ab0c26c65e77fbd5d76132e373980a017de5de44 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 10 Aug 2026 16:21:03 -0700 Subject: [PATCH 1/8] support illustration in attachment and center when preview only --- .../@react-spectrum/ai/src/AttachmentList.tsx | 17 ++++++- .../@react-spectrum/ai/src/HorizontalCard.tsx | 51 +++++++++++++++++-- .../ai/stories/AttachmentList.stories.tsx | 49 ++++++++++++++++++ .../ai/stories/PromptField.stories.tsx | 43 +--------------- 4 files changed, 113 insertions(+), 47 deletions(-) diff --git a/packages/@react-spectrum/ai/src/AttachmentList.tsx b/packages/@react-spectrum/ai/src/AttachmentList.tsx index da3e9cb4f49..d4f37a27a88 100644 --- a/packages/@react-spectrum/ai/src/AttachmentList.tsx +++ b/packages/@react-spectrum/ai/src/AttachmentList.tsx @@ -29,7 +29,7 @@ import {Button} from 'react-aria-components/Button'; import {CardProps} from '@react-spectrum/s2/Card'; import Cross from '../ui-icons/Cross'; import {forwardRef, ReactNode, useContext, useRef} from 'react'; -import {IconContext} from '@react-spectrum/s2/Icon'; +import {IconContext, IllustrationContext} from '@react-spectrum/s2/Icon'; import {ImageContext} from '@react-spectrum/s2/Image'; // @ts-ignore import intlMessages from '../intl/*.json'; @@ -228,12 +228,15 @@ function AttachmentContextProvider({ }) { let imageCtx = useContext(ImageContext); let iconCtx = useContext(IconContext); + let illustrationCtx = useContext(IllustrationContext); const opacityStyles = style({ opacity: {default: 1, isUploading: 0.15}, transition: 'default' })({isUploading}); const imageSlots = imageCtx && 'slots' in imageCtx ? imageCtx.slots : undefined; const iconSlots = iconCtx && 'slots' in iconCtx ? iconCtx.slots : undefined; + const illustrationSlots = + illustrationCtx && 'slots' in illustrationCtx ? illustrationCtx.slots : undefined; return ( {children} diff --git a/packages/@react-spectrum/ai/src/HorizontalCard.tsx b/packages/@react-spectrum/ai/src/HorizontalCard.tsx index 11b5828bbd6..a645be7064a 100644 --- a/packages/@react-spectrum/ai/src/HorizontalCard.tsx +++ b/packages/@react-spectrum/ai/src/HorizontalCard.tsx @@ -28,7 +28,7 @@ import {DOMProps, DOMRef, GlobalDOMAttributes} from '@react-types/shared'; import {filterDOMProps} from 'react-aria/filterDOMProps'; import {FooterContext} from '@react-spectrum/s2/Footer'; import {GridListItem, GridListItemProps} from 'react-aria-components/GridList'; -import {IconContext} from '@react-spectrum/s2/Icon'; +import {IconContext, IllustrationContext} from '@react-spectrum/s2/Icon'; import {ImageContext} from '@react-spectrum/s2/Image'; import {ImageCoordinator} from '@react-spectrum/s2/ImageCoordinator'; import {inertValue} from 'react-aria/private/utils/inertValue'; @@ -212,6 +212,9 @@ let card = style({ alignItems: { isBasic: 'center' }, + justifyContent: { + [onlyPreview]: 'center' + }, '--card-padding-y': { type: 'paddingTop', value: { @@ -810,13 +813,43 @@ export const HorizontalCard = forwardRef(function HorizontalCard( ); }); -const iconThumbnailStyles = style({ +// same as basic-thumb-size but isnt full size when only preview +const iconThumbnailSize = { + size: { + XS: 24, + S: 26, + M: 32, + L: 36, + XL: 40 + } +} as const; + +const illustThumbnailSize = { + size: { + XS: 32, + S: 36, + M: 40, + L: 44, + XL: 48 + } +} as const; + +const iconThumbnailStyles = style<{size: 'XS' | 'S' | 'M' | 'L' | 'XL'}>({ + position: 'relative', + alignSelf: 'center', + flexShrink: 0, + pointerEvents: 'none', + userSelect: 'none', + size: iconThumbnailSize +}); + +const illustThumbnailStyles = style<{size: 'XS' | 'S' | 'M' | 'L' | 'XL'}>({ position: 'relative', alignSelf: 'center', flexShrink: 0, pointerEvents: 'none', userSelect: 'none', - size: '--basic-thumb-size' + size: illustThumbnailSize }); export const BasicHorizontalCard = forwardRef(function BasicHorizontalCard( @@ -863,7 +896,17 @@ export const BasicHorizontalCard = forwardRef(function BasicHorizontalCard( slots: { icon: {}, thumbnail: { - styles: iconThumbnailStyles + styles: iconThumbnailStyles({size}) + } + } + } + ], + [ + IllustrationContext, + { + slots: { + thumbnail: { + styles: illustThumbnailStyles({size}) } } } diff --git a/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx b/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx index 57dc6287524..1227797ecc3 100644 --- a/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx +++ b/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx @@ -15,6 +15,7 @@ import {categorizeArgTypes, getActionArgs} from '../../s2/stories/utils'; import {Content} from '@react-spectrum/s2/Content'; import File from '@react-spectrum/s2/icons/File'; import FileText from '@react-spectrum/s2/icons/FileText'; +import FileTextIllustration from '../../s2/spectrum-illustrations/gradient/generic1/FileText'; import {Image} from '@react-spectrum/s2/Image'; import type {Meta, StoryObj} from '@storybook/react'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; @@ -165,3 +166,51 @@ export const LongContents: Story = { ) }; + +export const Mixed: Story = { + name: 'Mixed attachements', + render: (args: any) => ( +
+ + + + + + + + + + + + + + + + banner.png + PNG image + + + + + + notes.txt + Plain text + + + + + + notes.txt + Plain text + + + +
+ ) +}; diff --git a/packages/@react-spectrum/ai/stories/PromptField.stories.tsx b/packages/@react-spectrum/ai/stories/PromptField.stories.tsx index e8c004ba22d..ffd8099a434 100644 --- a/packages/@react-spectrum/ai/stories/PromptField.stories.tsx +++ b/packages/@react-spectrum/ai/stories/PromptField.stories.tsx @@ -315,46 +315,6 @@ function EverythingRender(args) { }); }; - let isFieldEmpty = (prompt: TokenFieldValue) => { - let text = prompt.toString(); - return text === '' && !text.includes('\n'); - }; - - // logic for using up/down arrow keys to fill field with previous prompts - let onKeyDown = (e: React.KeyboardEvent) => { - let canNavigate = historyIndexRef.current !== -1 || isFieldEmpty(value); - let history = historyRef.current; - if (!canNavigate || history.length === 0) { - return; - } - - if (e.key === 'ArrowUp') { - e.preventDefault(); - let nextIndex = - historyIndexRef.current === -1 - ? history.length - 1 - : Math.max(0, historyIndexRef.current - 1); - historyIndexRef.current = nextIndex; - isHistoryNavigating.current = true; - setValue(history[nextIndex]); - } else if (e.key === 'ArrowDown') { - if (historyIndexRef.current === -1) { - return; - } - e.preventDefault(); - let nextIndex = historyIndexRef.current + 1; - if (nextIndex >= history.length) { - historyIndexRef.current = -1; - isHistoryNavigating.current = true; - setValue(new PromptFieldValue([])); - } else { - historyIndexRef.current = nextIndex; - isHistoryNavigating.current = true; - setValue(history[nextIndex]); - } - } - }; - let handleChange = (newValue: TokenFieldValue) => { if (!isHistoryNavigating.current) { // if user edits the field, then we want to reset the index so up arrow starts from latest prompt again @@ -446,8 +406,7 @@ function EverythingRender(args) { } pixelLoader={data[args.pixelLoader]} placeholder={placeholder} - menuWidth={menuWidth} - onKeyDown={onKeyDown}> + menuWidth={menuWidth}> {segment => ( {icons[segment.value?.type]} From b18f49be60d2aa0380eba0080b68bdb8e645bc18 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Mon, 10 Aug 2026 16:36:10 -0700 Subject: [PATCH 2/8] switch to only use Illustration --- .../@react-spectrum/ai/src/AttachmentList.tsx | 16 +------- .../@react-spectrum/ai/src/HorizontalCard.tsx | 34 +--------------- .../ai/stories/AttachmentList.stories.tsx | 40 ++++++++++++------- 3 files changed, 28 insertions(+), 62 deletions(-) diff --git a/packages/@react-spectrum/ai/src/AttachmentList.tsx b/packages/@react-spectrum/ai/src/AttachmentList.tsx index d4f37a27a88..eb25f5b1f11 100644 --- a/packages/@react-spectrum/ai/src/AttachmentList.tsx +++ b/packages/@react-spectrum/ai/src/AttachmentList.tsx @@ -29,7 +29,7 @@ import {Button} from 'react-aria-components/Button'; import {CardProps} from '@react-spectrum/s2/Card'; import Cross from '../ui-icons/Cross'; import {forwardRef, ReactNode, useContext, useRef} from 'react'; -import {IconContext, IllustrationContext} from '@react-spectrum/s2/Icon'; +import {IllustrationContext} from '@react-spectrum/s2/Icon'; import {ImageContext} from '@react-spectrum/s2/Image'; // @ts-ignore import intlMessages from '../intl/*.json'; @@ -227,14 +227,12 @@ function AttachmentContextProvider({ isUploading: boolean; }) { let imageCtx = useContext(ImageContext); - let iconCtx = useContext(IconContext); let illustrationCtx = useContext(IllustrationContext); const opacityStyles = style({ opacity: {default: 1, isUploading: 0.15}, transition: 'default' })({isUploading}); const imageSlots = imageCtx && 'slots' in imageCtx ? imageCtx.slots : undefined; - const iconSlots = iconCtx && 'slots' in iconCtx ? iconCtx.slots : undefined; const illustrationSlots = illustrationCtx && 'slots' in illustrationCtx ? illustrationCtx.slots : undefined; @@ -254,18 +252,6 @@ function AttachmentContextProvider({ } } ], - [ - IconContext, - { - slots: { - ...iconSlots, - thumbnail: { - ...iconSlots?.thumbnail, - styles: mergeStyles(iconSlots?.thumbnail?.styles, opacityStyles) - } - } - } - ], [ IllustrationContext, { diff --git a/packages/@react-spectrum/ai/src/HorizontalCard.tsx b/packages/@react-spectrum/ai/src/HorizontalCard.tsx index a645be7064a..7f88e557b26 100644 --- a/packages/@react-spectrum/ai/src/HorizontalCard.tsx +++ b/packages/@react-spectrum/ai/src/HorizontalCard.tsx @@ -28,7 +28,7 @@ import {DOMProps, DOMRef, GlobalDOMAttributes} from '@react-types/shared'; import {filterDOMProps} from 'react-aria/filterDOMProps'; import {FooterContext} from '@react-spectrum/s2/Footer'; import {GridListItem, GridListItemProps} from 'react-aria-components/GridList'; -import {IconContext, IllustrationContext} from '@react-spectrum/s2/Icon'; +import {IllustrationContext} from '@react-spectrum/s2/Icon'; import {ImageContext} from '@react-spectrum/s2/Image'; import {ImageCoordinator} from '@react-spectrum/s2/ImageCoordinator'; import {inertValue} from 'react-aria/private/utils/inertValue'; @@ -813,17 +813,7 @@ export const HorizontalCard = forwardRef(function HorizontalCard( ); }); -// same as basic-thumb-size but isnt full size when only preview -const iconThumbnailSize = { - size: { - XS: 24, - S: 26, - M: 32, - L: 36, - XL: 40 - } -} as const; - +// same as basic-thumb-size (with changes to accomodate for illustration padding) but isnt full size when only preview const illustThumbnailSize = { size: { XS: 32, @@ -834,15 +824,6 @@ const illustThumbnailSize = { } } as const; -const iconThumbnailStyles = style<{size: 'XS' | 'S' | 'M' | 'L' | 'XL'}>({ - position: 'relative', - alignSelf: 'center', - flexShrink: 0, - pointerEvents: 'none', - userSelect: 'none', - size: iconThumbnailSize -}); - const illustThumbnailStyles = style<{size: 'XS' | 'S' | 'M' | 'L' | 'XL'}>({ position: 'relative', alignSelf: 'center', @@ -890,17 +871,6 @@ export const BasicHorizontalCard = forwardRef(function BasicHorizontalCard( } } ], - [ - IconContext, - { - slots: { - icon: {}, - thumbnail: { - styles: iconThumbnailStyles({size}) - } - } - } - ], [ IllustrationContext, { diff --git a/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx b/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx index 1227797ecc3..e307ed4b3cb 100644 --- a/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx +++ b/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx @@ -13,9 +13,9 @@ import {Attachment as AttachmentComponent, AttachmentList} from '../src/AttachmentList'; import {categorizeArgTypes, getActionArgs} from '../../s2/stories/utils'; import {Content} from '@react-spectrum/s2/Content'; -import File from '@react-spectrum/s2/icons/File'; -import FileText from '@react-spectrum/s2/icons/FileText'; import FileTextIllustration from '../../s2/spectrum-illustrations/gradient/generic1/FileText'; +import FileVideo from '@react-spectrum/s2/illustrations/gradient/generic1/FileVideo'; +import FileZip from '@react-spectrum/s2/illustrations/gradient/generic1/FileZip'; import {Image} from '@react-spectrum/s2/Image'; import type {Meta, StoryObj} from '@storybook/react'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; @@ -107,20 +107,19 @@ function NonImageAttachmentListRender(args) { let {isInvalid, size, uploadProgress, ...listArgs} = args; return ( - {/* TODO: what should the thumbnail only look like with icons */} - + - + notes.txt Plain text document @@ -131,7 +130,7 @@ function NonImageAttachmentListRender(args) { isInvalid={isInvalid} size={size} aria-label="data.csv"> - + data.csv @@ -170,7 +169,7 @@ export const LongContents: Story = { export const Mixed: Story = { name: 'Mixed attachements', render: (args: any) => ( -
+
- - - + + + + + + @@ -197,17 +199,25 @@ export const Mixed: Story = { - + notes.txt Plain text - - + + + - notes.txt - Plain text + video.mp4 + MP4 + + + + + + debug.zip + ZIP From cfcb6d03280859bda232279b680f555dfc128f89 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 11 Aug 2026 09:39:29 -0700 Subject: [PATCH 3/8] adjusting size of preview only illustration and reducing gapbetween illustration and text --- .../@react-spectrum/ai/src/HorizontalCard.tsx | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/@react-spectrum/ai/src/HorizontalCard.tsx b/packages/@react-spectrum/ai/src/HorizontalCard.tsx index 7f88e557b26..7f446cef2ae 100644 --- a/packages/@react-spectrum/ai/src/HorizontalCard.tsx +++ b/packages/@react-spectrum/ai/src/HorizontalCard.tsx @@ -252,6 +252,20 @@ let card = style({ }, [onlyPreview]: 'full' } + }, + '--illust-thumb-size': { + type: 'height', + value: { + default: 40, + size: { + XS: 32, + S: 36, + M: 40, + L: 44, + XL: 48 + }, + [onlyPreview]: 'full' + } } }); @@ -813,24 +827,14 @@ export const HorizontalCard = forwardRef(function HorizontalCard( ); }); -// same as basic-thumb-size (with changes to accomodate for illustration padding) but isnt full size when only preview -const illustThumbnailSize = { - size: { - XS: 32, - S: 36, - M: 40, - L: 44, - XL: 48 - } -} as const; - -const illustThumbnailStyles = style<{size: 'XS' | 'S' | 'M' | 'L' | 'XL'}>({ +const illustThumbnailStyles = style({ position: 'relative', alignSelf: 'center', flexShrink: 0, pointerEvents: 'none', userSelect: 'none', - size: illustThumbnailSize + size: '--illust-thumb-size', + marginX: -8 }); export const BasicHorizontalCard = forwardRef(function BasicHorizontalCard( @@ -876,7 +880,7 @@ export const BasicHorizontalCard = forwardRef(function BasicHorizontalCard( { slots: { thumbnail: { - styles: illustThumbnailStyles({size}) + styles: illustThumbnailStyles } } } From 91467c7655507d3c2637c069a62a79c03f63a707 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 11 Aug 2026 11:35:39 -0700 Subject: [PATCH 4/8] handle preview only invalid better, in progress case with illustration, get rid of reliance on horizontal card --- .../@react-spectrum/ai/src/AttachmentList.tsx | 308 +++++++++++++++++- .../@react-spectrum/ai/src/HorizontalCard.tsx | 38 --- .../ai/stories/AttachmentList.stories.tsx | 67 +++- 3 files changed, 348 insertions(+), 65 deletions(-) diff --git a/packages/@react-spectrum/ai/src/AttachmentList.tsx b/packages/@react-spectrum/ai/src/AttachmentList.tsx index eb25f5b1f11..c9c48beb597 100644 --- a/packages/@react-spectrum/ai/src/AttachmentList.tsx +++ b/packages/@react-spectrum/ai/src/AttachmentList.tsx @@ -20,23 +20,27 @@ import { } from '@react-types/shared'; import { baseColor, + color, focusRing, iconStyle, + lightDark, + space, style } from '@react-spectrum/s2/style' with {type: 'macro'}; -import {BasicHorizontalCard} from './HorizontalCard'; import {Button} from 'react-aria-components/Button'; import {CardProps} from '@react-spectrum/s2/Card'; +import {ContentContext} from '@react-spectrum/s2/Content'; import Cross from '../ui-icons/Cross'; +import {DEFAULT_SLOT, Provider} from 'react-aria-components/slots'; import {forwardRef, ReactNode, useContext, useRef} from 'react'; import {IllustrationContext} from '@react-spectrum/s2/Icon'; import {ImageContext} from '@react-spectrum/s2/Image'; +import {ImageCoordinator} from '@react-spectrum/s2/ImageCoordinator'; // @ts-ignore import intlMessages from '../intl/*.json'; import {mergeStyles} from '@react-spectrum/s2/mergeStyles'; import {pressScale} from '@react-spectrum/s2/pressScale'; import {ProgressCircle} from '@react-spectrum/s2/ProgressCircle'; -import {Provider} from 'react-aria-components/slots'; import {StyleString} from '@react-spectrum/s2/style' with {type: 'macro'}; import { Tag, @@ -46,6 +50,7 @@ import { TagListProps, TagProps } from 'react-aria-components/TagGroup'; +import {TextContext} from '@react-spectrum/s2/Text'; import {useDOMRef} from './useDOMRef'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; @@ -111,6 +116,183 @@ const styles = style<{ disableTapHighlight: true }); +const onlyPreview = ':not(:has([data-slot=content])):not(:has([data-slot=preview]))'; +const noDescription = ':not(:has([slot=description]))'; + +const attachmentCard = style({ + display: 'flex', + flexDirection: 'row', + position: 'relative', + borderRadius: 'default', + backgroundColor: { + default: lightDark('transparent-white-300', 'transparent-black-300'), + forcedColors: 'ButtonFace' + }, + boxShadow: { + default: `[inset 0 0 0 1px light-dark(${color('transparent-black-300')}, ${color('transparent-white-300')})]`, + isInvalid: `[inset 0 0 0 1px ${color('negative-900')}]` + }, + forcedColorAdjust: 'none', + transition: 'default', + fontFamily: 'sans', + overflow: 'clip', + contain: 'layout', + disableTapHighlight: true, + height: { + default: 68, + size: { + XS: 52, + S: 60, + M: 68, + L: 76, + XL: 80 + } + }, + width: { + default: 'full', + [onlyPreview]: 'auto' + }, + aspectRatio: { + [onlyPreview]: '1/1' + }, + '--card-spacing': { + type: 'paddingTop', + value: { + size: { + XS: 8, + S: 12, + M: 16, + L: 20, + XL: 24 + }, + [onlyPreview]: 0 + } + }, + alignItems: 'center', + + '--card-padding-y': { + type: 'paddingTop', + value: {default: '--card-spacing'} + }, + '--card-padding-x': { + type: 'paddingStart', + value: {default: '--card-spacing'} + }, + paddingY: '--card-padding-y', + paddingX: '--card-padding-x', + boxSizing: 'border-box', + justifyContent: { + [onlyPreview]: 'center' + }, + '--basic-thumb-size': { + type: 'height', + value: { + size: { + XS: 24, + S: 26, + M: 32, + L: 36, + XL: 40 + }, + [onlyPreview]: 'full' + } + }, + '--illust-thumb-size': { + type: 'height', + value: { + size: { + XS: 48, + S: 44, + M: 48, + L: 52, + XL: 56 + }, + [onlyPreview]: 'full' + } + }, + '--illust-margin-x': { + type: 'marginStart', + value: { + size: { + XS: -8, + S: -8, + M: -12, + L: -12, + XL: -12 + } + } + } +}); + +const illustThumbnailStyles = style({ + position: 'relative', + alignSelf: 'center', + flexShrink: 0, + pointerEvents: 'none', + userSelect: 'none', + size: '--illust-thumb-size', + marginX: '--illust-margin-x' +}); + +const attachmentTitle = style<{size: 'XS' | 'S' | 'M' | 'L' | 'XL'}>({ + font: 'title', + fontSize: { + size: { + XS: 'title-xs', + S: 'title-xs', + M: 'title-sm', + L: 'title', + XL: 'title-lg' + } + }, + lineClamp: 1, + gridArea: 'title' +}); + +const attachmentDescription = style<{size: 'XS' | 'S' | 'M' | 'L' | 'XL'}>({ + font: 'body', + fontSize: { + size: { + XS: 'body-2xs', + S: 'body-2xs', + M: 'body-xs', + L: 'body-sm', + XL: 'body' + } + }, + lineClamp: 1, + gridArea: 'description' +}); + +const attachmentContent = style<{size: 'XS' | 'S' | 'M' | 'L' | 'XL'}>({ + display: 'grid', + gridTemplateColumns: ['minmax(0, 1fr)'], + gridTemplateAreas: ['title', 'description'], + columnGap: 4, + flexGrow: 1, + minWidth: 0, + alignItems: 'baseline', + alignContent: 'start', + rowGap: { + size: { + XS: 4, + S: 4, + M: space(6), + L: space(6), + XL: 8 + }, + [noDescription]: 0 + }, + paddingStart: { + default: '--card-spacing', + ':first-child': 0 + }, + paddingEnd: { + default: 'calc(var(--card-spacing) * 1.5 / 2)', + ':last-child': 0 + } +}); + const CloseButton = function CloseButton(props) { let ref = useRef(null); // oxlint-disable react/react-compiler @@ -212,7 +394,22 @@ const attachmentErrorStyles = style({ display: 'flex', flexShrink: 0, alignItems: 'center', - paddingStart: 8, + paddingStart: { + default: 8, + ':not([data-slot=content] ~ *)': 0 + }, + position: { + ':not([data-slot=content] ~ *)': 'absolute' + }, + top: { + ':not([data-slot=content] ~ *)': '50%' + }, + insetStart: { + ':not([data-slot=content] ~ *)': '50%' + }, + transform: { + ':not([data-slot=content] ~ *)': 'translate(-50%, -50%)' + }, '--iconPrimary': { type: 'color', value: 'negative' @@ -221,17 +418,26 @@ const attachmentErrorStyles = style({ function AttachmentContextProvider({ children, - isUploading + isUploading, + isInvalid }: { children: ReactNode; isUploading: boolean; + isInvalid?: boolean; }) { let imageCtx = useContext(ImageContext); let illustrationCtx = useContext(IllustrationContext); const opacityStyles = style({ - opacity: {default: 1, isUploading: 0.15}, + opacity: { + default: 1, + isUploading: 0.15, + isInvalid: { + default: 1, + ':not(:has(~ [data-slot=content]))': 0.15 + } + }, transition: 'default' - })({isUploading}); + })({isUploading, isInvalid}); const imageSlots = imageCtx && 'slots' in imageCtx ? imageCtx.slots : undefined; const illustrationSlots = illustrationCtx && 'slots' in illustrationCtx ? illustrationCtx.slots : undefined; @@ -270,6 +476,82 @@ function AttachmentContextProvider({ ); } +interface AttachmentCardProps { + size?: 'XS' | 'S' | 'M' | 'L' | 'XL'; + isInvalid?: boolean; + children: ReactNode; +} + +function AttachmentCard({size = 'M', isInvalid = false, children}: AttachmentCardProps) { + return ( +
+ + {children} + +
+ ); +} + export const Attachment = forwardRef(function Attachment( props: AttachmentProps, ref: DOMRef @@ -283,8 +565,7 @@ export const Attachment = forwardRef(function Attachment( styles, isInvalid, children, - size = 'M', - ...otherProps + size = 'M' } = props; let domRef = useDOMRef(ref); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/ai'); @@ -297,7 +578,7 @@ export const Attachment = forwardRef(function Attachment( aria-describedby={ariaDescribedby} ref={domRef} className={renderProps => mergeStyles(tagStyles({...renderProps}), styles)}> - + {props.uploadProgress != null && props.uploadProgress < 100 && (
@@ -320,6 +603,7 @@ export const Attachment = forwardRef(function Attachment(
)} {typeof children === 'function' ? children({size}) : children} @@ -328,7 +612,7 @@ export const Attachment = forwardRef(function Attachment(
)} - + {/** Definitely not a close button, though looks like one. */}
@@ -875,16 +847,6 @@ export const BasicHorizontalCard = forwardRef(function BasicHorizontalCard( } } ], - [ - IllustrationContext, - { - slots: { - thumbnail: { - styles: illustThumbnailStyles - } - } - } - ], [ FooterContext, { diff --git a/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx b/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx index e307ed4b3cb..e4c9f865ec5 100644 --- a/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx +++ b/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx @@ -166,29 +166,50 @@ export const LongContents: Story = { ) }; -export const Mixed: Story = { - name: 'Mixed attachements', - render: (args: any) => ( +function MixedAttachments(args) { + let {isInvalid, size, uploadProgress, ...listArgs} = args; + + return (
- - + + - + - + - + - - + + PNG image - + notes.txt Plain text - - + video.mp4 MP4 - + debug.zip @@ -222,5 +254,10 @@ export const Mixed: Story = {
- ) + ); +} + +export const Mixed: Story = { + name: 'Mixed attachements', + render: args => }; From bcd66dc85d1b32eb98379fffab667cdbc6b75a2e Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 11 Aug 2026 11:37:17 -0700 Subject: [PATCH 5/8] delete horizontal card stories --- .../stories/BasicHorizontalCard.stories.tsx | 146 ------------------ .../ai/stories/HorizontalCard.stories.tsx | 112 -------------- 2 files changed, 258 deletions(-) delete mode 100644 packages/@react-spectrum/ai/stories/BasicHorizontalCard.stories.tsx delete mode 100644 packages/@react-spectrum/ai/stories/HorizontalCard.stories.tsx diff --git a/packages/@react-spectrum/ai/stories/BasicHorizontalCard.stories.tsx b/packages/@react-spectrum/ai/stories/BasicHorizontalCard.stories.tsx deleted file mode 100644 index 37290d5f691..00000000000 --- a/packages/@react-spectrum/ai/stories/BasicHorizontalCard.stories.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2024 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import {ActionButton} from '@react-spectrum/s2/ActionButton'; -import {ActionMenu} from '@react-spectrum/s2/ActionMenu'; -import {type BasicCardProps, BasicHorizontalCard} from '../src/HorizontalCard'; -import {categorizeArgTypes, getActionArgs} from '../../s2/stories/utils'; -import ChevronRight from '@react-spectrum/s2/icons/ChevronRight'; -import {Content} from '@react-spectrum/s2/Content'; -import {Footer} from '@react-spectrum/s2/Footer'; -import {Image} from '@react-spectrum/s2/Image'; -import {MenuItem} from '@react-spectrum/s2/Menu'; -import type {Meta, StoryObj} from '@storybook/react'; -import {Skeleton} from '@react-spectrum/s2/Skeleton'; -import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; -import {Text} from '@react-spectrum/s2/Text'; - -const events = ['onAction']; - -const meta: Meta = { - component: BasicHorizontalCard, - parameters: { - layout: 'centered' - }, - tags: ['autodocs'], - args: { - isLoading: false, - ...getActionArgs(events) - }, - argTypes: { - ...categorizeArgTypes('Events', events), - href: {table: {disable: true}}, - download: {table: {disable: true}}, - hrefLang: {table: {disable: true}}, - referrerPolicy: {table: {disable: true}}, - rel: {table: {disable: true}}, - routerOptions: {table: {disable: true}}, - ping: {table: {disable: true}}, - target: {table: {disable: true}}, - value: {table: {disable: true}}, - textValue: {table: {disable: true}}, - onAction: {table: {disable: true, category: 'Events'}}, - isDisabled: {table: {disable: true}}, - children: {table: {disable: true}} - }, - decorators: (children, {args}) => ( - {children(args)} - ), - title: 'AI/BasicHorizontalCard' -}; - -export default meta; - -type BasicStory = StoryObj; - -export const Basic: BasicStory = { - render: args => ( -
- - - - Card title - Card description. - -
- - Test - -
-
- - - - Card title - Card description. - -
- - - -
-
- - - - Card title - Card description. - - - - - - Card title - Card description. - - - - - -
- ), - argTypes: { - variant: { - control: 'radio', - options: ['primary', 'secondary', 'tertiary', 'quiet'] - } - } -}; diff --git a/packages/@react-spectrum/ai/stories/HorizontalCard.stories.tsx b/packages/@react-spectrum/ai/stories/HorizontalCard.stories.tsx deleted file mode 100644 index 246f42c8188..00000000000 --- a/packages/@react-spectrum/ai/stories/HorizontalCard.stories.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2024 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import {CardPreview, HorizontalCard, type HorizontalCardProps} from '../src/HorizontalCard'; -import {categorizeArgTypes, getActionArgs} from '../../s2/stories/utils'; -import {Content} from '@react-spectrum/s2/Content'; -import {Image} from '@react-spectrum/s2/Image'; -import type {Meta, StoryObj} from '@storybook/react'; -import {Skeleton} from '@react-spectrum/s2/Skeleton'; -import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; -import {Text} from '@react-spectrum/s2/Text'; - -const events = ['onAction']; - -const meta: Meta = { - component: HorizontalCard, - parameters: { - layout: 'centered' - }, - tags: ['autodocs'], - args: { - isLoading: false, - ...getActionArgs(events) - }, - argTypes: { - ...categorizeArgTypes('Events', events), - href: {table: {disable: true}}, - download: {table: {disable: true}}, - hrefLang: {table: {disable: true}}, - referrerPolicy: {table: {disable: true}}, - rel: {table: {disable: true}}, - routerOptions: {table: {disable: true}}, - ping: {table: {disable: true}}, - target: {table: {disable: true}}, - value: {table: {disable: true}}, - textValue: {table: {disable: true}}, - onAction: {table: {disable: true, category: 'Events'}}, - isDisabled: {table: {disable: true}}, - children: {table: {disable: true}} - }, - decorators: (children, {args}) => ( - {children(args)} - ), - title: 'AI/HorizontalCard' -}; - -export default meta; - -type Story = StoryObj; - -export const Horizontal: Story = { - render: args => ( -
- - - - - - - Card title - - Card description. Give a concise overview of the context or functionality that's - mentioned in the card title. - - - - - - - - - - - Card title - - - Card description. Give a concise overview of the context or functionality that's - mentioned in the card title. - - - -
- ) -}; From e8eab532ce53dd4754c544c9ab9da04abbe4a3eb Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 11 Aug 2026 11:58:06 -0700 Subject: [PATCH 6/8] ignore codex config for scout --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ac8c322e491..a96bd943803 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ starters/tailwind/registry starters/docs/yarn.lock starters/tailwind/yarn.lock .scout/ +.codex/ From b20a6abd18406d49987ce7d266f077475735e011 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 11 Aug 2026 16:38:03 -0700 Subject: [PATCH 7/8] review updates --- .../@react-spectrum/ai/src/AttachmentList.tsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/@react-spectrum/ai/src/AttachmentList.tsx b/packages/@react-spectrum/ai/src/AttachmentList.tsx index c9c48beb597..cd36b61eb62 100644 --- a/packages/@react-spectrum/ai/src/AttachmentList.tsx +++ b/packages/@react-spectrum/ai/src/AttachmentList.tsx @@ -390,25 +390,28 @@ const tagStyles = style({ borderRadius: 'default' }); +// this is checking that there isn't content in the attachment +// similar to onlyPreview, but specifically checking siblings before the alert icon (aka looking for Content) +const onlyPreviewFromError = ':not([data-slot=content] ~ *)'; const attachmentErrorStyles = style({ display: 'flex', flexShrink: 0, alignItems: 'center', paddingStart: { default: 8, - ':not([data-slot=content] ~ *)': 0 + [onlyPreviewFromError]: 0 }, position: { - ':not([data-slot=content] ~ *)': 'absolute' + [onlyPreviewFromError]: 'absolute' }, top: { - ':not([data-slot=content] ~ *)': '50%' + [onlyPreviewFromError]: '50%' }, insetStart: { - ':not([data-slot=content] ~ *)': '50%' + [onlyPreviewFromError]: '50%' }, transform: { - ':not([data-slot=content] ~ *)': 'translate(-50%, -50%)' + [onlyPreviewFromError]: 'translate(-50%, -50%)' }, '--iconPrimary': { type: 'color', @@ -416,6 +419,9 @@ const attachmentErrorStyles = style({ } }); +// this is also checking that there isn't content in the attachment +// similar to onlyPreview, but specifically checking siblings after the thumbnail (aka looking for Content) +const onlyPreviewFromThumbnail = ':not(:has(~ [data-slot=content]))'; function AttachmentContextProvider({ children, isUploading, @@ -433,7 +439,7 @@ function AttachmentContextProvider({ isUploading: 0.15, isInvalid: { default: 1, - ':not(:has(~ [data-slot=content]))': 0.15 + [onlyPreviewFromThumbnail]: 0.15 } }, transition: 'default' @@ -586,8 +592,10 @@ export const Attachment = forwardRef(function Attachment( top: '50%', insetStart: { default: '50%', + // this checks that there is text content in the attachment + a Image as a thumbnail ':has(~ [data-slot=content]):not(:has(~ [data-rsp-slot=thumbnail]))': '[calc(var(--card-padding-x) + var(--basic-thumb-size) / 2)]', + // this checks that there is text content in the attachment + a Illustration as a thumbnail ':has(~ [data-slot=content]):has(~ [data-rsp-slot=thumbnail])': '[calc(var(--card-padding-x) + var(--illust-margin-x) + var(--illust-thumb-size) / 2)]' }, From cff01057bbcc19d0513daf08b0c33037e6b434ca Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 11 Aug 2026 16:41:48 -0700 Subject: [PATCH 8/8] delete horizontal card --- .../@react-spectrum/ai/src/HorizontalCard.tsx | 869 ------------------ 1 file changed, 869 deletions(-) delete mode 100644 packages/@react-spectrum/ai/src/HorizontalCard.tsx diff --git a/packages/@react-spectrum/ai/src/HorizontalCard.tsx b/packages/@react-spectrum/ai/src/HorizontalCard.tsx deleted file mode 100644 index 9b48abe1963..00000000000 --- a/packages/@react-spectrum/ai/src/HorizontalCard.tsx +++ /dev/null @@ -1,869 +0,0 @@ -/* - * Copyright 2026 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import {ActionMenuContext} from '@react-spectrum/s2/ActionMenu'; -import {ButtonContext} from '@react-spectrum/s2/Button'; -import {Checkbox} from '@react-spectrum/s2/Checkbox'; -import { - color, - focusRing, - lightDark, - space, - style -} from '@react-spectrum/s2/style' with {type: 'macro'}; -import {composeRenderProps} from 'react-aria-components/composeRenderProps'; -import {ContentContext} from '@react-spectrum/s2/Content'; -import {createContext, forwardRef, ReactNode, useContext} from 'react'; -import {DEFAULT_SLOT, Provider} from 'react-aria-components/slots'; -import {DOMProps, DOMRef, GlobalDOMAttributes} from '@react-types/shared'; -import {filterDOMProps} from 'react-aria/filterDOMProps'; -import {FooterContext} from '@react-spectrum/s2/Footer'; -import {GridListItem, GridListItemProps} from 'react-aria-components/GridList'; -import {ImageContext} from '@react-spectrum/s2/Image'; -import {ImageCoordinator} from '@react-spectrum/s2/ImageCoordinator'; -import {inertValue} from 'react-aria/private/utils/inertValue'; -import {Link} from 'react-aria-components/Link'; -import {LinkButtonContext} from '@react-spectrum/s2/LinkButton'; -import {mergeStyles} from '@react-spectrum/s2/mergeStyles'; -import {pressScale} from '@react-spectrum/s2/pressScale'; -import {SkeletonContext, useIsSkeleton} from '@react-spectrum/s2/Skeleton'; -import {StyleString} from '@react-spectrum/s2/style' with {type: 'macro'}; -import {TextContext} from '@react-spectrum/s2/Text'; -import {useDOMRef} from './useDOMRef'; -interface HorizontalCardRenderProps { - /** The size of the Card. */ - size: 'XS' | 'S' | 'M' | 'L' | 'XL'; -} - -export interface HorizontalCardProps extends Omit< - GridListItemProps, - | 'className' - | 'style' - | 'render' - | 'children' - | 'onHoverChange' - | 'onHoverStart' - | 'onHoverEnd' - | 'onClick' - | keyof GlobalDOMAttributes -> { - /** The children of the Card. */ - children: ReactNode | ((renderProps: HorizontalCardRenderProps) => ReactNode); - /** - * The size of the Card. - * - * @default 'M' - */ - size?: 'XS' | 'S' | 'M' | 'L' | 'XL'; - /** - * The amount of internal padding within the Card. - * - * @default 'regular' - */ - density?: 'compact' | 'regular' | 'spacious'; - /** - * The visual style of the Card. - * - * @default 'primary' - */ - variant?: 'primary' | 'secondary' | 'tertiary'; - /** - * Spectrum-defined styles, returned by the `style()` macro. - */ - styles?: StyleString; -} - -export interface BasicCardProps extends Omit { - /** - * The visual style of the Card. - * - * @default 'primary' - */ - variant?: 'primary' | 'secondary' | 'tertiary' | 'quiet'; - /** Whether the card is in an error state. */ - isInvalid?: boolean; -} - -const borderRadius = { - default: 'lg', - size: { - XS: 'default', - S: 'default' - }, - isBasic: 'default' -} as const; - -// Figma missing a lot of combinations of variant, tshirt, density -// Quiet Basic cards? -// Does Basic not participate in selection? (It does, but it's denoted by the border...) -// Why is there a flipped horizontal card? -// Max width on contents for horizontal cards? Doesn't appear to be one that includes the preview because the preview can have any ratio and that -// causes the width grow. -// (Max) height on cards? Maybe that makes more sense. - -const onlyPreview = ':not(:has([data-slot=content])):not(:has([data-slot=preview]))'; - -let card = style({ - display: 'flex', - flexDirection: 'row', - position: 'relative', - borderRadius, - backgroundColor: { - default: lightDark('transparent-white-300', 'transparent-black-300'), - forcedColors: 'ButtonFace' - }, - // TODO: waiting for design to investigate thumbnail card/attachement error state - boxShadow: { - default: `[inset 0 0 0 1px light-dark(${color('transparent-black-300')}, ${color('transparent-white-300')})]`, - isInvalid: `[inset 0 0 0 1px ${color('negative-900')}]` - }, - forcedColorAdjust: 'none', - transition: 'default', - fontFamily: 'sans', - textDecoration: 'none', - overflow: { - default: 'clip', - variant: { - quiet: 'visible' - } - }, - contain: 'layout', - disableTapHighlight: true, - userSelect: { - isCardView: 'none' - }, - cursor: { - isLink: 'pointer' - }, - height: { - default: { - size: { - XS: 160, - S: 180, - M: 200, - L: 220, - XL: 240 - } - }, - isBasic: { - default: 68, - size: { - XS: 52, - S: 60, - M: 68, - L: 76, - XL: 80 - } - }, - isCardView: 'full' - }, - width: { - default: 'full', - [onlyPreview]: 'auto' - }, - aspectRatio: { - [onlyPreview]: '1/1' - }, - '--card-spacing': { - type: 'paddingTop', - value: { - density: { - compact: { - size: { - XS: '[6px]', - S: 8, - M: 12, - L: 16, - XL: 20 - } - }, - regular: { - size: { - XS: 8, - S: 12, - M: 16, - L: 20, - XL: 24 - } - }, - spacious: { - size: { - XS: 12, - S: 16, - M: 20, - L: 24, - XL: 28 - } - } - }, - [onlyPreview]: 0 - } - }, - alignItems: { - isBasic: 'center' - }, - '--card-padding-y': { - type: 'paddingTop', - value: { - default: '--card-spacing' - } - }, - '--card-padding-x': { - type: 'paddingStart', - value: { - default: '--card-spacing' - } - }, - paddingY: '--card-padding-y', - paddingX: '--card-padding-x', - boxSizing: 'border-box', - ...focusRing(), - outlineStyle: { - default: 'none', - isFocusVisible: 'solid', - // Focus ring moves to preview when quiet. - variant: { - quiet: 'none' - } - }, - '--basic-thumb-size': { - type: 'height', - value: { - default: 68, - size: { - XS: 24, - S: 26, - M: 32, - L: 36, - XL: 40 - }, - [onlyPreview]: 'full' - } - } -}); - -let selectionIndicator = style({ - position: 'absolute', - inset: 0, - zIndex: 2, - borderRadius, - pointerEvents: 'none', - borderWidth: 2, - borderStyle: 'solid', - borderColor: 'gray-1000', - transition: 'default', - opacity: { - default: 0, - isSelected: 1 - }, - // Quiet cards with no checkbox have an extra inner stroke - // to distinguish the selection indicator from the preview. - outlineColor: lightDark('transparent-white-600', 'transparent-black-600'), - outlineOffset: -4, - outlineStyle: { - default: 'none', - isStrokeInner: 'solid' - }, - outlineWidth: 2 -}); - -let preview = style({ - position: 'relative', - transition: 'default', - overflow: 'clip', - marginY: 'calc(var(--card-padding-y) * -1)', - marginStart: 'calc(var(--card-padding-x) * -1)', - marginEnd: { - ':last-child': 'calc(var(--card-padding-x) * -1)' - }, - borderRadius: { - isQuiet: borderRadius - }, - boxShadow: { - isQuiet: { - isHovered: 'elevated', - isFocusVisible: 'elevated', - isSelected: 'elevated' - } - }, - ...focusRing(), - outlineStyle: { - default: 'none', - isQuiet: { - isFocusVisible: 'solid' - } - } -}); - -const image = style({ - height: 'full', - aspectRatio: '1/1', - objectFit: 'cover', - userSelect: 'none', - pointerEvents: 'none' -}); - -let title = style<{size: 'XS' | 'S' | 'M' | 'L' | 'XL'; isBasic?: boolean}>({ - font: 'title', - fontSize: { - size: { - XS: 'title-xs', - S: 'title-xs', - M: 'title-sm', - L: 'title', - XL: 'title-lg' - } - }, - lineClamp: { - default: 3, - isBasic: 1 - }, - gridArea: 'title' -}); - -let description = style<{size: 'XS' | 'S' | 'M' | 'L' | 'XL'; isBasic?: boolean}>({ - font: 'body', - fontSize: { - size: { - XS: 'body-2xs', - S: 'body-2xs', - M: 'body-xs', - L: 'body-sm', - XL: 'body' - } - }, - lineClamp: { - default: 3, - isBasic: 1 - }, - gridArea: 'description' -}); - -let content = style({ - display: 'grid', - // By default, all elements are displayed in a stack. - // If an action menu is present, place it next to the title. - gridTemplateColumns: { - default: ['minmax(0, 1fr)'], - ':has([data-slot=menu])': ['minmax(0, 1fr)', 'auto'] - }, - gridTemplateAreas: { - default: ['title', 'description'], - ':has([data-slot=menu])': ['title menu', 'description description'] - }, - columnGap: 4, - flexGrow: 1, - minWidth: 0, - alignItems: 'baseline', - alignContent: 'start', - rowGap: { - size: { - XS: 4, - S: 4, - M: space(6), - L: space(6), - XL: 8 - } - }, - paddingStart: { - default: '--card-spacing', - ':first-child': 0 - }, - paddingEnd: { - default: 'calc(var(--card-spacing) * 1.5 / 2)', - ':last-child': 0 - } -}); - -let actionMenu = style({ - gridArea: 'menu', - // Don't cause the row to expand, preserve gap between title and description text. - // Would use -100% here but it doesn't work in Firefox. - marginY: 'calc(-1 * self(height))' -}); - -let footer = style({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - gap: 8 -}); - -export const InternalCardViewContext = createContext({ - ElementType: 'div' as 'div' | typeof GridListItem, - layout: 'grid' as 'grid' | 'waterfall' -}); - -interface InternalCardContextValue { - isQuiet: boolean; - size: 'XS' | 'S' | 'M' | 'L' | 'XL'; - isSelected: boolean; - isHovered: boolean; - isFocusVisible: boolean; - isPressed: boolean; - isCheckboxSelection: boolean; -} - -const InternalCardContext = createContext({ - isQuiet: false, - size: 'M', - isSelected: false, - isHovered: false, - isFocusVisible: false, - isPressed: false, - isCheckboxSelection: true -}); - -const actionButtonSize = { - XS: 'XS', - S: 'XS', - M: 'S', - L: 'M', - XL: 'L' -} as const; - -const Card = forwardRef(function Card( - props: Omit & { - isBasic?: boolean; - isInvalid?: boolean; - variant?: 'primary' | 'secondary' | 'tertiary' | 'quiet'; - }, - ref: DOMRef -) { - let {ElementType} = useContext(InternalCardViewContext); - let domRef = useDOMRef(ref); - let { - isBasic = false, - isInvalid = false, - density = 'regular', - size = 'M', - variant = 'primary', - styles, - id, - ...otherProps - } = props; - let isQuiet = variant === 'quiet'; - let isSkeleton = useIsSkeleton(); - let children = ( - - - {typeof props.children === 'function' ? props.children({size}) : props.children} - - - ); - - // oxlint-disable-next-line react/react-compiler - let press = pressScale(domRef); - if (ElementType === 'div' && !isSkeleton && props.href) { - // Standalone Card that has an href should be rendered as a Link. - // NOTE: In this case, the card must not contain interactive elements. - return ( - - mergeStyles( - card({ - ...renderProps, - size, - density, - variant, - isBasic, - isInvalid, - isCardView: false, - isLink: true - }), - styles - ) - } - style={renderProps => - // Only the preview in quiet cards scales down on press - variant === 'quiet' ? undefined : press(renderProps) - }> - {renderProps => ( - - {children} - - )} - - ); - } - - if (ElementType === 'div' || isSkeleton) { - return ( -
- - {children} - -
- ); - } - - return ( - - mergeStyles( - card({ - ...renderProps, - isCardView: true, - isLink: !!props.href, - size, - density, - variant, - isBasic, - isInvalid - }), - styles - ) - } - style={renderProps => - // Only the preview in quiet cards scales down on press - variant === 'quiet' ? undefined : press(renderProps) - }> - {({selectionMode, selectionBehavior, isHovered, isFocusVisible, isSelected, isPressed}) => ( - - {/* Selection indicator and checkbox move inside the preview for quiet cards */} - {!isQuiet && } - {!isQuiet && selectionMode !== 'none' && selectionBehavior === 'toggle' && ( - - )} - {/* this makes the :first-child selector work even with the checkbox */} -
{children}
-
- )} -
- ); -}); - -function SelectionIndicator() { - let {size, isSelected, isQuiet, isCheckboxSelection} = useContext(InternalCardContext); - return ( -
- ); -} - -function CardCheckbox() { - let {size} = useContext(InternalCardContext); - return ( -
- -
- ); -} - -export interface CardPreviewProps extends DOMProps { - children: ReactNode; - /** - * Spectrum-defined styles, returned by the `style()` macro. - */ - styles?: StyleString; -} - -// TODO: this should be the same component as the one in @react-spectrum/s2/Card -export const CardPreview = forwardRef(function CardPreview( - props: CardPreviewProps, - ref: DOMRef -) { - let {size, isQuiet, isHovered, isFocusVisible, isSelected, isPressed, isCheckboxSelection} = - useContext(InternalCardContext); - let domRef = useDOMRef(ref); - // oxlint-disable react/react-compiler - return ( -
- {isQuiet && } - {isQuiet && isCheckboxSelection && } -
- {props.children} -
-
- ); - // oxlint-enable react/react-compiler -}); - -const collection = style({ - display: 'grid', - gridTemplateColumns: 'repeat(3, 1fr)', - gap: { - default: 4, - size: { - XS: 2, - S: 2 - } - } -}); - -const collectionImage = style({ - width: 'full', - gridColumnEnd: { - ':nth-last-child(4):first-child': 'span 3' - }, - objectFit: 'cover', - pointerEvents: 'none', - userSelect: 'none' -}); - -export const CollectionCardPreview = forwardRef(function CollectionCardPreview( - props: CardPreviewProps, - ref: DOMRef -) { - let {size} = useContext(InternalCardContext)!; - return ( - -
- - {props.children} - -
-
- ); -}); - -const buttonSize = { - XS: 'S', - S: 'S', - M: 'M', - L: 'L', - XL: 'XL' -} as const; - -export const HorizontalCard = forwardRef(function HorizontalCard( - props: HorizontalCardProps, - ref: DOMRef -) { - let {size = 'M'} = props; - return ( - - {composeRenderProps(props.children, children => ( - - {children} - - ))} - - ); -}); - -export const BasicHorizontalCard = forwardRef(function BasicHorizontalCard( - props: BasicCardProps, - ref: DOMRef -) { - let {size = 'M'} = props; - return ( - - {composeRenderProps(props.children, children => ( - - {children} - - ))} - - ); -});