-
Notifications
You must be signed in to change notification settings - Fork 7
feat(AssetInput): add AssetInput component #2726
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
20c0a0e
54deae7
c2885fc
326fbf5
3575d9f
fee7646
3f8d1ee
79ac3bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@frontify/fondue-components": patch | ||
| --- | ||
|
|
||
| feat(AssetInput): add AssetInput component |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "category": "input", | ||
| "description": "A compound input for selecting, uploading and displaying assets.", | ||
| "instructions": "Use AssetInput.Placeholder for the empty state, composing AssetInput.UploadInput and/or AssetInput.BrowseInput. Use AssetInput.Root for the filled state, composing AssetInput.Preview (with one or more AssetInput.PreviewImage/AssetInput.PreviewIcon/AssetInput.PreviewLoading), AssetInput.Title, and AssetInput.Metadata wrapping one or more AssetInput.MetadataItem entries. Control the open state with isOpen and onPress on AssetInput.Root, typically to toggle an attached Dropdown for asset operations.", | ||
| "name": "AssetInput", | ||
| "relatedComponents": [], | ||
| "storyFilePaths": ["src/components/AssetInput/AssetInput.stories.tsx"], | ||
| "tags": ["asset", "upload", "file", "input", "form"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,251 @@ | ||
| /* (c) Copyright Frontify Ltd., all rights reserved. */ | ||
|
|
||
| import { IconArrowCircleUp, IconImageStack, IconMusicNote, IconTrashBin } from '@frontify/fondue-icons'; | ||
| import { type Meta, type StoryObj } from '@storybook/react-vite'; | ||
| import { useState } from 'react'; | ||
| import { action } from 'storybook/actions'; | ||
|
|
||
| import { Dropdown } from '../Dropdown/Dropdown'; | ||
| import { Flex } from '../Flex/Flex'; | ||
|
|
||
| import { AssetInput, type AssetInputRootProps } from './AssetInput'; | ||
|
|
||
| type StoryArgs = AssetInputRootProps & { acceptFileType: string }; | ||
|
|
||
| const meta = { | ||
| title: 'Components/AssetInput', | ||
| tags: ['autodocs'], | ||
| component: AssetInput.Root, | ||
| subcomponents: { | ||
| Placeholder: AssetInput.Placeholder, | ||
| UploadInput: AssetInput.UploadInput, | ||
| BrowseInput: AssetInput.BrowseInput, | ||
| Preview: AssetInput.Preview, | ||
| PreviewImage: AssetInput.PreviewImage, | ||
| PreviewIcon: AssetInput.PreviewIcon, | ||
| PreviewLoading: AssetInput.PreviewLoading, | ||
| Title: AssetInput.Title, | ||
| Metadata: AssetInput.Metadata, | ||
| MetadataItem: AssetInput.MetadataItem, | ||
| }, | ||
| argTypes: { | ||
| acceptFileType: { | ||
| control: { type: 'text' }, | ||
| description: 'File types accepted by the upload input.', | ||
| }, | ||
| orientation: { | ||
| control: { type: 'radio' }, | ||
| options: ['horizontal', 'vertical'], | ||
| description: 'The orientation of the asset input.', | ||
| }, | ||
| }, | ||
| args: { | ||
| acceptFileType: 'image/*', | ||
| orientation: 'horizontal', | ||
| }, | ||
| parameters: { | ||
| controls: { include: ['acceptFileType', 'orientation'] }, | ||
| }, | ||
| } satisfies Meta<StoryArgs>; | ||
| export default meta; | ||
|
|
||
| type Story = StoryObj<StoryArgs>; | ||
|
|
||
| export const Placeholder: Story = { | ||
| name: 'Placeholder', | ||
| parameters: { | ||
| controls: { include: ['acceptFileType'] }, | ||
| }, | ||
| render: ({ acceptFileType }) => ( | ||
| <AssetInput.Placeholder> | ||
| <AssetInput.UploadInput acceptFileType={acceptFileType} onSelect={action('onFileChange')} /> | ||
| <AssetInput.BrowseInput onBrowse={action('onBrowse')} /> | ||
| </AssetInput.Placeholder> | ||
| ), | ||
| }; | ||
|
|
||
| export const PlaceholderOnlyUpload: Story = { | ||
| name: 'Placeholder - Upload Only', | ||
| parameters: { | ||
| controls: { include: ['acceptFileType'] }, | ||
| }, | ||
| render: ({ acceptFileType }) => ( | ||
| <AssetInput.Placeholder> | ||
| <AssetInput.UploadInput acceptFileType={acceptFileType} onSelect={action('onFileChange')} /> | ||
| </AssetInput.Placeholder> | ||
| ), | ||
| }; | ||
|
|
||
| export const PlaceholderOnlyBrowse: Story = { | ||
| name: 'Placeholder - Browse Only', | ||
| parameters: { | ||
| controls: { include: [] }, | ||
| }, | ||
| render: () => ( | ||
| <AssetInput.Placeholder> | ||
| <AssetInput.BrowseInput onBrowse={action('onBrowse')} /> | ||
| </AssetInput.Placeholder> | ||
| ), | ||
| }; | ||
|
|
||
| export const AssetInputRoot: Story = { | ||
| name: 'Input - Single - Image', | ||
| parameters: { | ||
| controls: { include: ['orientation'] }, | ||
| }, | ||
| render: ({ orientation }) => { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
|
Check warning on line 97 in packages/components/src/components/AssetInput/AssetInput.stories.tsx
|
||
| return ( | ||
| <AssetInput.Root orientation={orientation} isOpen={isOpen} onPress={() => setIsOpen(!isOpen)}> | ||
| <AssetInput.Preview> | ||
| <AssetInput.PreviewImage src="https://picsum.photos/100/150" /> | ||
| </AssetInput.Preview> | ||
| <AssetInput.Title>foo1</AssetInput.Title> | ||
| <AssetInput.Metadata> | ||
| <AssetInput.MetadataItem> | ||
| <Flex align="center" gap={0.5}> | ||
| <IconArrowCircleUp size="16" /> | ||
| Uploaded | ||
| </Flex> | ||
| </AssetInput.MetadataItem> | ||
| <AssetInput.MetadataItem>JPG</AssetInput.MetadataItem> | ||
| <AssetInput.MetadataItem>2000 bytes</AssetInput.MetadataItem> | ||
| </AssetInput.Metadata> | ||
| </AssetInput.Root> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const AssetInputRootIcon: Story = { | ||
| name: 'Input - Single - Icon', | ||
| parameters: { | ||
| controls: { include: ['orientation'] }, | ||
| }, | ||
| render: ({ orientation }) => { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
|
Check warning on line 125 in packages/components/src/components/AssetInput/AssetInput.stories.tsx
|
||
| return ( | ||
| <AssetInput.Root orientation={orientation} isOpen={isOpen} onPress={() => setIsOpen(!isOpen)}> | ||
| <AssetInput.Preview> | ||
| <AssetInput.PreviewIcon> | ||
| <IconMusicNote /> | ||
| </AssetInput.PreviewIcon> | ||
| </AssetInput.Preview> | ||
| <AssetInput.Title>foo1</AssetInput.Title> | ||
| <AssetInput.Metadata> | ||
| <AssetInput.MetadataItem> | ||
| <Flex align="center" gap={0.5}> | ||
| <IconArrowCircleUp size="16" /> | ||
| Uploaded | ||
| </Flex> | ||
| </AssetInput.MetadataItem> | ||
| <AssetInput.MetadataItem>JPG</AssetInput.MetadataItem> | ||
| <AssetInput.MetadataItem>2000 bytes</AssetInput.MetadataItem> | ||
| </AssetInput.Metadata> | ||
| </AssetInput.Root> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const AssetInputRootLoading: Story = { | ||
| name: 'Input - Single - Loading', | ||
| parameters: { | ||
| controls: { include: ['orientation'] }, | ||
| }, | ||
| render: ({ orientation }) => { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
|
Check warning on line 155 in packages/components/src/components/AssetInput/AssetInput.stories.tsx
|
||
| return ( | ||
| <AssetInput.Root orientation={orientation} isOpen={isOpen} onPress={() => setIsOpen(!isOpen)}> | ||
| <AssetInput.Preview> | ||
| <AssetInput.PreviewLoading /> | ||
| </AssetInput.Preview> | ||
| <AssetInput.Title>foo1</AssetInput.Title> | ||
| <AssetInput.Metadata> | ||
| <AssetInput.MetadataItem> | ||
| <Flex align="center" gap={0.5}> | ||
| <IconArrowCircleUp size="16" /> | ||
| Uploaded | ||
| </Flex> | ||
| </AssetInput.MetadataItem> | ||
| <AssetInput.MetadataItem>JPG</AssetInput.MetadataItem> | ||
| <AssetInput.MetadataItem>2000 bytes</AssetInput.MetadataItem> | ||
| </AssetInput.Metadata> | ||
| </AssetInput.Root> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const AssetInputRootMultipleImages: Story = { | ||
| name: 'Input - Multiple - Mixed', | ||
| parameters: { | ||
| controls: { include: ['orientation'] }, | ||
| }, | ||
| render: ({ orientation }) => { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
|
Check warning on line 183 in packages/components/src/components/AssetInput/AssetInput.stories.tsx
|
||
| return ( | ||
| <AssetInput.Root orientation={orientation} isOpen={isOpen} onPress={() => setIsOpen(!isOpen)}> | ||
| <AssetInput.Preview> | ||
| <AssetInput.PreviewImage src="https://picsum.photos/100/150" /> | ||
| <AssetInput.PreviewImage src="https://picsum.photos/101/150" /> | ||
| <AssetInput.PreviewIcon> | ||
| <IconMusicNote /> | ||
| </AssetInput.PreviewIcon> | ||
| <AssetInput.PreviewLoading /> | ||
| </AssetInput.Preview> | ||
| <AssetInput.Title>3 assets</AssetInput.Title> | ||
| <AssetInput.Metadata> | ||
| <AssetInput.MetadataItem>2 locations</AssetInput.MetadataItem> | ||
| </AssetInput.Metadata> | ||
| </AssetInput.Root> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const AssetInputAsDropdownTrigger: Story = { | ||
| name: 'Input as Dropdown Trigger', | ||
| parameters: { | ||
| controls: { include: ['orientation'] }, | ||
| }, | ||
| render: ({ orientation }) => { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
|
Check warning on line 209 in packages/components/src/components/AssetInput/AssetInput.stories.tsx
|
||
| return ( | ||
| <Dropdown.Root open={isOpen} onOpenChange={setIsOpen}> | ||
| <Dropdown.Trigger asChild> | ||
| <AssetInput.Root orientation={orientation} isOpen={isOpen}> | ||
| <AssetInput.Preview> | ||
| <AssetInput.PreviewImage src="https://picsum.photos/100/150" /> | ||
| </AssetInput.Preview> | ||
| <AssetInput.Title>foo1</AssetInput.Title> | ||
| <AssetInput.Metadata> | ||
| <AssetInput.MetadataItem> | ||
| <Flex align="center" gap={0.5}> | ||
| <IconArrowCircleUp size="16" /> | ||
| Uploaded | ||
| </Flex> | ||
| </AssetInput.MetadataItem> | ||
| <AssetInput.MetadataItem>JPG</AssetInput.MetadataItem> | ||
| <AssetInput.MetadataItem>2000 bytes</AssetInput.MetadataItem> | ||
| </AssetInput.Metadata> | ||
| </AssetInput.Root> | ||
| </Dropdown.Trigger> | ||
| <Dropdown.Content align="start"> | ||
| <Dropdown.Group> | ||
| <Dropdown.Item onSelect={action('replace')}> | ||
| <IconImageStack size={16} /> | ||
| Replace with browse | ||
| </Dropdown.Item> | ||
| <Dropdown.Item onSelect={action('replace')}> | ||
| <IconArrowCircleUp size={16} /> | ||
| Replace with upload | ||
| </Dropdown.Item> | ||
| </Dropdown.Group> | ||
| <Dropdown.Group> | ||
| <Dropdown.Item emphasis="danger" onSelect={action('remove')}> | ||
| <IconTrashBin size={16} /> | ||
| Delete | ||
| </Dropdown.Item> | ||
| </Dropdown.Group> | ||
| </Dropdown.Content> | ||
| </Dropdown.Root> | ||
| ); | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| /* (c) Copyright Frontify Ltd., all rights reserved. */ | ||
|
|
||
| import { forwardRef } from 'react'; | ||
|
|
||
| import { AssetInputBrowseInput } from './AssetInputBrowseInput'; | ||
| import { AssetInputMetadata } from './AssetInputMetadata'; | ||
| import { AssetInputMetadataItem } from './AssetInputMetadataItem'; | ||
| import { AssetInputPlaceholder } from './AssetInputPlaceholder'; | ||
| import { AssetInputPreview } from './AssetInputPreview'; | ||
| import { AssetInputPreviewIcon } from './AssetInputPreviewIcon'; | ||
| import { AssetInputPreviewImage } from './AssetInputPreviewImage'; | ||
| import { AssetInputPreviewLoading } from './AssetInputPreviewLoading'; | ||
| import { AssetInputRoot, type AssetInputRootProps } from './AssetInputRoot'; | ||
| import { AssetInputTitle } from './AssetInputTitle'; | ||
| import { AssetInputUploadInput } from './AssetInputUploadInput'; | ||
|
|
||
| export type { AssetInputRootProps }; | ||
|
|
||
| export const AssetInput = { | ||
| Placeholder: AssetInputPlaceholder, | ||
| UploadInput: AssetInputUploadInput, | ||
| BrowseInput: AssetInputBrowseInput, | ||
| Root: forwardRef<HTMLButtonElement, AssetInputRootProps>(AssetInputRoot), | ||
| Preview: AssetInputPreview, | ||
| PreviewImage: AssetInputPreviewImage, | ||
| PreviewIcon: AssetInputPreviewIcon, | ||
| Title: AssetInputTitle, | ||
| Metadata: AssetInputMetadata, | ||
| MetadataItem: AssetInputMetadataItem, | ||
| PreviewLoading: AssetInputPreviewLoading, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| /* (c) Copyright Frontify Ltd., all rights reserved. */ | ||
|
|
||
| import { IconImageStack } from '@frontify/fondue-icons'; | ||
|
|
||
| import { useTranslation } from '#/hooks/useTranslation'; | ||
|
|
||
| import { Button } from '../Button/Button'; | ||
|
|
||
| export type AssetInputBrowseInputProps = { | ||
| onBrowse: () => void; | ||
| }; | ||
|
|
||
| export const AssetInputBrowseInput = ({ onBrowse }: AssetInputBrowseInputProps) => { | ||
| const { t } = useTranslation(); | ||
|
|
||
| return ( | ||
| <Button onPress={onBrowse} emphasis="weak" hugWidth={false} data-asset-input-action="browse"> | ||
| <IconImageStack size={20} /> | ||
| {t('AssetInput_browse')} | ||
| </Button> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| /* (c) Copyright Frontify Ltd., all rights reserved. */ | ||
|
|
||
| import { type ReactNode } from 'react'; | ||
|
|
||
| import styles from './styles/asset-input.module.scss'; | ||
|
|
||
| export type AssetInputMetadataProps = { | ||
| children: ReactNode; | ||
| }; | ||
|
|
||
| export const AssetInputMetadata = ({ children }: AssetInputMetadataProps) => { | ||
| return <div className={styles.metadata}>{children}</div>; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| /* (c) Copyright Frontify Ltd., all rights reserved. */ | ||
|
|
||
| import { type ReactNode } from 'react'; | ||
|
|
||
| import styles from './styles/asset-input.module.scss'; | ||
|
|
||
| export type AssetInputMetadataItemProps = { | ||
| children: ReactNode; | ||
| }; | ||
|
|
||
| export const AssetInputMetadataItem = ({ children }: AssetInputMetadataItemProps) => { | ||
| return <span className={styles.metadataItem}>{children}</span>; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /* (c) Copyright Frontify Ltd., all rights reserved. */ | ||
|
|
||
| import { type ReactElement } from 'react'; | ||
|
|
||
| import { Flex } from '../Flex/Flex'; | ||
|
|
||
| import { type AssetInputBrowseInputProps } from './AssetInputBrowseInput'; | ||
| import { type AssetInputUploadInputProps } from './AssetInputUploadInput'; | ||
| import styles from './styles/asset-input.module.scss'; | ||
|
|
||
| type AssetInputAction = ReactElement<AssetInputUploadInputProps | AssetInputBrowseInputProps>; | ||
|
|
||
| export type AssetInputPlaceholderProps = { | ||
| children: AssetInputAction | [AssetInputAction, AssetInputAction]; | ||
| }; | ||
|
|
||
| export const AssetInputPlaceholder = ({ children }: AssetInputPlaceholderProps) => { | ||
| return ( | ||
| <div className={styles.placeholder}> | ||
| <Flex gap={2} height="100%" p={3}> | ||
| {children} | ||
| </Flex> | ||
| </div> | ||
| ); | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.