# ImageAndFileDisplay
Package: @thinker-core/mantine-core
Import: import { ImageAndFileDisplay } from '@thinker-core/mantine-core';

## Usage

`ImageAndFileDisplay` is a component for displaying images and files with labels, descriptions, and optional remove button. It supports both horizontal and vertical orientations and can automatically show file sizes for file types.

```tsx
import { ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <ImageAndFileDisplay type="image" withRemoveButton={false} removeButtonPosition="right" size="md" radius="xs" orientation="horizontal" label="Input label" withAsterisk={false} scaleWrapper={false} description="Input description" error="" overlay="" />
  );
}
```


## With label

`ImageAndFileDisplay` supports labels that can be positioned alongside the display. In horizontal orientation, the label appears to the right of the display. In vertical orientation, it appears below the display.

```tsx
import { ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <Flex direction="row" gap="xl" justify="center">
      <ImageAndFileDisplay
        my="xs"
        label="Label on the right"
        description="Description on the right"
        src=""
        type="image"
      />
      <ImageAndFileDisplay
        my="xs"
        label="Label on the bottom"
        description="Description on the bottom"
        orientation="vertical"
        src=""
        type="image"
      />
    </Flex>
  );
}
```


## Display only

`ImageAndFileDisplay` can be used without labels or descriptions for a minimal display.

```tsx
import { Group, ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <Group gap="md" justify="center">
      <ImageAndFileDisplay
        src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-7.png"
        type="image"
        orientation="vertical"
      />
      <ImageAndFileDisplay
        src={new File(['testing'], 'test.pdf', { type: 'application/pdf' })}
        type="file"
        orientation="vertical"
      />
    </Group>
  );
}
```


## Overlay

Add an overlay to the display using the `overlay` prop.

```tsx
import { ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <ImageAndFileDisplay
      src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-7.png"
      type="image"
      orientation="vertical"
      overlay="99+"
    />
  );
}
```


## Fullwidth (horizontal only)

When `fullWidth` is set to `true` (default), the component takes the full width of its container in horizontal orientation.

```tsx
import {ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <ImageAndFileDisplay
      label="Perhaps some label that are really long and take up a lot of space"
      description="This is a description that is really long and take up a lot of space"
      src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-7.png"
      type="image"
      orientation="horizontal"
      fullWidth
      withRemoveButton
    />
  );
}
```


## Vertical orientation

Set `orientation="vertical"` to stack the label and description below the display instead of beside it.

```tsx
import { Group, ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <Group gap="md" justify="center">
      <ImageAndFileDisplay
        my="xs"
        label="Label on the bottom"
        description="Description on the bottom"
        src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-7.png"
        type="image"
        orientation="vertical"
        removeButtonPosition="top-right"
      />
      <ImageAndFileDisplay
        my="xs"
        label="Label on the bottom"
        description="Description on the bottom"
        src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-7.png"
        type="image"
        orientation="vertical"
        removeButtonPosition="top-right"
      />
      <ImageAndFileDisplay
        my="xs"
        label="Label on the bottom"
        description="Description on the bottom"
        src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-7.png"
        type="image"
        orientation="vertical"
        removeButtonPosition="top-right"
      />
    </Group>
  );
}
```


## File format

`ImageAndFileDisplay` can display file icons for different file formats. You can customize the file icon by providing a custom `getFileIcon` function. Currently supports the following file formats:

* `pdf`
* `xls`
* `xlsx`
* `doc`
* `docx`
* `txt`
* `tiff`
* `gif`

