feat: Ability to record videos and capture photos directly in app by selecting + in Files view (#2095)
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { PhotoRecorder } from '@/Controllers/Moments/PhotoRecorder'
|
||||
import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import Dropdown from '../Dropdown/Dropdown'
|
||||
import Icon from '../Icon/Icon'
|
||||
import DecoratedInput from '../Input/DecoratedInput'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
|
||||
type Props = {
|
||||
filesController: FilesController
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const PhotoCaptureModal = ({ filesController, close }: Props) => {
|
||||
const [fileName, setFileName] = useState(formatDateAndTimeForNote(new Date()))
|
||||
const [recorder, setRecorder] = useState<PhotoRecorder | undefined>(() => new PhotoRecorder())
|
||||
const [isRecorderReady, setIsRecorderReady] = useState(false)
|
||||
const [capturedPhoto, setCapturedPhoto] = useState<File>()
|
||||
|
||||
const fileNameInputRef = useRef<HTMLInputElement>(null)
|
||||
const previewRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!recorder) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsRecorderReady(false)
|
||||
|
||||
const init = async () => {
|
||||
await recorder.initialize()
|
||||
|
||||
if (previewRef.current) {
|
||||
recorder.video.style.position = ''
|
||||
recorder.video.style.display = ''
|
||||
recorder.video.style.height = '100%'
|
||||
previewRef.current.append(recorder.video)
|
||||
}
|
||||
|
||||
setIsRecorderReady(true)
|
||||
}
|
||||
|
||||
void init()
|
||||
|
||||
return () => {
|
||||
if (recorder.video) {
|
||||
recorder.finish()
|
||||
}
|
||||
}
|
||||
}, [recorder])
|
||||
|
||||
const takePhoto = useCallback(async () => {
|
||||
if (!recorder) {
|
||||
return
|
||||
}
|
||||
|
||||
const file = await recorder.takePhoto(fileName)
|
||||
setCapturedPhoto(file)
|
||||
setRecorder(undefined)
|
||||
}, [fileName, recorder])
|
||||
|
||||
const devicesAsDropdownItems = useMemo(() => {
|
||||
return recorder?.devices
|
||||
? recorder.devices.map((device) => ({
|
||||
label: device.label || `Camera (${device.deviceId.slice(0, 10)})`,
|
||||
value: device.deviceId,
|
||||
}))
|
||||
: []
|
||||
}, [recorder?.devices])
|
||||
|
||||
const savePhoto = useCallback(() => {
|
||||
if (!fileName) {
|
||||
fileNameInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
if (!capturedPhoto) {
|
||||
return
|
||||
}
|
||||
void filesController.uploadNewFile(capturedPhoto)
|
||||
close()
|
||||
}, [capturedPhoto, close, fileName, filesController])
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={close}>Take a photo</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="mb-4 flex flex-col">
|
||||
<label className="text-sm font-medium text-neutral">
|
||||
File name:
|
||||
<DecoratedInput
|
||||
className={{
|
||||
container: 'mt-1',
|
||||
}}
|
||||
value={fileName}
|
||||
onChange={(fileName) => setFileName(fileName)}
|
||||
ref={fileNameInputRef}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<div className="text-sm font-medium text-neutral">Preview:</div>
|
||||
{!isRecorderReady && (
|
||||
<div className="mt-1 w-full">
|
||||
<div className="flex h-64 w-full items-center justify-center gap-2 rounded-md bg-contrast text-base">
|
||||
<Icon type="camera" className="text-neutral-300" />
|
||||
Initializing...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={classNames('mt-1 w-full', capturedPhoto && 'hidden')} ref={previewRef}></div>
|
||||
{capturedPhoto && (
|
||||
<div className="mt-1 w-full">
|
||||
<img src={URL.createObjectURL(capturedPhoto)} alt="Captured photo" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{recorder && devicesAsDropdownItems.length > 1 && !capturedPhoto && (
|
||||
<div className="mt-4">
|
||||
<label className="text-sm font-medium text-neutral">
|
||||
Device:
|
||||
<Dropdown
|
||||
id={'photo-capture-device-dropdown'}
|
||||
label={'Photo Capture Device'}
|
||||
items={devicesAsDropdownItems}
|
||||
value={recorder.selectedDevice.deviceId}
|
||||
onChange={(value: string) => {
|
||||
void recorder.setDevice(value)
|
||||
}}
|
||||
className={{
|
||||
wrapper: 'mt-1',
|
||||
popover: 'z-modal',
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{!capturedPhoto && (
|
||||
<Button
|
||||
primary
|
||||
colorStyle="danger"
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
void takePhoto()
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" />
|
||||
Take photo
|
||||
</Button>
|
||||
)}
|
||||
{capturedPhoto && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setCapturedPhoto(undefined)
|
||||
setRecorder(new PhotoRecorder())
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
<Button primary className="flex items-center gap-2" onClick={savePhoto}>
|
||||
<Icon type="upload" />
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(PhotoCaptureModal)
|
||||
@@ -0,0 +1,166 @@
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { VideoRecorder } from '@/Controllers/Moments/VideoRecorder'
|
||||
import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import Icon from '../Icon/Icon'
|
||||
import DecoratedInput from '../Input/DecoratedInput'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
|
||||
type Props = {
|
||||
filesController: FilesController
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const VideoCaptureModal = ({ filesController, close }: Props) => {
|
||||
const [fileName, setFileName] = useState(formatDateAndTimeForNote(new Date()))
|
||||
const [recorder, setRecorder] = useState(() => new VideoRecorder(fileName))
|
||||
const [isRecorderReady, setIsRecorderReady] = useState(false)
|
||||
const [isRecording, setIsRecording] = useState(false)
|
||||
const [capturedVideo, setCapturedVideo] = useState<File>()
|
||||
|
||||
const fileNameInputRef = useRef<HTMLInputElement>(null)
|
||||
const previewRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
await recorder.initialize()
|
||||
|
||||
if (previewRef.current) {
|
||||
recorder.video.style.position = ''
|
||||
recorder.video.style.display = ''
|
||||
recorder.video.style.height = '100%'
|
||||
previewRef.current.append(recorder.video)
|
||||
}
|
||||
|
||||
setIsRecorderReady(true)
|
||||
}
|
||||
|
||||
void init()
|
||||
|
||||
return () => {
|
||||
if (recorder.video) {
|
||||
void recorder.stop()
|
||||
}
|
||||
}
|
||||
}, [recorder])
|
||||
|
||||
const startRecording = useCallback(async () => {
|
||||
await recorder.startRecording()
|
||||
setIsRecording(true)
|
||||
}, [recorder])
|
||||
|
||||
const saveVideo = useCallback(() => {
|
||||
if (!fileName) {
|
||||
fileNameInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
if (!capturedVideo) {
|
||||
return
|
||||
}
|
||||
const namedFile = new File([capturedVideo], fileName, {
|
||||
type: capturedVideo.type,
|
||||
})
|
||||
void filesController.uploadNewFile(namedFile)
|
||||
close()
|
||||
}, [capturedVideo, close, fileName, filesController])
|
||||
|
||||
const capturedVideoObjectURL = useMemo(() => {
|
||||
if (!capturedVideo) {
|
||||
return
|
||||
}
|
||||
return URL.createObjectURL(capturedVideo)
|
||||
}, [capturedVideo])
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={close}>Record a video</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="mb-4 flex flex-col">
|
||||
<label className="text-sm font-medium text-neutral">
|
||||
File name:
|
||||
<DecoratedInput
|
||||
className={{
|
||||
container: 'mt-1',
|
||||
}}
|
||||
value={fileName}
|
||||
onChange={(fileName) => setFileName(fileName)}
|
||||
ref={fileNameInputRef}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<div className="text-sm font-medium text-neutral">Preview:</div>
|
||||
{!isRecorderReady && (
|
||||
<div className="mt-1 w-full">
|
||||
<div className="flex h-64 w-full items-center justify-center gap-2 rounded-md bg-contrast text-base">
|
||||
<Icon type="camera" className="text-neutral-300" />
|
||||
Initializing...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={classNames('mt-1 w-full', capturedVideo && 'hidden')} ref={previewRef}></div>
|
||||
{capturedVideo && (
|
||||
<div className="mt-1 w-full">
|
||||
<video src={capturedVideoObjectURL} controls />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{!capturedVideo && !isRecording && (
|
||||
<Button
|
||||
primary
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
void startRecording()
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" />
|
||||
Start recording
|
||||
</Button>
|
||||
)}
|
||||
{!capturedVideo && isRecording && (
|
||||
<Button
|
||||
primary
|
||||
colorStyle="danger"
|
||||
className="flex items-center gap-2"
|
||||
onClick={async () => {
|
||||
const capturedVideo = await recorder.stop()
|
||||
setIsRecording(false)
|
||||
setCapturedVideo(capturedVideo)
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" />
|
||||
Stop recording
|
||||
</Button>
|
||||
)}
|
||||
{capturedVideo && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setCapturedVideo(undefined)
|
||||
setRecorder(new VideoRecorder(fileName))
|
||||
setIsRecorderReady(false)
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
<Button primary className="flex items-center gap-2" onClick={saveVideo}>
|
||||
<Icon type="upload" />
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(VideoCaptureModal)
|
||||
@@ -302,6 +302,7 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
||||
isFilesSmartView={isFilesSmartView}
|
||||
optionsSubtitle={optionsSubtitle}
|
||||
selectedTag={selectedTag}
|
||||
filesController={filesController}
|
||||
/>
|
||||
)}
|
||||
<SearchBar itemListController={itemListController} searchOptionsController={searchOptionsController} />
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import PhotoCaptureModal from '@/Components/CameraCaptureModal/PhotoCaptureModal'
|
||||
import VideoCaptureModal from '@/Components/CameraCaptureModal/VideoCaptureModal'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { PhotoRecorder } from '@/Controllers/Moments/PhotoRecorder'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
isDailyEntry: boolean
|
||||
isInFilesSmartView: boolean
|
||||
addButtonLabel: string
|
||||
addNewItem: () => void
|
||||
filesController: FilesController
|
||||
}
|
||||
|
||||
const AddItemMenuButton = ({
|
||||
filesController,
|
||||
isDailyEntry,
|
||||
addButtonLabel,
|
||||
isInFilesSmartView,
|
||||
addNewItem,
|
||||
}: Props) => {
|
||||
const addItemButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const [deviceHasCamera, setDeviceHasCamera] = useState(false)
|
||||
const [captureType, setCaptureType] = useState<'photo' | 'video'>()
|
||||
|
||||
useEffect(() => {
|
||||
const setCameraSupport = async () => {
|
||||
setDeviceHasCamera(await PhotoRecorder.isSupported())
|
||||
}
|
||||
|
||||
void setCameraSupport()
|
||||
}, [])
|
||||
|
||||
const canShowMenu = isInFilesSmartView && deviceHasCamera
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className={classNames(
|
||||
'hidden md:flex',
|
||||
'h-8 w-8 hover:brightness-125',
|
||||
'z-editor-title-bar ml-3 cursor-pointer items-center',
|
||||
`justify-center rounded-full border border-solid border-transparent ${
|
||||
isDailyEntry ? 'bg-danger text-danger-contrast' : 'bg-info text-info-contrast'
|
||||
}`,
|
||||
)}
|
||||
title={addButtonLabel}
|
||||
aria-label={addButtonLabel}
|
||||
onClick={() => {
|
||||
if (canShowMenu) {
|
||||
setIsMenuOpen((isOpen) => !isOpen)
|
||||
} else {
|
||||
addNewItem()
|
||||
}
|
||||
}}
|
||||
ref={addItemButtonRef}
|
||||
>
|
||||
<Icon type="add" size="custom" className="h-5 w-5" />
|
||||
</button>
|
||||
<Popover
|
||||
open={canShowMenu && isMenuOpen}
|
||||
anchorElement={addItemButtonRef.current}
|
||||
togglePopover={() => {
|
||||
setIsMenuOpen((isOpen) => !isOpen)
|
||||
}}
|
||||
side="bottom"
|
||||
align="center"
|
||||
className="py-2"
|
||||
>
|
||||
<Menu a11yLabel={'test'} isOpen={isMenuOpen}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
addNewItem()
|
||||
setIsMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
<Icon type="add" className="mr-2" />
|
||||
{addButtonLabel}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
setCaptureType('photo')
|
||||
setIsMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" className="mr-2" />
|
||||
Take photo
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
setCaptureType('video')
|
||||
setIsMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" className="mr-2" />
|
||||
Record video
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Popover>
|
||||
{captureType === 'photo' && (
|
||||
<PhotoCaptureModal
|
||||
filesController={filesController}
|
||||
close={() => {
|
||||
setCaptureType(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{captureType === 'video' && (
|
||||
<VideoCaptureModal
|
||||
filesController={filesController}
|
||||
close={() => {
|
||||
setCaptureType(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddItemMenuButton
|
||||
@@ -9,6 +9,8 @@ import { isTag, VectorIconNameOrEmoji } from '@standardnotes/snjs'
|
||||
import RoundIconButton from '@/Components/Button/RoundIconButton'
|
||||
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
|
||||
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import AddItemMenuButton from './AddItemMenuButton'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -19,6 +21,7 @@ type Props = {
|
||||
isFilesSmartView: boolean
|
||||
optionsSubtitle?: string
|
||||
selectedTag: AnyTag
|
||||
filesController: FilesController
|
||||
}
|
||||
|
||||
const ContentListHeader = ({
|
||||
@@ -30,6 +33,7 @@ const ContentListHeader = ({
|
||||
isFilesSmartView,
|
||||
optionsSubtitle,
|
||||
selectedTag,
|
||||
filesController,
|
||||
}: Props) => {
|
||||
const displayOptionsContainerRef = useRef<HTMLDivElement>(null)
|
||||
const displayOptionsButtonRef = useRef<HTMLButtonElement>(null)
|
||||
@@ -84,23 +88,15 @@ const ContentListHeader = ({
|
||||
|
||||
const AddButton = useMemo(() => {
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'hidden md:flex',
|
||||
'h-8 w-8 hover:brightness-125',
|
||||
'z-editor-title-bar ml-3 cursor-pointer items-center',
|
||||
`justify-center rounded-full border border-solid border-transparent ${
|
||||
isDailyEntry ? 'bg-danger text-danger-contrast' : 'bg-info text-info-contrast'
|
||||
}`,
|
||||
)}
|
||||
title={addButtonLabel}
|
||||
aria-label={addButtonLabel}
|
||||
onClick={addNewItem}
|
||||
>
|
||||
<Icon type="add" size="custom" className="h-5 w-5" />
|
||||
</button>
|
||||
<AddItemMenuButton
|
||||
isInFilesSmartView={isFilesSmartView}
|
||||
isDailyEntry={isDailyEntry}
|
||||
addButtonLabel={addButtonLabel}
|
||||
addNewItem={addNewItem}
|
||||
filesController={filesController}
|
||||
/>
|
||||
)
|
||||
}, [addButtonLabel, addNewItem, isDailyEntry])
|
||||
}, [addButtonLabel, addNewItem, filesController, isDailyEntry, isFilesSmartView])
|
||||
|
||||
const FolderName = useMemo(() => {
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import Icon from '@/Components/Icon/Icon'
|
||||
import { DropdownItem } from './DropdownItem'
|
||||
import StyledListboxButton from './StyledListboxButton'
|
||||
import StyledListboxOption from './StyledListboxOption'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
|
||||
type DropdownProps = {
|
||||
id: string
|
||||
@@ -14,7 +15,11 @@ type DropdownProps = {
|
||||
value: string
|
||||
onChange: (value: string, item: DropdownItem) => void
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
className?: {
|
||||
wrapper?: string
|
||||
button?: string
|
||||
popover?: string
|
||||
}
|
||||
fullWidth?: boolean
|
||||
portal?: boolean
|
||||
}
|
||||
@@ -63,12 +68,16 @@ const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
onChange(value, selectedItem)
|
||||
}
|
||||
|
||||
const wrapperClassName = className?.wrapper ?? ''
|
||||
const buttonClassName = className?.button ?? ''
|
||||
const popoverClassName = className?.popover ?? ''
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={wrapperClassName}>
|
||||
<VisuallyHidden id={labelId}>{label}</VisuallyHidden>
|
||||
<ListboxInput value={value} onChange={handleChange} aria-labelledby={labelId} disabled={disabled}>
|
||||
<StyledListboxButton
|
||||
className={`w-full ${!fullWidth ? 'md:w-fit' : ''}`}
|
||||
className={classNames('w-full', !fullWidth && 'md:w-fit', buttonClassName)}
|
||||
children={({ value, label, isExpanded }) => {
|
||||
const current = items.find((item) => item.value === value)
|
||||
const icon = current ? current?.icon : null
|
||||
@@ -82,7 +91,7 @@ const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ListboxPopover portal={portal} className="sn-dropdown sn-dropdown-popover">
|
||||
<ListboxPopover portal={portal} className={classNames('sn-dropdown sn-dropdown-popover', popoverClassName)}>
|
||||
<div className="sn-component">
|
||||
<ListboxList>
|
||||
{items.map((item) => (
|
||||
|
||||
@@ -62,6 +62,7 @@ export const IconNameToSvgMapping = {
|
||||
archive: icons.ArchiveIcon,
|
||||
asterisk: icons.AsteriskIcon,
|
||||
authenticator: icons.AuthenticatorIcon,
|
||||
camera: icons.CameraIcon,
|
||||
check: icons.CheckIcon,
|
||||
close: icons.CloseIcon,
|
||||
code: icons.CodeIcon,
|
||||
@@ -106,6 +107,7 @@ export const IconNameToSvgMapping = {
|
||||
tune: icons.TuneIcon,
|
||||
unarchive: icons.UnarchiveIcon,
|
||||
unpin: icons.UnpinIcon,
|
||||
upload: icons.UploadIcon,
|
||||
user: icons.UserIcon,
|
||||
view: icons.ViewIcon,
|
||||
warning: icons.WarningIcon,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { Fragment, FunctionComponent, ReactNode } from 'react'
|
||||
import { FunctionComponent, ReactNode } from 'react'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
@@ -9,16 +9,7 @@ type Props = {
|
||||
const ModalDialogButtons: FunctionComponent<Props> = ({ children, className }) => (
|
||||
<>
|
||||
<hr className="m-0 h-[1px] border-none bg-border" />
|
||||
<div className={classNames('flex items-center justify-end px-4 py-4', className)}>
|
||||
{children != undefined && Array.isArray(children)
|
||||
? children.map((child, idx, arr) => (
|
||||
<Fragment key={idx}>
|
||||
{child}
|
||||
{idx < arr.length - 1 ? <div className="min-w-3" /> : undefined}
|
||||
</Fragment>
|
||||
))
|
||||
: children}
|
||||
</div>
|
||||
<div className={classNames('flex items-center justify-end gap-3 px-4 py-4', className)}>{children}</div>
|
||||
</>
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user