From e8826835b85b01428c6f8ba9514347fe6a0d7a0c Mon Sep 17 00:00:00 2001 From: kseniyakuzina Date: Thu, 26 Dec 2024 10:54:03 +0300 Subject: [PATCH] feat(Gallery): add Gallery component --- CODEOWNERS | 1 + src/components/FilePreview/FilePreview.tsx | 10 +- src/components/FilePreview/README.md | 17 +- src/components/Gallery/Gallery.scss | 214 ++++++++++++++ src/components/Gallery/Gallery.tsx | 142 ++++++++++ src/components/Gallery/GalleryItem.tsx | 13 + src/components/Gallery/README.md | 261 ++++++++++++++++++ .../Gallery/__stories__/Gallery.stories.tsx | 157 +++++++++++ .../Gallery/__stories__/mockData.ts | 62 +++++ src/components/Gallery/assets/arrow-left.svg | 1 + src/components/Gallery/assets/arrow-right.svg | 1 + .../components/FallbackText/FallbackText.scss | 19 ++ .../components/FallbackText/FallbackText.tsx | 17 ++ .../components/FallbackText/i18n/en.json | 3 + .../components/FallbackText/i18n/index.ts | 8 + .../components/FallbackText/i18n/ru.json | 3 + .../Gallery/components/FallbackText/index.ts | 1 + .../FullScreenAction/FullScreenAction.tsx | 45 +++ .../hooks/useFullScreenHotkeys.ts | 26 ++ .../actions/FullScreenAction/i18n/en.json | 4 + .../actions/FullScreenAction/i18n/index.ts | 8 + .../actions/FullScreenAction/i18n/ru.json | 4 + .../actions/FullScreenAction/index.ts | 1 + .../Gallery/components/actions/index.ts | 1 + .../views/DocumentView/DocumentView.scss | 10 + .../views/DocumentView/DocumentView.tsx | 18 ++ .../components/views/ImageView/ImageView.scss | 13 + .../components/views/ImageView/ImageView.tsx | 44 +++ .../components/views/VideoView/VideoView.scss | 12 + .../components/views/VideoView/VideoView.tsx | 57 ++++ .../components/views/VideoView/i18n/en.json | 4 + .../components/views/VideoView/i18n/index.ts | 8 + .../components/views/VideoView/i18n/ru.json | 4 + src/components/Gallery/hooks/useNavigation.ts | 58 ++++ src/components/Gallery/i18n/en.json | 3 + src/components/Gallery/i18n/index.ts | 8 + src/components/Gallery/i18n/ru.json | 3 + src/components/Gallery/index.ts | 7 + .../utils/getDefaultGalleryItemDocument.tsx | 27 ++ .../utils/getDefaultGalleryItemImage.tsx | 27 ++ .../utils/getDefaultGalleryItemVideo.tsx | 27 ++ .../Gallery/utils/getInvertedTheme.ts | 16 ++ src/components/index.ts | 1 + 43 files changed, 1355 insertions(+), 11 deletions(-) create mode 100644 src/components/Gallery/Gallery.scss create mode 100644 src/components/Gallery/Gallery.tsx create mode 100644 src/components/Gallery/GalleryItem.tsx create mode 100644 src/components/Gallery/README.md create mode 100644 src/components/Gallery/__stories__/Gallery.stories.tsx create mode 100644 src/components/Gallery/__stories__/mockData.ts create mode 100644 src/components/Gallery/assets/arrow-left.svg create mode 100644 src/components/Gallery/assets/arrow-right.svg create mode 100644 src/components/Gallery/components/FallbackText/FallbackText.scss create mode 100644 src/components/Gallery/components/FallbackText/FallbackText.tsx create mode 100644 src/components/Gallery/components/FallbackText/i18n/en.json create mode 100644 src/components/Gallery/components/FallbackText/i18n/index.ts create mode 100644 src/components/Gallery/components/FallbackText/i18n/ru.json create mode 100644 src/components/Gallery/components/FallbackText/index.ts create mode 100644 src/components/Gallery/components/actions/FullScreenAction/FullScreenAction.tsx create mode 100644 src/components/Gallery/components/actions/FullScreenAction/hooks/useFullScreenHotkeys.ts create mode 100644 src/components/Gallery/components/actions/FullScreenAction/i18n/en.json create mode 100644 src/components/Gallery/components/actions/FullScreenAction/i18n/index.ts create mode 100644 src/components/Gallery/components/actions/FullScreenAction/i18n/ru.json create mode 100644 src/components/Gallery/components/actions/FullScreenAction/index.ts create mode 100644 src/components/Gallery/components/actions/index.ts create mode 100644 src/components/Gallery/components/views/DocumentView/DocumentView.scss create mode 100644 src/components/Gallery/components/views/DocumentView/DocumentView.tsx create mode 100644 src/components/Gallery/components/views/ImageView/ImageView.scss create mode 100644 src/components/Gallery/components/views/ImageView/ImageView.tsx create mode 100644 src/components/Gallery/components/views/VideoView/VideoView.scss create mode 100644 src/components/Gallery/components/views/VideoView/VideoView.tsx create mode 100644 src/components/Gallery/components/views/VideoView/i18n/en.json create mode 100644 src/components/Gallery/components/views/VideoView/i18n/index.ts create mode 100644 src/components/Gallery/components/views/VideoView/i18n/ru.json create mode 100644 src/components/Gallery/hooks/useNavigation.ts create mode 100644 src/components/Gallery/i18n/en.json create mode 100644 src/components/Gallery/i18n/index.ts create mode 100644 src/components/Gallery/i18n/ru.json create mode 100644 src/components/Gallery/index.ts create mode 100644 src/components/Gallery/utils/getDefaultGalleryItemDocument.tsx create mode 100644 src/components/Gallery/utils/getDefaultGalleryItemImage.tsx create mode 100644 src/components/Gallery/utils/getDefaultGalleryItemVideo.tsx create mode 100644 src/components/Gallery/utils/getInvertedTheme.ts diff --git a/CODEOWNERS b/CODEOWNERS index f95a046c..d65468e8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -13,3 +13,4 @@ /src/components/StoreBadge @NikitaCG /src/components/Stories @darkgenius /src/components/ConfirmDialog @kseniya57 +/src/components/Gallery @kseniya57 diff --git a/src/components/FilePreview/FilePreview.tsx b/src/components/FilePreview/FilePreview.tsx index ee31103b..ec3941e6 100644 --- a/src/components/FilePreview/FilePreview.tsx +++ b/src/components/FilePreview/FilePreview.tsx @@ -53,6 +53,7 @@ export interface FilePreviewProps extends QAProps { onClick?: React.MouseEventHandler; actions?: FilePreviewActionProps[]; + hideName?: boolean; } export function FilePreview({ @@ -63,6 +64,7 @@ export function FilePreview({ description, onClick, actions, + hideName, }: FilePreviewProps) { const id = useUniqId(); @@ -125,9 +127,11 @@ export function FilePreview({ )} - - {file.name} - + {!hideName && ( + + {file.name} + + )} {Boolean(description) && ( \| AnchorHTMLAttributes` | | | Additional action button props | +| Property | Type | Required | Default | Description | +| ---------- | ----------------------------------------- | ---------------------------------------- | ------- | ------------------------------ | +| id | `String` | | | Action id | +| icon | `String` | ✓ | | Action icon | +| title | `String` | ✓ | | Action hint on hover | +| onClick | `function` | | | Action click handler | +| href | `String` | | | Action button href | +| extraProps | `ButtonHTMLAttributes` | AnchorHTMLAttributes` | | Additional action button props | ```jsx diff --git a/src/components/Gallery/Gallery.scss b/src/components/Gallery/Gallery.scss new file mode 100644 index 00000000..b5c69378 --- /dev/null +++ b/src/components/Gallery/Gallery.scss @@ -0,0 +1,214 @@ +@use '../variables'; + +$block: '.#{variables.$ns}gallery'; +$filePreviewBlock: '.#{variables.$ns}file-preview'; + +#{$block} { + .g-modal__content-wrapper { + margin: 0; + } + + &__content { + display: flex; + flex-direction: column; + + width: calc(100vw - 264px); + height: calc(100vh - 56px); + } + + &__header { + display: flex; + align-items: start; + + padding: var(--g-spacing-2) var(--g-spacing-3) var(--g-spacing-2) var(--g-spacing-5); + + > * { + flex: 1; + min-width: 0; + } + } + + &__navigation { + display: flex; + gap: var(--g-spacing-2); + align-items: center; + justify-content: center; + } + + &__actions { + display: flex; + gap: var(--g-spacing-1); + align-items: stretch; + justify-content: flex-end; + } + + &__active-item-info { + align-self: stretch; + align-items: center; + display: flex; + } + + &__body { + position: relative; + + display: flex; + align-items: center; + justify-content: center; + + flex: 1; + min-height: 0; + + padding: 0 var(--g-spacing-2); + } + + &__body-navigation-button { + position: absolute; + inset-block: 0 60px; + z-index: 2; + + width: 200px; + max-width: 20%; + padding: 0; + margin: 0; + + appearance: none; + cursor: pointer; + + background-color: transparent; + border: none; + outline: none; + + &_direction_left { + inset-inline-start: 0; + // cursor design is in progress + // cursor: + // url('./assets/arrow-left.svg') 2 2, + // default; + } + + &_direction_right { + inset-inline-end: var(--g-spacing-5); + // cursor design is in progress + // cursor: + // url('./assets/arrow-right.svg') 2 2, + // default; + } + } + + &__footer { + padding: var(--g-spacing-2) var(--g-spacing-5) var(--g-spacing-4) var(--g-spacing-5); + } + + &__preview-list { + display: flex; + gap: var(--g-spacing-2); + align-items: stretch; + overflow: auto hidden; + -ms-overflow-style: none; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + } + + &__preview-list-item { + width: 48px; + min-width: 48px; + height: 48px; + border: 2px solid transparent; + border-radius: var(--g-border-radius-l); + padding: 0; + margin: 0; + + appearance: none; + cursor: pointer; + + background-color: transparent; + outline: none; + overflow: hidden; + + &_selected { + border-color: var(--g-color-line-brand); + } + } + + &_mode_full-screen { + overflow: hidden; + + .g-modal__content-wrapper, + .g-modal__content { + border-radius: 0; + } + + #{$block} { + &__content { + width: 100vw; + height: 100vh; + } + + &__body { + padding: 0; + } + + &__header { + position: absolute; + inset-block-start: 0; + inset-inline: 0; + z-index: 3; + + opacity: 0; + + &:hover { + opacity: 1; + } + } + + &__footer { + position: absolute; + inset-inline: 0; + inset-block-end: 0; + z-index: 1; + + opacity: 0; + background-color: rgba(0, 0, 0, 0.45); + + &:hover { + opacity: 1; + } + } + } + + .g-root_theme_light, + .g-root_theme_light-hc { + #{$block}__header { + background-color: var(--g-color-private-white-450); + } + } + + .g-root_theme_dark, + .g-root_theme_dark-hc { + #{$block}__header { + background-color: var(--g-color-private-black-450); + } + } + } + + #{$filePreviewBlock}[class] { + width: 100%; + height: 100%; + } + + #{$filePreviewBlock}__card { + width: 100%; + min-width: 100%; + height: 100%; + padding: 0; + } + + #{$filePreviewBlock}__image, + #{$filePreviewBlock}__icon { + width: 100%; + height: 100%; + } +} diff --git a/src/components/Gallery/Gallery.tsx b/src/components/Gallery/Gallery.tsx new file mode 100644 index 00000000..dcf1db6e --- /dev/null +++ b/src/components/Gallery/Gallery.tsx @@ -0,0 +1,142 @@ +import React from 'react'; + +import {ArrowLeft, ArrowRight, Xmark} from '@gravity-ui/icons'; +import type {ModalProps} from '@gravity-ui/uikit'; +import {Button, Icon, Modal, Text, ThemeProvider, useThemeValue} from '@gravity-ui/uikit'; + +import {block} from '../utils/cn'; + +import type {GalleryItemProps} from './GalleryItem'; +import type {UseNavigationProps} from './hooks/useNavigation'; +import {useNavigation} from './hooks/useNavigation'; +import {i18n} from './i18n'; +import {getInvertedTheme} from './utils/getInvertedTheme'; + +import './Gallery.scss'; + +const cnGallery = block('gallery'); + +export type GalleryProps = { + onClose: () => void; + fullScreen?: boolean; + modalClassName?: string; + className?: string; + children: React.ReactElement[]; + invertTheme?: boolean; +} & Pick & + Pick; + +export const Gallery = ({ + initialItemIndex, + open, + onClose, + fullScreen, + container, + modalClassName, + className, + invertTheme, + children, +}: GalleryProps) => { + const items = React.Children.map(children, (child) => child.props); + const theme = useThemeValue(); + + const {activeItemIndex, setActiveItemIndex, handleGoToNext, handleGoToPrevious} = useNavigation( + { + itemsCount: items.length, + initialItemIndex, + selectedPreviewItemClass: `.${cnGallery('preview-list-item')}_selected`, + }, + ); + + const activeItem = items[activeItemIndex] || items[0]; + + if (!items.length) { + return null; + } + + return ( + + +
+
+
{activeItem?.meta}
+
+ + + {activeItemIndex + 1}/{items.length} + + +
+
+ {activeItem.actions} + +
+
+
+ {activeItem?.view} + {activeItem?.interactive !== false && ( + +
+ {!fullScreen && ( +
+
+ {items.map((item, index) => { + const handleClick = () => { + setActiveItemIndex(index); + }; + + const selected = activeItemIndex === index; + + return ( + + ); + })} +
+
+ )} +
+
+
+ ); +}; diff --git a/src/components/Gallery/GalleryItem.tsx b/src/components/Gallery/GalleryItem.tsx new file mode 100644 index 00000000..eb4a427d --- /dev/null +++ b/src/components/Gallery/GalleryItem.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export type GalleryItemProps = { + view: React.ReactNode; + thumbnail: React.ReactNode; + meta?: React.ReactNode; + actions?: React.ReactNode[]; + interactive?: boolean; +}; + +export const GalleryItem = (_props: GalleryItemProps) => { + return null; +}; diff --git a/src/components/Gallery/README.md b/src/components/Gallery/README.md new file mode 100644 index 00000000..9e5c9fcc --- /dev/null +++ b/src/components/Gallery/README.md @@ -0,0 +1,261 @@ +## Gallery + +The base component for rendering galleries of any type of data. +The component is responsible for the gallery navigation (keyboard arrows, body side click and header arrow click). +The children of the Gallery should be an array of [GalleryItem with the required properties](#GalleryItem) for rendering the gallery item view. + +### PropTypes + +| Property | Type | Required | Values | Default | Description | +| :--------------- | :----------- | :------- | :----- | :------ | :------------------------------- | +| initialItemIndex | `Number` | | | 0 | The initial active item index | +| open | `Boolean` | | | | The modal opened state | +| onClose | `() => void` | Yes | | | The modal close handler | +| fullScreen | `Boolean` | | | | The gallery full screen mode | +| modalClassName | `String` | | | | The modal class | +| className | `String` | | | | The modal content class | +| invertTheme | `Boolean` | | | | Invert the theme for the gallery | + +### GalleryItem + +| Property | Type | Required | Values | Default | Description | +| :---------- | :------------ | :------- | :----- | :------ | :----------------------------------------------------------------------------------------------- | +| view | `ReactNode` | Yes | | 0 | The gallery item body (displayed in the center of the gallery) | +| thumbnail | `ReactNode` | Yes | | | The gallery item thumbnail (displayed as the preview in the footer of the gallery) | +| meta | `ReactNode` | | | | The gallery item meta info (displayed in the gallery header left side) | +| actions | `ReactNode[]` | | | | The array of the gallery item action buttons | +| interactive | `boolean` | | | | Provide true if the gallery item is interactive and the navigation by body click should not work | + +### Default actions renderers + +We export some default actions renderers, you can put them to actions array in the gallery item props: + +```tsx +import {FullScreenAction} from '@gravity-ui/components'; + +const actions: React.ReactNode[] = [ + , +]; +``` + +### Default gallery item props + +We export some utility functions for getting the gallery item props: + +```tsx +import { + GalleryItem, + getDefaultGalleryItemDocument, + getDefaultGalleryItemImage, + getDefaultGalleryItemVideo, +} from '@gravity-ui/components'; + +// render the image gallery item + + +// render the video gallery item + + +// render the iframe gallery item + +``` + +### Examples + +#### Simple images gallery + +```tsx +import React from 'react'; + +import {Button, usePortalContainer} from '@gravity-ui/uikit'; +import { + FullScreenAction, + Gallery, + GalleryItem, + getDefaultGalleryItemImage, +} from '@gravity-ui/components'; + +const images = [ + 'https://i.pinimg.com/originals/d8/bd/b4/d8bdb45a931b4265bec8e8d3f15021bf.jpg', + 'https://i.pinimg.com/originals/c2/31/a0/c231a069c5e24099723564dae736f438.jpg', +]; + +const ImagesGallery = () => { + const [open, setOpen] = React.useState(false); + const [fullScreen, setFullScreen] = React.useState(false); + + const container = usePortalContainer(); + + const handleClose = React.useCallback(() => { + setOpen(false); + }, []); + + const handleOpen = React.useCallback(() => { + setOpen(true); + }, []); + + const renderActions = React.useCallback(() => { + return [ + , + ]; + }, [fullScreen]); + + return ( + + + + {images.map((image, index) => ( + + ))} + + + ); +}; +``` + +#### Files gallery + +```tsx +import React from 'react'; + +import {Button, Text, usePortalContainer} from '@gravity-ui/uikit'; +import { + FullScreenAction, + FilePreview, + Gallery, + GalleryItem, + GalleryProps, + getDefaultGalleryItemDocument, + getDefaultGalleryItemImage, + getDefaultGalleryItemVideo, +} from '@gravity-ui/components'; + +const FilesGalleryTemplate: StoryFn = () => { + const [open, setOpen] = React.useState(false); + const [fullScreen, setFullScreen] = React.useState(false); + + const container = usePortalContainer(); + + const handleClose = React.useCallback(() => { + setOpen(false); + }, []); + + const handleOpen = React.useCallback(() => { + setOpen(true); + }, []); + + const renderActions = React.useCallback(() => { + return [ + , + ]; + }, [fullScreen]); + + return ( + + + + {files.map((file, index) => ( + + ))} + + + ); +}; + +type GalleryFile = + | { + name: string; + type: 'image' | 'video' | 'document'; + url: string; + interactive?: boolean; + } + | {name: string; type: 'text'; text: string; interactive?: boolean}; + +const getGalleryItemFile = (file: GalleryFile) => { + switch (file.type) { + case 'image': + return getDefaultGalleryItemImage({src: file.url, name: file.name}); + case 'video': + return getDefaultGalleryItemVideo({src: file.url, name: file.name}); + case 'document': + return getDefaultGalleryItemDocument({ + src: file.url, + file: {name: file.name, type: file.type} as File, + }); + case 'text': + return { + thumbnail: , + view: {file.text}, + meta: file.name, + }; + } +}; + +const files: GalleryFile[] = [ + { + type: 'image', + url: 'https://santreyd.ru/upload/iblock/acc/accd0c751590e792f7e43a05f22472f9.jpg', + name: 'Corgi image', + }, + { + type: 'video', + url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', + name: 'Bunny Film', + }, + { + type: 'text', + text: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum', + name: 'Some text', + }, + { + type: 'document', + url: 'https://preview.gravity-ui.com/icons', + name: 'Html page', + interactive: true, + }, +]; +``` diff --git a/src/components/Gallery/__stories__/Gallery.stories.tsx b/src/components/Gallery/__stories__/Gallery.stories.tsx new file mode 100644 index 00000000..afa31e47 --- /dev/null +++ b/src/components/Gallery/__stories__/Gallery.stories.tsx @@ -0,0 +1,157 @@ +import React from 'react'; + +import {Button, Text, usePortalContainer} from '@gravity-ui/uikit'; +import type {Meta, StoryFn} from '@storybook/react'; + +import { + FullScreenAction, + Gallery, + GalleryItem, + GalleryProps, + getDefaultGalleryItemDocument, + getDefaultGalleryItemImage, + getDefaultGalleryItemVideo, +} from '../'; +import {FilePreview} from '../../FilePreview'; + +import {GalleryFile, files, images} from './mockData'; + +export default { + title: 'Components/Gallery', + component: Gallery, + parameters: { + a11y: { + element: '#storybook-root', + config: { + rules: [ + { + id: 'color-contrast', + enabled: false, + }, + ], + }, + }, + }, +} as Meta; + +const ImagesGalleryTemplate: StoryFn = () => { + const [open, setOpen] = React.useState(false); + const [fullScreen, setFullScreen] = React.useState(false); + + const container = usePortalContainer(); + + const handleClose = React.useCallback(() => { + setOpen(false); + }, []); + + const handleOpen = React.useCallback(() => { + setOpen(true); + }, []); + + const renderActions = React.useCallback(() => { + return [ + , + ]; + }, [fullScreen]); + + return ( + + + + {images.map((image, index) => ( + + ))} + + + ); +}; + +export const ImagesGallery = ImagesGalleryTemplate.bind({}); + +const getGalleryItemFile = (file: GalleryFile) => { + switch (file.type) { + case 'image': + return getDefaultGalleryItemImage({src: file.url, name: file.name}); + case 'video': + return getDefaultGalleryItemVideo({src: file.url, name: file.name}); + case 'document': + return getDefaultGalleryItemDocument({ + src: file.url, + file: {name: file.name, type: file.type} as File, + }); + case 'text': + return { + thumbnail: ( + + ), + view: {file.text}, + meta: file.name, + }; + } +}; + +const FilesGalleryTemplate: StoryFn = () => { + const [open, setOpen] = React.useState(false); + const [fullScreen, setFullScreen] = React.useState(false); + + const container = usePortalContainer(); + + const handleClose = React.useCallback(() => { + setOpen(false); + }, []); + + const handleOpen = React.useCallback(() => { + setOpen(true); + }, []); + + const renderActions = React.useCallback(() => { + return [ + , + ]; + }, [fullScreen]); + + return ( + + + + {files.map((file, index) => ( + + ))} + + + ); +}; + +export const FilesGallery = FilesGalleryTemplate.bind({}); diff --git a/src/components/Gallery/__stories__/mockData.ts b/src/components/Gallery/__stories__/mockData.ts new file mode 100644 index 00000000..57ea0892 --- /dev/null +++ b/src/components/Gallery/__stories__/mockData.ts @@ -0,0 +1,62 @@ +export const images = [ + 'https://i.pinimg.com/originals/d8/bd/b4/d8bdb45a931b4265bec8e8d3f15021bf.jpg', + 'https://i.pinimg.com/originals/c2/31/a0/c231a069c5e24099723564dae736f438.jpg', + 'https://cs4.pikabu.ru/post_img/big/2015/02/27/6/1425024947_2006737473.jpeg', + 'https://i.pinimg.com/originals/ef/7b/97/ef7b9724ad06cd6dfce92193e95a5caa.jpg', + 'https://avatars.mds.yandex.net/i?id=ea31df78678a1b3f4f1fb7199090831d_l-5235412-images-thumbs&n=13', + 'https://i.ytimg.com/vi/WA63GQpLzjA/maxresdefault.jpg', + 'https://i.pinimg.com/originals/02/eb/fd/02ebfd63d5435ec87c7413b8b2428214.jpg', + 'https://mir-s3-cdn-cf.behance.net/project_modules/max_3840/2b800731080995.5640a39521da5.jpg', + 'https://pic.rutubelist.ru/video/7a/1b/7a1b88f88ff7a470ea6f8131d51c2c5c.jpg', + 'https://i.pinimg.com/originals/4b/c7/ed/4bc7ed612f2080303644deb0f857b70f.jpg', + 'https://img1.reactor.cc/pics/post/нейроарт-нейронные-сети-красивые-картинки-art-7821877.png', + 'https://steamuserimages-a.akamaihd.net/ugc/841461304090603934/D3243F5856FEAE2052FC7CDB748B5BB65E6B247A/?imw=512&imh=306&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true', + 'https://celes.club/uploads/posts/2022-06/1654752045_50-celes-club-p-multyashnii-kosmos-oboi-krasivie-53.jpg', + // duplicate the list to show the previews scroll + 'https://i.pinimg.com/originals/d8/bd/b4/d8bdb45a931b4265bec8e8d3f15021bf.jpg', + 'https://i.pinimg.com/originals/c2/31/a0/c231a069c5e24099723564dae736f438.jpg', + 'https://cs4.pikabu.ru/post_img/big/2015/02/27/6/1425024947_2006737473.jpeg', + 'https://i.pinimg.com/originals/ef/7b/97/ef7b9724ad06cd6dfce92193e95a5caa.jpg', + 'https://avatars.mds.yandex.net/i?id=ea31df78678a1b3f4f1fb7199090831d_l-5235412-images-thumbs&n=13', + 'https://i.ytimg.com/vi/WA63GQpLzjA/maxresdefault.jpg', + 'https://i.pinimg.com/originals/02/eb/fd/02ebfd63d5435ec87c7413b8b2428214.jpg', + 'https://mir-s3-cdn-cf.behance.net/project_modules/max_3840/2b800731080995.5640a39521da5.jpg', + 'https://pic.rutubelist.ru/video/7a/1b/7a1b88f88ff7a470ea6f8131d51c2c5c.jpg', + 'https://i.pinimg.com/originals/4b/c7/ed/4bc7ed612f2080303644deb0f857b70f.jpg', + 'https://img1.reactor.cc/pics/post/нейроарт-нейронные-сети-красивые-картинки-art-7821877.png', + 'https://steamuserimages-a.akamaihd.net/ugc/841461304090603934/D3243F5856FEAE2052FC7CDB748B5BB65E6B247A/?imw=512&imh=306&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true', + 'https://celes.club/uploads/posts/2022-06/1654752045_50-celes-club-p-multyashnii-kosmos-oboi-krasivie-53.jpg', +]; + +export type GalleryFile = + | { + name: string; + type: 'image' | 'video' | 'document'; + url: string; + interactive?: boolean; + } + | {name: string; type: 'text'; text: string; interactive?: boolean}; + +export const files: GalleryFile[] = [ + { + type: 'image', + url: 'https://santreyd.ru/upload/iblock/acc/accd0c751590e792f7e43a05f22472f9.jpg', + name: 'Corgi image', + }, + { + type: 'video', + url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', + name: 'Bunny Film', + }, + { + type: 'text', + text: 'Lorem Ipsum - это текст-"рыба", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной "рыбой" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum', + name: 'Some text', + }, + { + type: 'document', + url: 'https://preview.gravity-ui.com/icons', + name: 'Html page', + interactive: true, + }, +]; diff --git a/src/components/Gallery/assets/arrow-left.svg b/src/components/Gallery/assets/arrow-left.svg new file mode 100644 index 00000000..fa65bab2 --- /dev/null +++ b/src/components/Gallery/assets/arrow-left.svg @@ -0,0 +1 @@ + diff --git a/src/components/Gallery/assets/arrow-right.svg b/src/components/Gallery/assets/arrow-right.svg new file mode 100644 index 00000000..a03b2945 --- /dev/null +++ b/src/components/Gallery/assets/arrow-right.svg @@ -0,0 +1 @@ + diff --git a/src/components/Gallery/components/FallbackText/FallbackText.scss b/src/components/Gallery/components/FallbackText/FallbackText.scss new file mode 100644 index 00000000..f3890848 --- /dev/null +++ b/src/components/Gallery/components/FallbackText/FallbackText.scss @@ -0,0 +1,19 @@ +@use '../../../variables'; + +$block: '.#{variables.$ns}files-gallery-fallback-text'; + +#{$block} { + display: flex; + align-items: center; + justify-content: center; + + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 0 var(--g-spacing-10); + + font-size: 25px; + line-height: 1.2; + + color: var(--g-color-text-secondary); +} diff --git a/src/components/Gallery/components/FallbackText/FallbackText.tsx b/src/components/Gallery/components/FallbackText/FallbackText.tsx new file mode 100644 index 00000000..4405e2db --- /dev/null +++ b/src/components/Gallery/components/FallbackText/FallbackText.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import {block} from '../../../utils/cn'; + +import {i18n} from './i18n'; + +import './FallbackText.scss'; + +const cnFallbackText = block('files-gallery-fallback-text'); + +export type FallbackTextProps = React.HTMLAttributes; + +export const FilesGalleryFallbackText = ({children, className, ...props}: FallbackTextProps) => ( +
+ {children || i18n('cannot-display-file')} +
+); diff --git a/src/components/Gallery/components/FallbackText/i18n/en.json b/src/components/Gallery/components/FallbackText/i18n/en.json new file mode 100644 index 00000000..db7b1965 --- /dev/null +++ b/src/components/Gallery/components/FallbackText/i18n/en.json @@ -0,0 +1,3 @@ +{ + "cannot-display-file": "Cannot display file" +} diff --git a/src/components/Gallery/components/FallbackText/i18n/index.ts b/src/components/Gallery/components/FallbackText/i18n/index.ts new file mode 100644 index 00000000..fd527175 --- /dev/null +++ b/src/components/Gallery/components/FallbackText/i18n/index.ts @@ -0,0 +1,8 @@ +import {addComponentKeysets} from '@gravity-ui/uikit/i18n'; + +import {NAMESPACE} from '../../../../utils/cn'; + +import en from './en.json'; +import ru from './ru.json'; + +export const i18n = addComponentKeysets({en, ru}, `${NAMESPACE}files-gallery-fallback-text`); diff --git a/src/components/Gallery/components/FallbackText/i18n/ru.json b/src/components/Gallery/components/FallbackText/i18n/ru.json new file mode 100644 index 00000000..dc837ba9 --- /dev/null +++ b/src/components/Gallery/components/FallbackText/i18n/ru.json @@ -0,0 +1,3 @@ +{ + "cannot-display-file": "Невозможно отобразить файл" +} diff --git a/src/components/Gallery/components/FallbackText/index.ts b/src/components/Gallery/components/FallbackText/index.ts new file mode 100644 index 00000000..4ecbb69c --- /dev/null +++ b/src/components/Gallery/components/FallbackText/index.ts @@ -0,0 +1 @@ +export * from './FallbackText'; diff --git a/src/components/Gallery/components/actions/FullScreenAction/FullScreenAction.tsx b/src/components/Gallery/components/actions/FullScreenAction/FullScreenAction.tsx new file mode 100644 index 00000000..233dc234 --- /dev/null +++ b/src/components/Gallery/components/actions/FullScreenAction/FullScreenAction.tsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import {ChevronsCollapseUpRight, ChevronsExpandUpRight} from '@gravity-ui/icons'; +import {ActionTooltip, Button, ButtonProps, Icon} from '@gravity-ui/uikit'; + +import {useFullScreenHotkeys} from './hooks/useFullScreenHotkeys'; +import {i18n} from './i18n'; + +export type FullScreenActionProps = { + fullScreen: boolean; + onUpdateFullScreen: React.Dispatch>; +} & Omit; + +export const FullScreenAction = React.memo( + ({fullScreen, onUpdateFullScreen, ...buttonProps}: FullScreenActionProps) => { + const handleToggleFullScreen = React.useCallback(() => { + onUpdateFullScreen((value) => !value); + }, [onUpdateFullScreen]); + + useFullScreenHotkeys({fullScreen, onUpdateFullScreen}); + + return ( + + + + ); + }, +); + +FullScreenAction.displayName = 'FullScreenAction'; diff --git a/src/components/Gallery/components/actions/FullScreenAction/hooks/useFullScreenHotkeys.ts b/src/components/Gallery/components/actions/FullScreenAction/hooks/useFullScreenHotkeys.ts new file mode 100644 index 00000000..c40bd5b7 --- /dev/null +++ b/src/components/Gallery/components/actions/FullScreenAction/hooks/useFullScreenHotkeys.ts @@ -0,0 +1,26 @@ +import React from 'react'; + +import {FullScreenActionProps} from '../FullScreenAction'; + +export type UseFullScreenHotkeysProps = Pick< + FullScreenActionProps, + 'fullScreen' | 'onUpdateFullScreen' +>; + +export function useFullScreenHotkeys({fullScreen, onUpdateFullScreen}: UseFullScreenHotkeysProps) { + React.useEffect(() => { + const handleCloseFullScreen = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onUpdateFullScreen(false); + } else if (event.shiftKey && event.code === 'KeyF') { + onUpdateFullScreen((value) => !value); + } + }; + + document.addEventListener('keyup', handleCloseFullScreen); + + return () => { + document.removeEventListener('keyup', handleCloseFullScreen); + }; + }, [fullScreen, onUpdateFullScreen]); +} diff --git a/src/components/Gallery/components/actions/FullScreenAction/i18n/en.json b/src/components/Gallery/components/actions/FullScreenAction/i18n/en.json new file mode 100644 index 00000000..6864fdcf --- /dev/null +++ b/src/components/Gallery/components/actions/FullScreenAction/i18n/en.json @@ -0,0 +1,4 @@ +{ + "enter-full-screen": "Activate full screen mode", + "exit-full-screen": "Exit full screen mode" +} diff --git a/src/components/Gallery/components/actions/FullScreenAction/i18n/index.ts b/src/components/Gallery/components/actions/FullScreenAction/i18n/index.ts new file mode 100644 index 00000000..82070841 --- /dev/null +++ b/src/components/Gallery/components/actions/FullScreenAction/i18n/index.ts @@ -0,0 +1,8 @@ +import {addComponentKeysets} from '@gravity-ui/uikit/i18n'; + +import {NAMESPACE} from '../../../../../utils/cn'; + +import en from './en.json'; +import ru from './ru.json'; + +export const i18n = addComponentKeysets({en, ru}, `${NAMESPACE}gallery-full-screen-action`); diff --git a/src/components/Gallery/components/actions/FullScreenAction/i18n/ru.json b/src/components/Gallery/components/actions/FullScreenAction/i18n/ru.json new file mode 100644 index 00000000..de3a5837 --- /dev/null +++ b/src/components/Gallery/components/actions/FullScreenAction/i18n/ru.json @@ -0,0 +1,4 @@ +{ + "enter-full-screen": "Открыть полноэкранный режим", + "exit-full-screen": "Закрыть полноэкранный режим" +} diff --git a/src/components/Gallery/components/actions/FullScreenAction/index.ts b/src/components/Gallery/components/actions/FullScreenAction/index.ts new file mode 100644 index 00000000..3932114c --- /dev/null +++ b/src/components/Gallery/components/actions/FullScreenAction/index.ts @@ -0,0 +1 @@ +export * from './FullScreenAction'; diff --git a/src/components/Gallery/components/actions/index.ts b/src/components/Gallery/components/actions/index.ts new file mode 100644 index 00000000..3932114c --- /dev/null +++ b/src/components/Gallery/components/actions/index.ts @@ -0,0 +1 @@ +export * from './FullScreenAction'; diff --git a/src/components/Gallery/components/views/DocumentView/DocumentView.scss b/src/components/Gallery/components/views/DocumentView/DocumentView.scss new file mode 100644 index 00000000..8713b5b4 --- /dev/null +++ b/src/components/Gallery/components/views/DocumentView/DocumentView.scss @@ -0,0 +1,10 @@ +@use '../../../../variables'; + +$block: '.#{variables.$ns}gallery-document-view'; + +#{$block} { + width: 100%; + height: 100%; + + border: none; +} diff --git a/src/components/Gallery/components/views/DocumentView/DocumentView.tsx b/src/components/Gallery/components/views/DocumentView/DocumentView.tsx new file mode 100644 index 00000000..82f62479 --- /dev/null +++ b/src/components/Gallery/components/views/DocumentView/DocumentView.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import {block} from '../../../../utils/cn'; + +import './DocumentView.scss'; + +const cnDocumentView = block('gallery-document-view'); + +export type DocumentViewProps = { + className?: string; + name: string; + src: string; + sandbox?: string; +} & React.HTMLAttributes; + +export const DocumentView = ({className, name, ...props}: DocumentViewProps) => { + return