```tsx
// Demo.tsx
import { SimpleGrid, ImageAndFileDisplay } from '@thinker-core/mantine-core';

function Demo() {
  return (
    <Group
      gap="xl"
      justify="center"
      wrap="wrap"
    >
      {data.map((item) => (
        <ImageAndFileDisplay
          key={item.name}
          src={new File(['testing'], item.name, { type: item.format })}
          type="file"
          orientation="vertical"
        />
      ))}
    </Group>
  );
}

// data.ts
export const data = [
  {
    name: 'test.pdf',
    format: MIME_TYPES.pdf,
  },
  {
    name: 'test.xls',
    format: MIME_TYPES.xls,
  },
  {
    name: 'test.xlsx',
    format: MIME_TYPES.xlsx,
  },
  {
    name: 'test.doc',
    format: MIME_TYPES.doc,
  },
  {
    name: 'test.docx',
    format: MIME_TYPES.docx,
  },
  {
    name: 'test.txt',
    format: 'text/plain',
  },
  {
    name: 'test.tiff',
    format: 'image/tiff',
  },
  {
    name: 'test.gif',
    format: MIME_TYPES.gif,
  },
];
```


## Design pattern (vertical)

Common design pattern is to display a list of files in a vertical orientation with a limited number of files displayed at a time using `FileButton` and a `Collapse` component.

```tsx
// Demo.tsx
import { useMemo, useState } from 'react';
import {
  Box,
  Button,
  Collapse,
  FileButton,
  Group,
  ImageAndFileDisplay,
  Input,
  Stack,
} from '@thinker-core/mantine-core';

type MockData = {
  name: string;
  format?: string;
  type: 'image' | 'file';
  src: string | File;
  description?: string;
};

function Demo() {
  const [files, setFiles] = useState<MockData[]>([]);
  const [opened, setOpened] = useState(false);
  const top4 = useMemo(() => files.slice(0, 4), [files]);
  const remaining = useMemo(() => files.slice(4), [files]);

  const handleFiles = (files: File[]) => {
    const newFiles: MockData[] = [];

    // simulate image upload if the file is an image
    files.forEach((file) => {
      if (file.type.startsWith('image/')) {
        // get the src
        const src = URL.createObjectURL(file);

        // replace the file with the src
        newFiles.push({
          name: file.name,
          type: 'image',
          src,
          description:
            file.size > 1024 * 1024
              ? (file.size / 1024 / 1024).toFixed(2) + ' MB'
              : (file.size / 1024).toFixed(2) + ' KB',
        });
        return;
      }

      // simulate
      newFiles.push({
        name: file.name,
        type: 'file',
        format: file.type,
        src: file,
      });
    });

    setFiles((prev) => [...prev, ...newFiles]);
  };

  return (
    <Stack p="2xl" bg="ocean.0" style={{ borderRadius: 'var(--mantine-radius-md)' }}>
      <Group justify="space-between" mb="2xl">
        <Box>
          <Input.Label size="sm" required>
            Label
          </Input.Label>
          <Input.Description size="sm">Description</Input.Description>
        </Box>
        <Group gap="md">
          <Button variant="subtle" color="cherry" size="xs" onClick={() => setFiles([])}>
            Clear All
          </Button>
          <FileButton accept="image/*" onChange={handleFiles} multiple>
            {(props) => (
              <Button {...props} size="xs">
                Upload
              </Button>
            )}
          </FileButton>
        </Group>
      </Group>

      <Group gap="2xl" align="flex-start">
        {top4.map((file, index) => (
          <ImageAndFileDisplay
            key={file.name}
            label={(index === 3 && !opened) ? undefined : file.name}
            description={(index === 3 && !opened) ? undefined : file.description}
            src={file.src}
            type={file.type}
            orientation="vertical"
            withRemoveButton={!(index === 3 && !opened)}
            removeButtonPosition="top-right"
            overlay={index === 3 && !opened ? (files.length - top4.length) + '+' : undefined} // display remaining files count
            onClick={index === 3 && !opened ? () => setOpened(true) : undefined}
            onRemove={() => {
              setFiles(files.filter((f) => f.name !== file.name));
            }}
          />
        ))}
      </Group>
      <Collapse expanded={opened}>
        <Group gap="2xl" pt="2xl">
          {remaining.map((file) => (
            <ImageAndFileDisplay
              key={file.name}
              label={file.name}
              description={file.description}
              src={file.src}
              type={file.type}
              orientation="vertical"
              withRemoveButton
              removeButtonPosition="top-right"
              onRemove={() => {
                setFiles(files.filter((f) => f.name !== file.name));
              }}
            />
          ))}
        </Group>
        <Button mt="2xl" variant="outline" size="xs" onClick={() => setOpened(false)} fullWidth>
          See less
        </Button>
      </Collapse>
    </Stack>
  );
}

// data.ts
export const data = [
  {
    name: 'test.pdf',
    format: MIME_TYPES.pdf,
    type: 'file',
    src: new File(['testing'], 'test.pdf', { type: MIME_TYPES.pdf }),
  },
  {
    name: 'test.xls',
    format: MIME_TYPES.xls,
    type: 'file',
    src: new File(['testing'], 'test.xls', { type: MIME_TYPES.xls }),
  },
  {
    name: 'test.png',
    type: 'image',
    src: mockImage,
  },
  {
    name: 'test.xlsx',
    format: MIME_TYPES.xlsx,
    type: 'file',
    src: new File(['testing'], 'test.xlsx', { type: MIME_TYPES.xlsx }),
  },
  {
    name: 'test.doc',
    format: MIME_TYPES.doc,
    type: 'file',
    src: new File(['testing'], 'test.doc', { type: MIME_TYPES.doc }),
  },
  {
    name: 'test.docx',
    format: MIME_TYPES.docx,
    type: 'file',
    src: new File(['testing'], 'test.docx', { type: MIME_TYPES.docx }),
  },
  {
    name: 'test.txt',
    format: 'text/plain',
    type: 'file',
    src: new File(['testing'], 'test.txt', { type: 'text/plain' }),
  },
  {
    name: 'test.tiff',
    format: 'image/tiff',
    type: 'file',
    src: new File(['testing'], 'test.tiff', { type: 'image/tiff' }),
  },
  {
    name: 'test.gif',
    format: MIME_TYPES.gif,
    type: 'file',
    src: new File(['testing'], 'test.gif', { type: MIME_TYPES.gif }),
  },
  {
    name: 'test2.png',
    type: 'image',
    src: mockImage,
  },
];
```


## Design pattern (horizontal)

Another common design pattern is to display a list of files in a horizontal orientation with a limited number of files displayed at a time using `FileButton` and a `Collapse` component.

```tsx
// Demo.tsx
import { useMemo, useState } from 'react';
import {
  Box,
  Button,
  Collapse,
  FileButton,
  Group,
  ImageAndFileDisplay,
  Input,
  Stack,
} from '@thinker-core/mantine-core';

type MockData = {
  name: string;
  format?: string;
  type: 'image' | 'file';
  src: string | File;
  description?: string;
};

function Demo() {
  const [files, setFiles] = useState<MockData[]>([]);
  const [opened, setOpened] = useState(false);
  const top4 = useMemo(() => files.slice(0, 4), [files]);
  const remaining = useMemo(() => files.slice(4), [files]);
  const isLengthGreaterThan4 = files.length > 4;

  const handleFiles = (files: File[]) => {
    const newFiles: MockData[] = [];

    // simulate image upload if the file is an image
    files.forEach((file) => {
      if (file.type.startsWith('image/')) {
        // get the src
        const src = URL.createObjectURL(file);

        // replace the file with the src
        newFiles.push({
          name: file.name,
          type: 'image',
          src,
          description:
            file.size > 1024 * 1024
              ? (file.size / 1024 / 1024).toFixed(2) + ' MB'
              : (file.size / 1024).toFixed(2) + ' KB',
        });
        return;
      }

      // simulate
      newFiles.push({
        name: file.name,
        type: 'file',
        format: file.type,
        src: file,
      });
    });

    setFiles((prev) => [...prev, ...newFiles]);
  };

  return (
    <Stack gap={0}>
      <Stack gap="md" mb="2xl">
        <Box>
          <Input.Label size="sm" required>
            Label
          </Input.Label>
          <Input.Description size="sm">Description</Input.Description>
        </Box>
        <Group gap="md">
          <FileButton accept="image/*" onChange={handleFiles} multiple>
            {(props) => (
              <Button {...props} size="xs" variant="outline">
                Upload
              </Button>
            )}
          </FileButton>
          <Button variant="subtle" color="cherry" size="xs" onClick={() => setFiles([])}>
            Clear All
          </Button>
        </Group>
        <Input.Error size="sm">Error</Input.Error>
      </Stack>

      <Stack gap="md">
        {top4.map((file) => (
          <ImageAndFileDisplay
            key={file.name}
            label={file.name}
            description={file.description}
            src={file.src}
            type={file.type}
            orientation="horizontal"
            withRemoveButton
            size="sm"
            onRemove={() => {
              setFiles(files.filter((f) => f.name !== file.name));
            }}
          />
        ))}
      </Stack>
      <Collapse expanded={opened}>
        <Stack gap="md" pt="md">
          {remaining.map((file) => (
            <ImageAndFileDisplay
              key={file.name}
              label={file.name}
              description={file.description}
              src={file.src}
              type={file.type}
              orientation="horizontal"
              withRemoveButton
              size="sm"
              onRemove={() => {
                setFiles(files.filter((f) => f.name !== file.name));
              }}
            />
          ))}
        </Stack>
      </Collapse>
      {(opened ? true : isLengthGreaterThan4) && (
        <Button variant="outline" size="xs" onClick={() => setOpened(!opened)} fullWidth mt="2xl">
          See {opened ? 'less' : 'more (' + (files.length - top4.length) + ')'}
        </Button>
      )}
    </Stack>
  );
}

// data.ts
export const data = [
  {
    name: 'test.pdf',
    format: MIME_TYPES.pdf,
    type: 'file',
    src: new File(['testing'], 'test.pdf', { type: MIME_TYPES.pdf }),
  },
  {
    name: 'test.xls',
    format: MIME_TYPES.xls,
    type: 'file',
    src: new File(['testing'], 'test.xls', { type: MIME_TYPES.xls }),
  },
  {
    name: 'test.png',
    type: 'image',
    src: mockImage,
  },
  {
    name: 'test.xlsx',
    format: MIME_TYPES.xlsx,
    type: 'file',
    src: new File(['testing'], 'test.xlsx', { type: MIME_TYPES.xlsx }),
  },
  {
    name: 'test.doc',
    format: MIME_TYPES.doc,
    type: 'file',
    src: new File(['testing'], 'test.doc', { type: MIME_TYPES.doc }),
  },
  {
    name: 'test.docx',
    format: MIME_TYPES.docx,
    type: 'file',
    src: new File(['testing'], 'test.docx', { type: MIME_TYPES.docx }),
  },
  {
    name: 'test.txt',
    format: 'text/plain',
    type: 'file',
    src: new File(['testing'], 'test.txt', { type: 'text/plain' }),
  },
  {
    name: 'test.tiff',
    format: 'image/tiff',
    type: 'file',
    src: new File(['testing'], 'test.tiff', { type: 'image/tiff' }),
  },
  {
    name: 'test.gif',
    format: MIME_TYPES.gif,
    type: 'file',
    src: new File(['testing'], 'test.gif', { type: MIME_TYPES.gif }),
  },
  {
    name: 'test2.png',
    type: 'image',
    src: mockImage,
  },
];
```



#### Props

**ImageAndFileDisplay props**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| color | MantineColor | - | Key of `theme.colors` or any valid CSS color value, by default value depends on color scheme |
| description | React.ReactNode | - | Description of the image or file. For file type it will show file size by default, can also be set to `true` to show file size |
| descriptionProps | Record<string, any> | - | Props passed down to the `Input.Description` component |
| error | React.ReactNode | - | Determines whether the pill should have error styles and `aria-invalid` attribute |
| errorProps | Record<string, any> | - | Props passed down to the `Input.Error` component |
| fullWidth | boolean | - | Full width of the display (horizontal only), `true` by default |
| getFileIcon | (file: File) => ReactNode | - | Get file icon |
| id | string | - | Static id used as base to generate `aria-` attributes, by default generates random id |
| imageProps | Omit<ImageProps, "src"> | - | Image component props |
| label | React.ReactNode | - | ImageAndFileDisplay label, visible only when `orientation` is `horizontal` |
| labelProps | Record<string, any> | - | Props passed down to the `Input.Label` component |
| onRemove | () => void | - | Called when the remove button is clicked |
| orientation | "horizontal" \| "vertical" | - | Controls orientation, `'horizontal'` by default |
| overlay | React.ReactNode | - | Overlay content |
| radius | MantineRadius \| number | - | Key of `theme.radius` or any valid CSS value to set border-radius. Numbers are converted to rem. `'full'` by default. |
| removeButtonPosition | "right" \| "top-right" | - | Controls the position of the remove button, `'right'` by default |
| removeButtonProps | ActionIconProps & Omit<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> | - | Props passed down to the remove button |
| scaleWrapper | boolean | - | Scale label, description and error text to the size of the display |
| size | MantineSize \| number | - | Controls width/height of the display, `'md'` by default |
| src | string \| File | - | Source of the image or file |
| ts | StyleProp<"h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "d2" \| "d1" \| "d3" \| "d4" \| "d5" \| "d6" \| "d7" \| "d8" \| "d9" \| "d10" \| "h7" \| "h8" \| "h9" \| "h10" \| "t2" \| "t1" \| "t3" \| "t4" \| "t5" \| "t6" \| ... 63 more ... \| "lu10"> | - | TextStyle, custom text styles that will override fz, lh, fw, td |
| type | "image" \| "file" | - | Type of the image or file, `'image'` by default |
| withAsterisk | boolean | - | Determines whether the label should have required asterisk |
| withRemoveButton | boolean | - | Controls if the remove button is shown, `true` by default |
| wrapperOrder | ("label" \| "description" \| "error")[] | - | Controls order of the elements, `['label', 'description', 'error']` by default |


#### Styles API

ImageAndFileDisplay component supports Styles API. With Styles API, you can customize styles of any inner element. Follow the documentation to learn how to use CSS modules, CSS variables and inline styles to get full control over component styles.

**ImageAndFileDisplay selectors**

| Selector | Static selector | Description |
|----------|----------------|-------------|
| root | .mantine-ImageAndFileDisplay-root | Root element |
| display | .mantine-ImageAndFileDisplay-display | Display element |
| contentWrapper | .mantine-ImageAndFileDisplay-contentWrapper | Content wrapper |
| labelWrapper | .mantine-ImageAndFileDisplay-labelWrapper | Label wrapper |
| label | .mantine-ImageAndFileDisplay-label | Label |
| description | .mantine-ImageAndFileDisplay-description | Description |
| remove | .mantine-ImageAndFileDisplay-remove | Remove button |
| error | .mantine-ImageAndFileDisplay-error | Error |
| overlay | .mantine-ImageAndFileDisplay-overlay | Overlay |

**ImageAndFileDisplay CSS variables**

| Selector | Variable | Description |
|----------|----------|-------------|
| root | --ifd-background-color | Controls `background-color` property |
| root | --ifd-size | Controls `width` and `height` properties |
| root | --ifd-border-radius | Controls `border-radius` property |
| root | --ifd-label-fz | Controls `font-size` property of label |
| root | --ifd-description-fz | Controls `font-size` property of description |

**ImageAndFileDisplay data attributes**

| Selector | Attribute | Condition | Value |
|----------|-----------|-----------|-------|
| root | with-label | `label` prop is set | - |
| root | full-width | `fullWidth` prop is set | - |
| root | orientation | - | Value of `orientation` prop |
| display | type | - | Value of `type` prop |
