Browse Source

Fix more components

Streamline mounting of components for file preview components.
pull/1/head
TheoryOfNekomata 1 year ago
parent
commit
325551c654
24 changed files with 676 additions and 398 deletions
  1. +15
    -16
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx
  2. +204
    -16
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioMiniFilePreview/index.tsx
  3. +7
    -8
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/BinaryFilePreview/index.tsx
  4. +10
    -32
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/FilePreview/index.tsx
  5. +90
    -103
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/FileSelectBox/index.tsx
  6. +122
    -50
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx
  7. +4
    -7
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/TextFilePreview/index.tsx
  8. +11
    -10
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx
  9. +10
    -4
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/blob/metadata.ts
  10. +10
    -0
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/image.ts
  11. +38
    -43
      packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/media.ts
  12. +2
    -2
      packages/web-kitchensink-reactnext/src/categories/blob/react/index.ts
  13. +0
    -2
      packages/web-kitchensink-reactnext/src/categories/freeform/react/components/MaskedTextInput/index.tsx
  14. +10
    -10
      packages/web-kitchensink-reactnext/src/categories/number/react/components/Spinner/index.tsx
  15. +12
    -0
      packages/web-kitchensink-reactnext/src/categories/number/react/components/Spinner/style.module.css
  16. +1
    -0
      packages/web-kitchensink-reactnext/src/categories/number/react/index.ts
  17. +0
    -1
      packages/web-kitchensink-reactnext/src/packages/react-refractor/index.tsx
  18. +17
    -0
      packages/web-kitchensink-reactnext/src/packages/react-utils/index.ts
  19. +49
    -32
      packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/SpectrogramCanvas/index.tsx
  20. +43
    -26
      packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveformCanvas/index.tsx
  21. +9
    -9
      packages/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx
  22. +9
    -6
      packages/web-kitchensink-reactnext/src/pages/categories/number/index.tsx
  23. +2
    -21
      packages/web-kitchensink-reactnext/src/utils/blob.ts
  24. +1
    -0
      packages/web-kitchensink-reactnext/tailwind.config.js

+ 15
- 16
packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx View File

@@ -14,16 +14,17 @@ import clsx from 'clsx';
import {SpectrogramCanvas, WaveformCanvas} from '@modal-soft/react-wavesurfer'; import {SpectrogramCanvas, WaveformCanvas} from '@modal-soft/react-wavesurfer';
import {Slider} from '@/categories/number/react'; import {Slider} from '@/categories/number/react';
import {KeyValueTable} from '@/categories/information/react'; import {KeyValueTable} from '@/categories/information/react';
import {useEnhanced} from '@modal-soft/react-utils';


type AudioFilePreviewDerivedComponent = HTMLAudioElement;
type AudioFilePreviewDerivedElement = HTMLAudioElement;


export interface AudioFilePreviewProps<F extends Partial<File> = Partial<File>> extends Omit<React.HTMLProps<AudioFilePreviewDerivedComponent>, 'controls'> {
export interface AudioFilePreviewProps<F extends Partial<File> = Partial<File>> extends Omit<React.HTMLProps<AudioFilePreviewDerivedElement>, 'controls'> {
file?: F; file?: F;
disabled?: boolean; disabled?: boolean;
enhanced?: boolean; enhanced?: boolean;
} }


export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponent, AudioFilePreviewProps>(({
export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedElement, AudioFilePreviewProps>(({
file, file,
style, style,
className, className,
@@ -58,16 +59,12 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
endSeek, endSeek,
setSeek, setSeek,
visualizationId, visualizationId,
formId,
} = useMediaControls<HTMLAudioElement>({ } = useMediaControls<HTMLAudioElement>({
controllerRef: forwardedRef, controllerRef: forwardedRef,
visualizationMode: 'waveform', visualizationMode: 'waveform',
}); });
const formId = React.useId();

const [enhanced, setEnhanced] = React.useState(false);
React.useEffect(() => {
setEnhanced(enhancedProp);
}, [enhancedProp]);
const { enhanced } = useEnhanced({ enhanced: enhancedProp });


if (!fileWithMetadata) { if (!fileWithMetadata) {
return null; return null;
@@ -90,11 +87,10 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
&& ( && (
<div <div
className="w-full h-full bg-black flex flex-col items-stretch" className="w-full h-full bg-black flex flex-col items-stretch"
data-testid="preview"
key={`${fileWithMetadata.url}:${fileWithMetadata.type}`}
> >
<div <div
className="w-full flex-auto relative aspect-video sm:aspect-auto" className="w-full flex-auto relative aspect-video sm:aspect-auto"
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`}
> >
<audio <audio
{...etcProps} {...etcProps}
@@ -104,9 +100,9 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
onDurationChange={refreshControls} onDurationChange={refreshControls}
onEnded={reset} onEnded={reset}
onTimeUpdate={updateSeekFromPlayback} onTimeUpdate={updateSeekFromPlayback}
data-testid="preview"
> >
<source <source
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`}
src={fileWithMetadata.url} src={fileWithMetadata.url}
type={fileWithMetadata.type} type={fileWithMetadata.type}
/> />
@@ -149,7 +145,7 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
'absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-primary/10 cursor-text opacity-0', 'absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-primary/10 cursor-text opacity-0',
'peer-checked/waveform:opacity-100', 'peer-checked/waveform:opacity-100',
)} )}
ref={mediaControllerRef}
audioRef={mediaControllerRef}
data-testid="preview" data-testid="preview"
barWidth={1} barWidth={1}
barGap={1} barGap={1}
@@ -199,7 +195,7 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
'absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-primary/10 pointer-events-none opacity-0', 'absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-primary/10 pointer-events-none opacity-0',
'peer-checked/waveform:opacity-100', 'peer-checked/waveform:opacity-100',
)} )}
ref={mediaControllerRef}
audioRef={mediaControllerRef}
data-testid="preview" data-testid="preview"
barWidth={1} barWidth={1}
barGap={1} barGap={1}
@@ -216,7 +212,7 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
)} )}
</div> </div>
{enhanced && ( {enhanced && (
<div className="w-full flex-shrink-0 h-10 flex gap-4 items-center">
<div className="w-full flex-shrink-0 h-10 flex gap-4 items-center bg-[#000000] px-3">
<div <div
className="py-1 w-14 h-full flex-shrink-0 text-primary flex items-center justify-center" className="py-1 w-14 h-full flex-shrink-0 text-primary flex items-center justify-center"
> >
@@ -297,8 +293,10 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
} }
> >
<Slider <Slider
className="bg-negative text-base flex-auto"
className="text-base flex-auto"
ref={seekRef} ref={seekRef}
min={0}
max={durationDisplay}
onMouseDown={startSeek} onMouseDown={startSeek}
onMouseUp={endSeek} onMouseUp={endSeek}
onChange={setSeek} onChange={setSeek}
@@ -342,6 +340,7 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
className: 'font-bold', className: 'font-bold',
valueProps: { valueProps: {
ref: filenameRef, ref: filenameRef,
title: fileWithMetadata.name,
children: fileWithMetadata.name, children: fileWithMetadata.name,
}, },
}, },


+ 204
- 16
packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioMiniFilePreview/index.tsx View File

@@ -1,26 +1,214 @@
import * as React from 'react'; import * as React from 'react';
import {AudioFile, getMimeTypeDescription} from '@/utils/blob';
import {formatFileSize, formatNumeral, formatSecondsDurationPrecise} from '@/utils/numeral';
import {useAudioControls} from '@tesseract-design/web-blob-react';
import {augmentAudioFile} from '@/utils/blob';
import theme from '@/styles/theme';
import {useMediaControls} from '../../hooks/interactive';
import {useFileMetadata, useFileUrl} from '@/categories/blob/react';
import clsx from 'clsx';
import {SpectrogramCanvas, WaveformCanvas} from '@modal-soft/react-wavesurfer';
import {useEnhanced} from '@modal-soft/react-utils';


export interface AudioMiniFilePreviewProps {
file: AudioFile;
type AudioMiniFilePreviewDerivedElement = HTMLAudioElement;

export interface AudioMiniFilePreviewProps<F extends Partial<File> = Partial<File>> extends Omit<React.HTMLProps<AudioMiniFilePreviewDerivedElement>, 'controls'> {
file?: F;
disabled?: boolean;
enhanced?: boolean;
} }


export const AudioMiniFilePreview: React.FC<AudioMiniFilePreviewProps> = ({
file: f,
}) => {
export const AudioMiniFilePreview = React.forwardRef<AudioMiniFilePreviewDerivedElement, AudioMiniFilePreviewProps>(({
file,
style,
className,
enhanced: enhancedProp = false,
disabled = false,
...etcProps
}, forwardedRef) => {
const { fileWithUrl } = useFileUrl({
file,
});
const { fileWithMetadata, error } = useFileMetadata({
file: fileWithUrl,
augmentFunction: augmentAudioFile,
});
const { const {
mountRef,
playMedia,
mediaControllerRef,
refreshControls,
reset,
updateSeekFromPlayback,
isPlaying, isPlaying,
} = useAudioControls({ file: f });
isSeeking,
currentTimeDisplay = 0,
seekTimeDisplay = 0,
durationDisplay = 0,
isSeekTimeCountingDown,
adjustVolume,
volumeRef,
handleAction,
filenameRef,
seekRef,
startSeek,
endSeek,
setSeek,
visualizationId,
formId,
} = useMediaControls<HTMLAudioElement>({
controllerRef: forwardedRef,
visualizationMode: 'waveform',
});
const { enhanced } = useEnhanced({ enhanced: enhancedProp });

if (!fileWithMetadata) {
return null;
}

const finalSeekTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - seekTimeDisplay) : seekTimeDisplay;
const finalCurrentTimeDisplay = isSeekTimeCountingDown ? (durationDisplay - currentTimeDisplay) : currentTimeDisplay;


return ( return (
<div <div
className="absolute top-0 left-0 w-full h-full cursor-pointer"
ref={mountRef}
onClick={playMedia}
/>
className={clsx(
'flex flex-col sm:grid sm:grid-cols-3 gap-8 w-full',
className,
)}
style={style}
>
<div className="sm:h-full relative col-span-2">
{
typeof fileWithMetadata.url === 'string'
&& (
<div
className="w-full h-full bg-black flex flex-col items-stretch"
data-testid="preview"
>
<div
className="w-full flex-auto relative aspect-video sm:aspect-auto"
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`}
>
<audio
{...etcProps}
controls={!enhanced}
ref={mediaControllerRef}
onLoadedMetadata={refreshControls}
onDurationChange={refreshControls}
onEnded={reset}
onTimeUpdate={updateSeekFromPlayback}
>
<source
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`}
src={fileWithMetadata.url}
type={fileWithMetadata.type}
/>
Audio playback not supported.
</audio>
{enhanced && (
<>
<div className="flex justify-end w-full h-full gap-4 absolute top-0 right-0 z-[5] px-4">
<div className="contents">
<input
type="radio"
name="visualizationMode"
value="waveform"
className="sr-only peer/waveform"
defaultChecked
id={`${visualizationId}-waveform`}
/>
<label
htmlFor={`${visualizationId}-waveform`}
className={clsx(
'relative z-[5]',
'h-12 flex items-center justify-center leading-none gap-4 select-none',
'text-primary cursor-pointer',
'peer-focus/waveform:text-secondary',
'peer-active/waveform:text-tertiary',
'peer-checked/waveform:text-tertiary',
'peer-disabled/waveform:text-primary peer-disabled/waveform:cursor-not-allowed peer-disabled/waveform:opacity-50',
)}
>
<span
className={clsx(
'flex items-center uppercase font-bold h-full w-full whitespace-nowrap overflow-hidden text-ellipsis',
)}
>
Waveform
</span>
</label>
<WaveformCanvas
className={clsx(
'absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-primary/10 cursor-text opacity-0',
'peer-checked/waveform:opacity-100',
)}
audioRef={mediaControllerRef}
data-testid="preview"
barWidth={1}
barGap={1}
progressColor={`rgb(${theme['color-primary']})`}
waveColor={`rgb(${theme['color-primary'].split(' ').map((c) => Math.floor(Number(c) / 2)).join(' ')})`}
interact
// waveColor={`rgb(${theme.primary})`}
// barHeight={4}
// minPxPerSec={20000}
// hideScrollbar
// autoCenter
// autoScroll
/>
</div>
<div
className="contents"
>
<input
type="radio"
name="visualizationMode"
value="spectrum"
className="sr-only peer/waveform"
id={`${visualizationId}-spectrum`}
/>
<label
htmlFor={`${visualizationId}-spectrum`}
className={clsx(
'relative z-[5]',
'h-12 flex items-center justify-center leading-none gap-4 select-none',
'text-primary cursor-pointer',
'peer-focus/waveform:text-secondary',
'peer-active/waveform:text-tertiary',
'peer-checked/waveform:text-tertiary',
'peer-disabled/waveform:text-primary peer-disabled/waveform:cursor-not-allowed peer-disabled/waveform:opacity-50',
)}
>
<span
className={clsx(
'flex items-center uppercase font-bold h-full w-full whitespace-nowrap overflow-hidden text-ellipsis',
)}
>
Spectrum
</span>
</label>
<SpectrogramCanvas
className={clsx(
'absolute w-full sm:h-full top-0 left-0 block object-center object-contain flex-auto aspect-video sm:aspect-auto bg-primary/10 pointer-events-none opacity-0',
'peer-checked/waveform:opacity-100',
)}
audioRef={mediaControllerRef}
data-testid="preview"
barWidth={1}
barGap={1}
waveColor={`rgb(${theme['color-primary']})`}
cursorWidth={2}
minPxPerSec={20000}
hideScrollbar
autoCenter
autoScroll
/>
</div>
</div>
</>
)}
</div>
</div>
)
}
</div>
</div>
); );
};
});

AudioMiniFilePreview.displayName = 'AudioMiniFilePreview';

+ 7
- 8
packages/web-kitchensink-reactnext/src/categories/blob/react/components/BinaryFilePreview/index.tsx View File

@@ -5,15 +5,16 @@ import {useFileMetadata, useFileUrl} from '@/categories/blob/react';
import clsx from 'clsx'; import clsx from 'clsx';
import {KeyValueTable} from '@/categories/information/react'; import {KeyValueTable} from '@/categories/information/react';
import {BinaryDataCanvas} from '@modal-soft/react-binary-data-canvas'; import {BinaryDataCanvas} from '@modal-soft/react-binary-data-canvas';
import {useEnhanced} from '@modal-soft/react-utils';


type BinaryFilePreviewDerivedComponent = HTMLDivElement;
type BinaryFilePreviewDerivedElement = HTMLDivElement;


export interface BinaryFilePreviewProps<F extends Partial<File> = Partial<File>> extends React.HTMLProps<BinaryFilePreviewDerivedComponent> {
export interface BinaryFilePreviewProps<F extends Partial<File> = Partial<File>> extends React.HTMLProps<BinaryFilePreviewDerivedElement> {
file?: F; file?: F;
enhanced?: boolean; enhanced?: boolean;
} }


export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedComponent, BinaryFilePreviewProps>(({
export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedElement, BinaryFilePreviewProps>(({
file, file,
className, className,
style, style,
@@ -25,11 +26,7 @@ export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedCompon
file: fileWithUrl, file: fileWithUrl,
augmentFunction: augmentBinaryFile, augmentFunction: augmentBinaryFile,
}); });

const [enhanced, setEnhanced] = React.useState(false);
React.useEffect(() => {
setEnhanced(enhancedProp);
}, [enhancedProp]);
const { enhanced } = useEnhanced({ enhanced: enhancedProp });


if (!fileWithMetadata) { if (!fileWithMetadata) {
return null; return null;
@@ -42,6 +39,7 @@ export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedCompon
className, className,
)} )}
style={style} style={style}
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`}
> >
<div className="h-full relative"> <div className="h-full relative">
<div className="absolute top-0 left-0 w-full h-full"> <div className="absolute top-0 left-0 w-full h-full">
@@ -54,6 +52,7 @@ export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedCompon
role="presentation" role="presentation"
className="w-full h-full select-none overflow-hidden text-xs" className="w-full h-full select-none overflow-hidden text-xs"
ref={forwardedRef} ref={forwardedRef}
key={`${fileWithMetadata.url}:${fileWithMetadata.type}`}
> >
<BinaryDataCanvas <BinaryDataCanvas
arrayBuffer={fileWithMetadata.metadata?.contents} arrayBuffer={fileWithMetadata.metadata?.contents}


+ 10
- 32
packages/web-kitchensink-reactnext/src/categories/blob/react/components/FilePreview/index.tsx View File

@@ -4,7 +4,7 @@ import { ImageFilePreview } from '../ImageFilePreview';
import { AudioFilePreview } from '../AudioFilePreview'; import { AudioFilePreview } from '../AudioFilePreview';
import { VideoFilePreview } from '../VideoFilePreview'; import { VideoFilePreview } from '../VideoFilePreview';
import { BinaryFilePreview } from '../BinaryFilePreview'; import { BinaryFilePreview } from '../BinaryFilePreview';
import {AugmentedFile, augmentFile, ContentType, getContentType} from '@/utils/blob';
import { ContentType, getContentType } from '@/utils/blob';


const FILE_PREVIEW_COMPONENTS: Record<ContentType, React.ElementType> = { const FILE_PREVIEW_COMPONENTS: Record<ContentType, React.ElementType> = {
[ContentType.TEXT]: TextFilePreview, [ContentType.TEXT]: TextFilePreview,
@@ -14,43 +14,16 @@ const FILE_PREVIEW_COMPONENTS: Record<ContentType, React.ElementType> = {
[ContentType.BINARY]: BinaryFilePreview, [ContentType.BINARY]: BinaryFilePreview,
}; };


const useFilePreviews = (fileList?: FileList) => {
const [selectedFiles, setSelectedFiles] = React.useState([] as AugmentedFile[]);
React.useEffect(() => {
const loadFilePreviews = async (fileList: FileList) => {
const files = Array.from(fileList);
return Promise.all(
files.map(async (f) => {
const augmentedFile = await augmentFile(f);
if (augmentedFile.resolvedType === ContentType.TEXT && augmentedFile.metadata?.scheme) {
await import(`prismjs/components/prism-${augmentedFile.metadata.scheme}`);
}
return augmentedFile;
})
);
}

if (fileList) {
loadFilePreviews(fileList).then((fileResult) => {
setSelectedFiles(fileResult);
});
}
}, [fileList]);

return React.useMemo(() => ({
files: selectedFiles,
}), [selectedFiles]);
};

export interface FilePreviewProps { export interface FilePreviewProps {
fileList?: FileList; fileList?: FileList;
className?: string;
} }


export const FilePreview: React.FC<FilePreviewProps> = ({ export const FilePreview: React.FC<FilePreviewProps> = ({
fileList, fileList,
className,
}) => { }) => {
const { files } = useFilePreviews(fileList);
if (files.length < 1) {
if ((fileList?.length ?? 0) < 1) {
return null; return null;
} }


@@ -59,6 +32,11 @@ export const FilePreview: React.FC<FilePreviewProps> = ({
const FilePreviewComponent = FILE_PREVIEW_COMPONENTS[contentType] ?? BinaryFilePreview; const FilePreviewComponent = FILE_PREVIEW_COMPONENTS[contentType] ?? BinaryFilePreview;


return ( return (
<FilePreviewComponent file={f} />
<FilePreviewComponent
key={contentType}
file={f}
className={className}
enhanced
/>
); );
}; };

+ 90
- 103
packages/web-kitchensink-reactnext/src/categories/blob/react/components/FileSelectBox/index.tsx View File

@@ -1,10 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import {AugmentedFile, augmentFile, ContentType, getMimeTypeDescription} from '@/utils/blob';
import { FilePreview as FilePreviewComponent} from '../FilePreview';
import {ContentType, FileWithResolvedContentType, getMimeTypeDescription} from '@/utils/blob';
import { FilePreview } from '../FilePreview';
import {formatFileSize} from '@/utils/numeral'; import {formatFileSize} from '@/utils/numeral';
import {AudioMiniFilePreview} from '@tesseract-design/web-blob-react'; import {AudioMiniFilePreview} from '@tesseract-design/web-blob-react';
import {delegateTriggerChangeEvent} from '@/utils/event'; import {delegateTriggerChangeEvent} from '@/utils/event';
import clsx from 'clsx'; import clsx from 'clsx';
import {useEnhanced} from '@modal-soft/react-utils';


export interface FileButtonProps extends Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style' | 'label' | 'list'> { export interface FileButtonProps extends Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style' | 'label' | 'list'> {
/** /**
@@ -30,69 +31,46 @@ export interface FileButtonProps extends Omit<React.HTMLProps<HTMLInputElement>,
hiddenLabel?: boolean, hiddenLabel?: boolean,
} }


const useFilePreviews = (fileList?: FileList) => {
const [selectedFiles, setSelectedFiles] = React.useState([] as AugmentedFile[]);
React.useEffect(() => {
const loadFilePreviews = async (fileList: FileList) => {
const files = Array.from(fileList);
return Promise.all(
files.map(async (f) => {
const augmentedFile = await augmentFile(f);
if (augmentedFile.resolvedType === ContentType.TEXT && augmentedFile.metadata?.scheme) {
await import(`prismjs/components/prism-${augmentedFile.metadata.scheme}`);
}
return augmentedFile;
})
);
}

if (fileList) {
loadFilePreviews(fileList).then((fileResult) => {
setSelectedFiles(fileResult);
});
}
}, [fileList]);

return React.useMemo(() => ({
files: selectedFiles,
}), [selectedFiles]);
}

const FilePreviewGrid = ({ const FilePreviewGrid = ({
fileList,
fileList: files,
}: { fileList?: FileList }) => { }: { fileList?: FileList }) => {
const { files } = useFilePreviews(fileList);
if (!files) {
return null;
}


return ( return (
<div className="w-full h-full overflow-auto -mx-4 px-4"> <div className="w-full h-full overflow-auto -mx-4 px-4">
<div className="w-full grid gap-4 grid-cols-3"> <div className="w-full grid gap-4 grid-cols-3">
{files.map((f, i) => (
<div
data-testid="selectedFileItem"
key={i}
className={`w-full aspect-square rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10`}
title={[f.name, getMimeTypeDescription(f.type), formatFileSize(f.size)].join(', ')}
>
{
f.resolvedType === ContentType.IMAGE
&& typeof f.metadata?.previewUrl === 'string'
&& (
<img
className="block w-full h-full object-center object-cover"
src={f.metadata.previewUrl}
alt={f.name}
data-testid="preview"
/>
)
}
{
f.resolvedType === ContentType.AUDIO
&& (
<AudioMiniFilePreview file={f} />
)
}
</div>
))}
{Array.from(files).map((file: File, i) => {
const f = file as unknown as FileWithResolvedContentType;
return (
<div
data-testid="selectedFileItem"
key={i}
className={`w-full aspect-square rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10`}
title={[f.name, getMimeTypeDescription(f.type), formatFileSize(f.size)].join(', ')}
>
{
f.resolvedType === ContentType.IMAGE
&& typeof f?.url === 'string'
&& (
<img
className="block w-full h-full object-center object-cover"
src={f.url}
alt={f.name}
data-testid="preview"
/>
)
}
{
f.resolvedType === ContentType.AUDIO
&& (
<AudioMiniFilePreview file={f} />
)
}
</div>
);
})}
</div> </div>
</div> </div>
) )
@@ -105,7 +83,7 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>
hint = '', hint = '',
border = false, border = false,
block = false, block = false,
enhanced = false,
enhanced: enhancedProp = false,
hiddenLabel = false, hiddenLabel = false,
multiple = false, multiple = false,
onChange, onChange,
@@ -116,34 +94,39 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>
}: FileButtonProps, }: FileButtonProps,
forwardedRef, forwardedRef,
) => { ) => {
const [renderEnhanced, setRenderEnhanced] = React.useState(false);
const { enhanced } = useEnhanced({ enhanced: enhancedProp });
const [fileList, setFileList] = React.useState<FileList>(); const [fileList, setFileList] = React.useState<FileList>();
const [lastSelectedFileAt, setLastSelectedFileAt] = React.useState<number>();
const [lastUpdated, setLastUpdated] = React.useState<number>();
const defaultRef = React.useRef<HTMLInputElement>(null); const defaultRef = React.useRef<HTMLInputElement>(null);
const ref = forwardedRef ?? defaultRef; const ref = forwardedRef ?? defaultRef;
const labelId = React.useId(); const labelId = React.useId();
const defaultId = React.useId(); const defaultId = React.useId();
const id = idProp ?? defaultId; const id = idProp ?? defaultId;


const addFile: React.ChangeEventHandler<HTMLInputElement> = (e) => {
if (!enhanced) {
onChange?.(e);
return;
const doSetFileList: React.ChangeEventHandler<HTMLInputElement> = (e) => {
if (enhancedProp) {
setFileList(e.currentTarget.files as FileList);
setLastUpdated(Date.now());
} }

setFileList(e.currentTarget.files as FileList);
setLastSelectedFileAt(Date.now());
onChange?.(e); onChange?.(e);
}; };


const deleteFiles: React.MouseEventHandler<HTMLButtonElement> = () => {
if (typeof ref === 'object' && ref.current) {
ref.current.value = '';
setFileList(undefined);
setTimeout(() => {
delegateTriggerChangeEvent(ref.current as HTMLInputElement);
})
const doClearFileList: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault();
if (!(typeof ref === 'object' && ref)) {
return;
} }
const { current } = ref;
if (!current) {
return;
}

current.value = '';
setFileList(undefined);
setLastUpdated(Date.now());
setTimeout(() => {
delegateTriggerChangeEvent(current);
});
}; };


const cancelEvent = (e: React.DragEvent) => { const cancelEvent = (e: React.DragEvent) => {
@@ -153,20 +136,26 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>


const handleDropZone: React.DragEventHandler<HTMLDivElement> = async (e) => { const handleDropZone: React.DragEventHandler<HTMLDivElement> = async (e) => {
cancelEvent(e); cancelEvent(e);
if (!(typeof ref === 'object' && ref)) {
return;
}
const { current } = ref;
if (!current) {
return;
}
const { dataTransfer } = e; const { dataTransfer } = e;
if (typeof ref === 'object' && ref.current) {
const { files } = dataTransfer;
if (!(files && files.length > 0)) {
return;
}
setFileList(ref.current.files = files);
delegateTriggerChangeEvent(ref.current);
const { files } = dataTransfer;
if (!(files && files.length > 0)) {
return;
} }
setFileList(current.files = files);
setLastUpdated(Date.now());
setTimeout(() => {
delegateTriggerChangeEvent(current);
});
} }


React.useEffect(() => {
setRenderEnhanced(enhanced);
}, [enhanced]);
const filesCount = React.useMemo(() => fileList?.length ?? 0, [fileList]);


return ( return (
<div <div
@@ -195,10 +184,10 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>
className={clsx( className={clsx(
'peer', 'peer',
{ {
'sr-only': renderEnhanced,
'sr-only': enhanced,
} }
)} )}
onChange={addFile}
onChange={doSetFileList}
multiple={multiple} multiple={multiple}
data-testid="input" data-testid="input"
aria-labelledby={label ? `${labelId}` : undefined} aria-labelledby={label ? `${labelId}` : undefined}
@@ -222,8 +211,8 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>
) )
} }
{ {
(fileList?.length ?? 0) < 1
&& renderEnhanced
filesCount < 1
&& enhanced
&& hint && hint
&& ( && (
<div <div
@@ -237,14 +226,13 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>
) )
} }
{ {
(fileList?.length ?? 0) > 0
&& renderEnhanced
filesCount > 0
&& enhanced
&& ( && (
<>
<div className={`absolute top-0 left-0 w-full h-full pointer-events-none pb-12 box-border overflow-hidden pt-8`}>
<React.Fragment key={lastUpdated}>
<div className={`sm:absolute top-0 left-0 w-full h-full pointer-events-none pb-12 box-border overflow-hidden pt-8`}>
<div <div
className={`pointer-events-auto w-full h-full px-4 pb-4 box-border`} className={`pointer-events-auto w-full h-full px-4 pb-4 box-border`}
key={lastSelectedFileAt}
> >
{ {
multiple multiple
@@ -259,11 +247,10 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>
className={`w-full h-full`} className={`w-full h-full`}
> >
<div data-testid="selectedFileItem" className={`h-full w-full p-4 box-border rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10`}> <div data-testid="selectedFileItem" className={`h-full w-full p-4 box-border rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10`}>
<div className="relative">
<FilePreviewComponent
fileList={fileList}
/>
</div>
<FilePreview
className="w-full h-full relative"
fileList={fileList}
/>
</div> </div>
</div> </div>
) )
@@ -288,18 +275,18 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>
<button <button
data-testid="clear" data-testid="clear"
type="button" type="button"
onClick={deleteFiles}
onClick={doClearFileList}
className="flex w-full h-full bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none focus:outline-0" className="flex w-full h-full bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none focus:outline-0"
> >
<span <span
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded" className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
> >
{multiple ? 'Clear' : 'Delete'}
Clear
</span> </span>
</button> </button>
</div> </div>
</div> </div>
</>
</React.Fragment>
) )
} }
{ {


+ 122
- 50
packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx View File

@@ -4,16 +4,114 @@ import {formatFileSize, formatNumeral} from '@/utils/numeral';
import clsx from 'clsx'; import clsx from 'clsx';
import {useFileMetadata, useFileUrl, useImageControls} from '@/categories/blob/react'; import {useFileMetadata, useFileUrl, useImageControls} from '@/categories/blob/react';
import {KeyValueTable} from '@/categories/information/react'; import {KeyValueTable} from '@/categories/information/react';
import {useEnhanced} from '@modal-soft/react-utils';


type ImageFilePreviewDerivedComponent = HTMLImageElement;
type RgbTuple = [number, number, number];


export interface ImageFilePreviewProps<F extends Partial<File> = Partial<File>> extends Omit<React.HTMLProps<ImageFilePreviewDerivedComponent>, 'src' | 'alt'> {
type SwatchDerivedElement = HTMLInputElement;

export interface SwatchProps extends Omit<React.HTMLProps<SwatchDerivedElement>, 'color'> {
color: RgbTuple;
mode?: 'rgb' | 'hsl';
}

export const useSwatchControls = () => {
const id = React.useId();
const copyColor: React.ReactEventHandler<SwatchDerivedElement> = React.useCallback(async (e) => {
const { value } = e.currentTarget;
await window.navigator.clipboard.writeText(value);
}, []);
return React.useMemo(() => ({
id,
copyColor,
}), [id, copyColor]);
};

export const Swatch = React.forwardRef<SwatchDerivedElement, SwatchProps>(({
color,
mode = 'rgb',
className,
style,
...etcProps
}, forwardedRef) => {
const { id, copyColor } = useSwatchControls();
const colorValue = `${mode}(${color.join(', ')})`;

return (
<span
className={clsx(
'inline-block align-middle',
className,
)}
style={style}
>
<input
{...etcProps}
ref={forwardedRef}
type="text"
value={colorValue}
className="sr-only select-all peer"
readOnly
id={id}
onSelect={copyColor}
/>
<label
className={clsx(
'relative rounded ring-secondary/50 whitespace-nowrap inline-block align-top leading-none cursor-pointer', // todo eyedropper cursor
'peer-focus:outline-0 peer-focus:ring-4',
'peer-active:ring-tertiary/50',
'peer-disabled:opacity-50 peer-disabled:cursor-not-allowed',
)}
title={colorValue}
htmlFor={id}
>
<span
className="inline-block w-5 h-5 align-middle border border-[#ffffff]"
>
<span
className="block w-full h-full border border-[#000000]"
style={{
backgroundColor: `${mode}(${color.join(' ')})`,
}}
/>
</span>
<span className="tabular-nums text-xs sr-only">
{
color
.map((c) => c
.toString()
.padStart(4, ' ')
.split('')
.map((cc, j) => (
<span
key={`${cc}:${j}`}
className={clsx({
'opacity-0': cc === ' ',
})}
>
{j === 0 && ' '}
{cc === ' ' && j > 0 ? '0' : cc}
</span>
))
)
}
</span>
</label>
</span>
);
});

Swatch.displayName = 'Swatch';

type ImageFilePreviewDerivedElement = HTMLImageElement;

export interface ImageFilePreviewProps<F extends Partial<File> = Partial<File>> extends Omit<React.HTMLProps<ImageFilePreviewDerivedElement>, 'src' | 'alt'> {
file?: F; file?: F;
disabled?: boolean; disabled?: boolean;
enhanced?: boolean; enhanced?: boolean;
} }


export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponent, ImageFilePreviewProps>(({
export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedElement, ImageFilePreviewProps>(({
file, file,
className, className,
style, style,
@@ -26,6 +124,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
file: fileWithUrl as File, file: fileWithUrl as File,
augmentFunction: augmentImageFile, augmentFunction: augmentImageFile,
}); });

const { const {
fullScreen, fullScreen,
handleAction, handleAction,
@@ -35,10 +134,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
forwardedRef, forwardedRef,
}); });


const [enhanced, setEnhanced] = React.useState(false);
React.useEffect(() => {
setEnhanced(enhancedProp);
}, [enhancedProp]);
const { enhanced } = useEnhanced({ enhanced: enhancedProp });


if (!(fileWithMetadata)) { if (!(fileWithMetadata)) {
return null; return null;
@@ -66,7 +162,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
className={clsx( className={clsx(
'block h-full max-w-full object-center bg-[#000000]', 'block h-full max-w-full object-center bg-[#000000]',
{ {
'object-contain fixed w-full top-0 left-0': fullScreen,
'object-contain fixed w-full top-0 left-0 z-[3]': fullScreen,
'object-cover w-full': !fullScreen, 'object-cover w-full': !fullScreen,
}, },
)} )}
@@ -94,6 +190,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
className: 'font-bold', className: 'font-bold',
valueProps: { valueProps: {
ref: filenameRef, ref: filenameRef,
title: fileWithMetadata.name,
children: fileWithMetadata.name, children: fileWithMetadata.name,
}, },
}, },
@@ -128,47 +225,22 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
&& { && {
key: 'Palette', key: 'Palette',
valueProps: { valueProps: {
className: 'mt-1',
children: fileWithMetadata.metadata?.palette.map((rgb, i) => (
<React.Fragment
key={rgb.join(' ')}
>
{i > 0 && ' '}
<span
className="whitespace-nowrap inline-block align-top leading-none"
title={`rgb(${rgb.join(', ')})`}
>
<span
className="inline-block w-5 h-5 align-middle border border-[#ffffff] ring-1 ring-[#000000]"
style={{
backgroundColor: `rgb(${rgb.join(' ')})`,
}}
className: '-ml-4 pl-4 pt-4 ',
children: (
<div className="flex flex-wrap gap-3">
{fileWithMetadata.metadata?.palette.map((rgb, i) => (
<React.Fragment
key={rgb.join(' ')}
>
{i > 0 && ' '}
<Swatch
color={rgb}
mode="rgb"
/> />
<span className="tabular-nums text-xs sr-only">
{
rgb
.map((c) => c
.toString()
.padStart(4, ' ')
.split('')
.map((cc, j) => (
<span
key={`${rgb.join(',')}:${c}:${j}`}
className={clsx({
'opacity-0': cc === ' ',
})}
>
{j === 0 && ' '}
{cc === ' ' && j > 0 ? '0' : cc}
</span>
))
)
.flat()
}
</span>
</span>
</React.Fragment>
)),
</React.Fragment>
))}
</div>
),
}, },
}, },
]} ]}
@@ -190,7 +262,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
name="action" name="action"
value="toggleFullScreen" value="toggleFullScreen"
className={clsx( className={clsx(
'h-12 flex bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'h-12 flex text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'focus:outline-0', 'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed', 'disabled:opacity-50 disabled:cursor-not-allowed',
{ {
@@ -210,7 +282,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
name="action" name="action"
value="download" value="download"
className={clsx( className={clsx(
'h-12 flex bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'h-12 flex text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'focus:outline-0', 'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed', 'disabled:opacity-50 disabled:cursor-not-allowed',
)} )}


+ 4
- 7
packages/web-kitchensink-reactnext/src/categories/blob/react/components/TextFilePreview/index.tsx View File

@@ -4,7 +4,8 @@ import {useFileMetadata, useFileUrl} from '@/categories/blob/react';
import {augmentTextFile, getMimeTypeDescription} from '@/utils/blob'; import {augmentTextFile, getMimeTypeDescription} from '@/utils/blob';
import clsx from 'clsx'; import clsx from 'clsx';
import {KeyValueTable} from '@/categories/information/react'; import {KeyValueTable} from '@/categories/information/react';
import {Refractor} from '../../../../../packages/react-refractor';
import {Refractor} from '@modal-soft/react-refractor';
import {useEnhanced} from '@modal-soft/react-utils';


type TextFilePreviewDerivedComponent = HTMLDivElement; type TextFilePreviewDerivedComponent = HTMLDivElement;


@@ -25,11 +26,7 @@ export const TextFilePreview = React.forwardRef<TextFilePreviewDerivedComponent,
file: fileWithUrl, file: fileWithUrl,
augmentFunction: augmentTextFile, augmentFunction: augmentTextFile,
}); });

const [enhanced, setEnhanced] = React.useState(false);
React.useEffect(() => {
setEnhanced(enhancedProp);
}, [enhancedProp]);
const { enhanced } = useEnhanced({ enhanced: enhancedProp });


if (!fileWithMetadata) { if (!fileWithMetadata) {
return null; return null;
@@ -54,12 +51,12 @@ export const TextFilePreview = React.forwardRef<TextFilePreviewDerivedComponent,
role="presentation" role="presentation"
className="w-full h-full select-none overflow-hidden text-xs" className="w-full h-full select-none overflow-hidden text-xs"
ref={forwardedRef} ref={forwardedRef}
key={`${fileWithMetadata.url}:${fileWithMetadata.type}`}
> >
<Refractor <Refractor
code={fileWithMetadata.metadata.contents} code={fileWithMetadata.metadata.contents}
language={fileWithMetadata.metadata.scheme} language={fileWithMetadata.metadata.scheme}
lineNumbers={Boolean(fileWithMetadata.metadata.scheme)} lineNumbers={Boolean(fileWithMetadata.metadata.scheme)}
maxLineNumber={20}
/> />
</div> </div>
) )


+ 11
- 10
packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx View File

@@ -5,6 +5,7 @@ import {useFileMetadata, useFileUrl, useMediaControls} from '@tesseract-design/w
import clsx from 'clsx'; import clsx from 'clsx';
import {Slider} from '@tesseract-design/web-number-react'; import {Slider} from '@tesseract-design/web-number-react';
import {KeyValueTable} from '@/categories/information/react'; import {KeyValueTable} from '@/categories/information/react';
import {useEnhanced} from '@modal-soft/react-utils';


type VideoFilePreviewDerivedComponent = HTMLVideoElement; type VideoFilePreviewDerivedComponent = HTMLVideoElement;


@@ -46,15 +47,12 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
mediaControllerRef, mediaControllerRef,
handleAction, handleAction,
filenameRef, filenameRef,
formId,
} = useMediaControls<HTMLVideoElement>({ } = useMediaControls<HTMLVideoElement>({
controllerRef: forwardedRef, controllerRef: forwardedRef,
}); });
const formId = React.useId();


const [enhanced, setEnhanced] = React.useState(false);
React.useEffect(() => {
setEnhanced(enhancedProp);
}, [enhancedProp]);
const { enhanced } = useEnhanced({ enhanced: enhancedProp });


if (!fileWithMetadata) { if (!fileWithMetadata) {
return null; return null;
@@ -78,10 +76,10 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
<div <div
className="w-full h-full bg-black flex flex-col items-stretch" className="w-full h-full bg-black flex flex-col items-stretch"
data-testid="preview" data-testid="preview"
key={`${fileWithMetadata.url}:${fileWithMetadata.type}`}
> >
<div <div
className="w-full flex-auto relative" className="w-full flex-auto relative"
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`}
> >
<video <video
{...etcProps} {...etcProps}
@@ -95,10 +93,10 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
controls={!enhanced} controls={!enhanced}
> >
<source <source
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`}
src={fileWithMetadata.url} src={fileWithMetadata.url}
type={fileWithMetadata.type} type={fileWithMetadata.type}
/> />
Video playback not supported.
</video> </video>
<button <button
className="absolute w-full h-full top-0 left-0" className="absolute w-full h-full top-0 left-0"
@@ -114,7 +112,7 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
</button> </button>
</div> </div>
{enhanced && ( {enhanced && (
<div className="w-full flex-shrink-0 h-10 flex gap-4 items-center">
<div className="w-full flex-shrink-0 h-10 flex gap-4 items-center bg-[#000000] px-3">
<div <div
className="py-1 w-14 h-full flex-shrink-0 text-primary flex items-center justify-center" className="py-1 w-14 h-full flex-shrink-0 text-primary flex items-center justify-center"
> >
@@ -195,8 +193,10 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
} }
> >
<Slider <Slider
className="flex-auto bg-negative text-base"
className="flex-auto text-base"
ref={seekRef} ref={seekRef}
min={0}
max={durationDisplay}
onMouseDown={startSeek} onMouseDown={startSeek}
onMouseUp={endSeek} onMouseUp={endSeek}
onChange={setSeek} onChange={setSeek}
@@ -242,6 +242,7 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
className: 'font-bold', className: 'font-bold',
valueProps: { valueProps: {
ref: filenameRef, ref: filenameRef,
title: fileWithMetadata.name,
children: fileWithMetadata.name, children: fileWithMetadata.name,
}, },
}, },
@@ -294,7 +295,7 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
name="action" name="action"
value="download" value="download"
className={clsx( className={clsx(
'h-12 flex bg-negative text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'h-12 flex text-primary disabled:text-primary focus:text-secondary active:text-tertiary items-center justify-center leading-none gap-4 select-none',
'focus:outline-0', 'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed', 'disabled:opacity-50 disabled:cursor-not-allowed',
)} )}


+ 10
- 4
packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/blob/metadata.ts View File

@@ -12,11 +12,14 @@ export const useFileUrl = (options: UseFileUrlOptions) => {


React.useEffect(() => { React.useEffect(() => {
if (!file) { if (!file) {
setFileWithUrl(undefined);
if (fileWithUrl) {
setFileWithUrl(undefined);
}
setLoading(false); setLoading(false);
return; return;
} }


setFileWithUrl(undefined);
setLoading(true); setLoading(true);
addDataUrl(file) addDataUrl(file)
.then((fileWithUrl) => { .then((fileWithUrl) => {
@@ -24,7 +27,6 @@ export const useFileUrl = (options: UseFileUrlOptions) => {
setLoading(false); setLoading(false);
}) })
.catch(() => { .catch(() => {
setFileWithUrl(file);
setLoading(false); setLoading(false);
}); });
}, [file]); }, [file]);
@@ -40,7 +42,7 @@ export interface UseFileMetadataOptions<T extends Partial<File> = Partial<File>,
augmentFunction: (file: U) => Promise<T>; augmentFunction: (file: U) => Promise<T>;
} }


export const useFileMetadata = <T extends Partial<File>>(options: UseFileMetadataOptions<T>) => {
export const useFileMetadata = <T extends Partial<FileWithDataUrl>>(options: UseFileMetadataOptions<T>) => {
const { file, augmentFunction } = options; const { file, augmentFunction } = options;
const [fileWithMetadata, setFileWithMetadata] = React.useState<T | undefined>(file as T | undefined); const [fileWithMetadata, setFileWithMetadata] = React.useState<T | undefined>(file as T | undefined);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
@@ -48,11 +50,14 @@ export const useFileMetadata = <T extends Partial<File>>(options: UseFileMetadat


React.useEffect(() => { React.useEffect(() => {
if (!file) { if (!file) {
setFileWithMetadata(undefined);
if (fileWithMetadata) {
setFileWithMetadata(undefined);
}
setLoading(false); setLoading(false);
return; return;
} }


setFileWithMetadata(undefined);
setLoading(true); setLoading(true);
setError(undefined); setError(undefined);
augmentFunction(file) augmentFunction(file)
@@ -61,6 +66,7 @@ export const useFileMetadata = <T extends Partial<File>>(options: UseFileMetadat
setLoading(false); setLoading(false);
}) })
.catch((error) => { .catch((error) => {
setFileWithMetadata(file as T);
setError(error); setError(error);
setLoading(false); setLoading(false);
}); });


+ 10
- 0
packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/image.ts View File

@@ -13,6 +13,16 @@ export const useImageControls = (options = {} as UseImageControlsOptions) => {
const imageRef = forwardedRef ?? defaultRef; const imageRef = forwardedRef ?? defaultRef;
const filenameRef = React.useRef<HTMLElement>(null); const filenameRef = React.useRef<HTMLElement>(null);


React.useEffect(() => {
if (fullScreen) {
window.document.body.style.overflow = 'hidden';
}

if (!fullScreen) {
window.document.body.style.overflow = '';
}
}, [fullScreen]);

const toggleFullScreen = React.useCallback(() => { const toggleFullScreen = React.useCallback(() => {
setFullScreen((b) => !b); setFullScreen((b) => !b);
}, []); }, []);


+ 38
- 43
packages/web-kitchensink-reactnext/src/categories/blob/react/hooks/interactive/media.ts View File

@@ -10,7 +10,6 @@ export interface UseMediaControlsOptions<T extends HTMLMediaElement> {
export const useMediaControls = <T extends HTMLMediaElement>({ export const useMediaControls = <T extends HTMLMediaElement>({
controllerRef: forwardedRef, controllerRef: forwardedRef,
actionFormKey = 'action' as const, actionFormKey = 'action' as const,
visualizationMode: initialVisualizationMode,
}: UseMediaControlsOptions<T>) => { }: UseMediaControlsOptions<T>) => {
const defaultRef = React.useRef<T>(null); const defaultRef = React.useRef<T>(null);
const ref = forwardedRef ?? defaultRef; const ref = forwardedRef ?? defaultRef;
@@ -18,6 +17,7 @@ export const useMediaControls = <T extends HTMLMediaElement>({
const volumeRef = React.useRef<HTMLInputElement>(null); const volumeRef = React.useRef<HTMLInputElement>(null);
const filenameRef = React.useRef<HTMLElement>(null); const filenameRef = React.useRef<HTMLElement>(null);
const visualizationId = React.useId(); const visualizationId = React.useId();
const formId = React.useId();
const [isPlaying, setIsPlaying] = React.useState(false); const [isPlaying, setIsPlaying] = React.useState(false);
const [isSeeking, setIsSeeking] = React.useState(false); const [isSeeking, setIsSeeking] = React.useState(false);
const [currentTimeDisplay, setCurrentTimeDisplay] = React.useState<number>(); const [currentTimeDisplay, setCurrentTimeDisplay] = React.useState<number>();
@@ -45,10 +45,9 @@ export const useMediaControls = <T extends HTMLMediaElement>({
}, []); }, []);


const setSeek: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => { const setSeek: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => {
if (!(typeof ref === 'object' && ref !== null)) {
if (!(typeof ref === 'object' && ref)) {
return; return;
} }

const { current: mediaController } = ref; const { current: mediaController } = ref;
if (!mediaController) { if (!mediaController) {
return; return;
@@ -65,12 +64,10 @@ export const useMediaControls = <T extends HTMLMediaElement>({
}, [ref, isSeeking, doSetSeek]); }, [ref, isSeeking, doSetSeek]);


const endSeek: React.MouseEventHandler<HTMLInputElement> = React.useCallback((e) => { const endSeek: React.MouseEventHandler<HTMLInputElement> = React.useCallback((e) => {
if (!(typeof ref === 'object' && ref !== null)) {
if (!(typeof ref === 'object' && ref)) {
return; return;
} }

const { current: mediaController } = ref; const { current: mediaController } = ref;

if (!mediaController) { if (!mediaController) {
return; return;
} }
@@ -96,7 +93,7 @@ export const useMediaControls = <T extends HTMLMediaElement>({
const currentTime = videoElement.currentTime; const currentTime = videoElement.currentTime;
setCurrentTimeDisplay(currentTime); setCurrentTimeDisplay(currentTime);


if (!seekRef.current) {
if (!(typeof seekRef === 'object' && seekRef)) {
return; return;
} }
const { current: seek } = seekRef; const { current: seek } = seekRef;
@@ -104,24 +101,32 @@ export const useMediaControls = <T extends HTMLMediaElement>({
return; return;
} }
seek.value = String(currentTime); seek.value = String(currentTime);
}, [isSeeking]);
}, [isSeeking, seekRef]);


const toggleSeekTimeCountMode = React.useCallback(() => { const toggleSeekTimeCountMode = React.useCallback(() => {
setIsSeekTimeCountingDown((b) => !b); setIsSeekTimeCountingDown((b) => !b);
}, []); }, []);


const download = React.useCallback(() => { const download = React.useCallback(() => {
if (!(typeof ref === 'object' && ref?.current !== null)) {
if (!(typeof ref === 'object' && ref)) {
return;
}
const { current: mediaController } = ref;
if (!mediaController) {
return; return;
} }


if (!(typeof filenameRef === 'object' && filenameRef?.current !== null)) {
if (!(typeof filenameRef === 'object' && filenameRef)) {
return;
}
const { current: filename } = filenameRef;
if (!filename) {
return; return;
} }


const downloadLink = window.document.createElement('a'); const downloadLink = window.document.createElement('a');
downloadLink.download = filenameRef.current.textContent ?? 'image';
downloadLink.href = ref.current.currentSrc;
downloadLink.download = filename.textContent ?? 'media';
downloadLink.href = mediaController.currentSrc;
downloadLink.addEventListener('click', () => { downloadLink.addEventListener('click', () => {
downloadLink.remove(); downloadLink.remove();
}); });
@@ -136,7 +141,6 @@ export const useMediaControls = <T extends HTMLMediaElement>({
}), [togglePlayback, toggleSeekTimeCountMode, download]); }), [togglePlayback, toggleSeekTimeCountMode, download]);


const handleAction: React.FormEventHandler<HTMLFormElement> = React.useCallback((e) => { const handleAction: React.FormEventHandler<HTMLFormElement> = React.useCallback((e) => {
e.preventDefault();
e.preventDefault(); e.preventDefault();
const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement }; const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement };
const formData = getFormValues( const formData = getFormValues(
@@ -151,77 +155,66 @@ export const useMediaControls = <T extends HTMLMediaElement>({
}, [actions, actionFormKey]); }, [actions, actionFormKey]);


const adjustVolume: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => { const adjustVolume: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((e) => {
if (!(typeof ref === 'object' && ref !== null)) {
if (!(typeof ref === 'object' && ref)) {
return; return;
} }
if (!ref.current) {
const { current: mediaController } = ref;
if (!mediaController) {
return; return;
} }

const { value } = e.currentTarget; const { value } = e.currentTarget;
ref.current.volume = Number(value);
mediaController.volume = Number(value);
}, [ref]); }, [ref]);


React.useEffect(() => { React.useEffect(() => {
if (!(typeof ref === 'object' && ref !== null)) {
if (!(typeof ref === 'object' && ref)) {
return; return;
} }
if (!ref.current) {
const { current: mediaController } = ref;
if (!mediaController) {
return; return;
} }


if (isPlaying) { if (isPlaying) {
void ref.current.play();
void mediaController.play();
return return
} }


ref.current.pause();
mediaController.pause();
}, [isPlaying, ref]); }, [isPlaying, ref]);


React.useEffect(() => { React.useEffect(() => {
if (!seekRef.current) {
if (!(typeof seekRef === 'object' && seekRef)) {
return; return;
} }

const { current: seek } = seekRef; const { current: seek } = seekRef;
if (!seek) { if (!seek) {
return; return;
} }


seek.value = String(currentTimeDisplay); seek.value = String(currentTimeDisplay);
}, [currentTimeDisplay]);
}, [currentTimeDisplay, seekRef]);


React.useEffect(() => { React.useEffect(() => {
if (!seekRef.current) {
return;
}

const { current: seek } = seekRef;
if (!seek) {
if (!(typeof ref === 'object' && ref)) {
return; return;
} }

seek.max = String(durationDisplay);
}, [durationDisplay]);

React.useEffect(() => {
if (!volumeRef.current) {
const { current: mediaController } = ref;
if (!mediaController) {
return; return;
} }


if (!(typeof ref === 'object' && ref !== null)) {
if (!(typeof volumeRef === 'object' && volumeRef)) {
return; return;
} }
if (!ref.current) {
const { current: volume } = volumeRef;
if (!volume) {
return; return;
} }


const { current: mediaController } = ref;
const { current: volume } = volumeRef;
volume.value = String(mediaController.volume); volume.value = String(mediaController.volume);
}, [ref]);
}, [ref, volumeRef]);


return React.useMemo(() => ({ return React.useMemo(() => ({
seekRef, seekRef,
@@ -243,6 +236,7 @@ export const useMediaControls = <T extends HTMLMediaElement>({
handleAction, handleAction,
filenameRef, filenameRef,
visualizationId, visualizationId,
formId,
}), [ }), [
refreshControls, refreshControls,
isPlaying, isPlaying,
@@ -261,5 +255,6 @@ export const useMediaControls = <T extends HTMLMediaElement>({
handleAction, handleAction,
filenameRef, filenameRef,
visualizationId, visualizationId,
formId,
]); ]);
}; };

+ 2
- 2
packages/web-kitchensink-reactnext/src/categories/blob/react/index.ts View File

@@ -1,7 +1,7 @@
export * from './components/AudioFilePreview'; export * from './components/AudioFilePreview';
//export * from './components/AudioMiniFilePreview';
export * from './components/AudioMiniFilePreview';
export * from './components/BinaryFilePreview'; export * from './components/BinaryFilePreview';
//export * from './components/FileSelectBox';
export * from './components/FileSelectBox';
export * from './components/ImageFilePreview'; export * from './components/ImageFilePreview';
export * from './components/TextFilePreview'; export * from './components/TextFilePreview';
export * from './components/VideoFilePreview'; export * from './components/VideoFilePreview';


+ 0
- 2
packages/web-kitchensink-reactnext/src/categories/freeform/react/components/MaskedTextInput/index.tsx View File

@@ -41,8 +41,6 @@ export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInp


/** /**
* Component for inputting textual values. * Component for inputting textual values.
*
* This component supports multiline input and adjusts its layout accordingly.
*/ */
export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, MaskedTextInputProps>( export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, MaskedTextInputProps>(
( (


+ 10
- 10
packages/web-kitchensink-reactnext/src/categories/number/react/components/Spinner/index.tsx View File

@@ -1,10 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import * as TextControlBase from '@tesseract-design/web-base-textcontrol'; import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
import clsx from 'clsx'; import clsx from 'clsx';
import styles from './style.module.css';


type MaskedTextInputDerivedElement = HTMLInputElement;
type SpinnerDerivedElement = HTMLInputElement;


export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInputDerivedElement>, 'size' | 'type' | 'label'> {
export interface SpinnerProps extends Omit<React.HTMLProps<SpinnerDerivedElement>, 'size' | 'type' | 'label'> {
/** /**
* Short textual description indicating the nature of the component's value. * Short textual description indicating the nature of the component's value.
*/ */
@@ -40,11 +41,9 @@ export interface MaskedTextInputProps extends Omit<React.HTMLProps<MaskedTextInp
} }


/** /**
* Component for inputting textual values.
*
* This component supports multiline input and adjusts its layout accordingly.
* Component for inputting numeric values.
*/ */
export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, MaskedTextInputProps>(
export const Spinner = React.forwardRef<SpinnerDerivedElement, SpinnerProps>(
( (
{ {
label, label,
@@ -57,7 +56,7 @@ export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, M
hiddenLabel = false, hiddenLabel = false,
className, className,
...etcProps ...etcProps
}: MaskedTextInputProps,
}: SpinnerProps,
ref, ref,
) => { ) => {
const labelId = React.useId(); const labelId = React.useId();
@@ -78,12 +77,13 @@ export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, M
{...etcProps} {...etcProps}
ref={ref} ref={ref}
aria-labelledby={labelId} aria-labelledby={labelId}
type="password"
type="number"
data-testid="input" data-testid="input"
className={clsx( className={clsx(
'bg-negative rounded-inherit w-full peer block',
'bg-negative rounded-inherit w-full peer block tabular-nums',
'focus:outline-0', 'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed', 'disabled:opacity-50 disabled:cursor-not-allowed',
styles['spinner'],
{ {
'text-xxs': size === 'small', 'text-xxs': size === 'small',
'text-xs': size === 'medium', 'text-xs': size === 'medium',
@@ -195,4 +195,4 @@ export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, M
}, },
); );


MaskedTextInput.displayName = 'MaskedTextInput';
Spinner.displayName = 'Spinner';

+ 12
- 0
packages/web-kitchensink-reactnext/src/categories/number/react/components/Spinner/style.module.css View File

@@ -0,0 +1,12 @@
.spinner {
position: relative;
}

.spinner::-webkit-inner-spin-button {
position: absolute;
top: 0;
right: 0;
height: 100%;
width: 1.5rem;
z-index: 2;
}

+ 1
- 0
packages/web-kitchensink-reactnext/src/categories/number/react/index.ts View File

@@ -1 +1,2 @@
export * from './components/Slider'; export * from './components/Slider';
export * from './components/Spinner';

+ 0
- 1
packages/web-kitchensink-reactnext/src/packages/react-refractor/index.tsx View File

@@ -44,7 +44,6 @@ export const Refractor = React.forwardRef<PrismDerivedElement, PrismProps>(({
{language} {language}
</div> </div>
{actions} {actions}

</div> </div>
)} )}
<div <div


+ 17
- 0
packages/web-kitchensink-reactnext/src/packages/react-utils/index.ts View File

@@ -0,0 +1,17 @@
import * as React from 'react';

export interface UseEnhancedOptions {
enhanced: boolean;
}

export const useEnhanced = (options: UseEnhancedOptions) => {
const { enhanced: enhancedProp } = options;
const [enhanced, setEnhanced] = React.useState(false);
React.useEffect(() => {
setEnhanced(enhancedProp);
}, [enhancedProp]);

return React.useMemo(() => ({
enhanced,
}), [enhanced]);
};

+ 49
- 32
packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/SpectrogramCanvas/index.tsx View File

@@ -3,15 +3,16 @@ import {WaveSurferOptions} from 'wavesurfer.js';
import clsx from 'clsx'; import clsx from 'clsx';
import {getFormValues} from '@theoryofnekomata/formxtra'; import {getFormValues} from '@theoryofnekomata/formxtra';


type SpectrogramCanvasDerivedComponent = HTMLAudioElement;
type SpectrogramCanvasDerivedElement = HTMLDivElement;


export interface SpectrogramCanvasProps export interface SpectrogramCanvasProps
extends React.HTMLProps<SpectrogramCanvasDerivedComponent>,
extends React.HTMLProps<SpectrogramCanvasDerivedElement>,
Omit<WaveSurferOptions, 'waveColor' | 'plugins' | 'height' | 'media' | 'container' | 'fillParent' | 'url' | 'autoplay' | 'renderFunction'> { Omit<WaveSurferOptions, 'waveColor' | 'plugins' | 'height' | 'media' | 'container' | 'fillParent' | 'url' | 'autoplay' | 'renderFunction'> {
waveColor?: string; waveColor?: string;
audioRef?: React.Ref<HTMLAudioElement>;
} }


export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent, SpectrogramCanvasProps>(({
export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedElement, SpectrogramCanvasProps>(({
className, className,
children, children,
controls, controls,
@@ -36,17 +37,16 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon
sampleRate, sampleRate,
splitChannels, splitChannels,
normalize, normalize,
audioRef,
...etcProps ...etcProps
}, forwardedRef) => { }, forwardedRef) => {
const [isPlaying, setIsPlaying] = React.useState(false); const [isPlaying, setIsPlaying] = React.useState(false);
const defaultRef = React.useRef<HTMLAudioElement>(null);
const ref = forwardedRef ?? defaultRef;
const containerRef = React.useRef<HTMLDivElement>(null);
const defaultRef = React.useRef<SpectrogramCanvasDerivedElement>(null);
const containerRef = forwardedRef ?? defaultRef;
const waveSurferRef = React.useRef<any>(null); const waveSurferRef = React.useRef<any>(null);
const cursorRef = React.useRef<HTMLDivElement>(null); const cursorRef = React.useRef<HTMLDivElement>(null);


const handleAction: React.FormEventHandler<HTMLFormElement> = (e) => { const handleAction: React.FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
e.preventDefault(); e.preventDefault();
const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement }; const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement };
const formData = getFormValues( const formData = getFormValues(
@@ -66,24 +66,36 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon
}; };


React.useEffect(() => { React.useEffect(() => {
if (!(typeof audioRef === 'object' && audioRef)) {
return;
}
const { current: media } = audioRef;
if (!media) {
return;
}

if (!(typeof containerRef === 'object' && containerRef)) {
return;
}
const { current: container } = containerRef; const { current: container } = containerRef;
const media = typeof ref === 'object' ? ref?.current : null;
const { current: waveSurferCurrent } = waveSurferRef;
if (!container) {
return;
}

if (!(typeof cursorRef === 'object' && cursorRef)) {
return;
}
const { current: cursor } = cursorRef;
if (!cursor) {
return;
}

const handleTimeUpdate = (e: Event) => { const handleTimeUpdate = (e: Event) => {
const thisMedia = e.currentTarget as HTMLAudioElement; const thisMedia = e.currentTarget as HTMLAudioElement;
if (cursorRef.current) {
cursorRef.current.style.width = `${(thisMedia?.currentTime ?? 0) / (thisMedia?.duration ?? 1) * 100}%`;
}
cursor.style.width = `${(thisMedia?.currentTime ?? 0) / (thisMedia?.duration ?? 1) * 100}%`;
}; };


const load = async (ref: React.Ref<HTMLAudioElement>) => {
if (!(typeof ref === 'object' && ref?.current)) {
return;
}

if (!(typeof containerRef === 'object' && containerRef?.current)) {
return;
}
const load = async (media: HTMLAudioElement, container: HTMLElement) => {
const { default: WaveSurfer } = await import('wavesurfer.js'); const { default: WaveSurfer } = await import('wavesurfer.js');
const { default: Spectrogram, } = await import('wavesurfer.js/dist/plugins/spectrogram'); const { default: Spectrogram, } = await import('wavesurfer.js/dist/plugins/spectrogram');
const dummyContainer = window.document.createElement('div'); const dummyContainer = window.document.createElement('div');
@@ -115,7 +127,7 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon
normalize, normalize,
plugins: [], plugins: [],
cursorWidth, cursorWidth,
media: media ?? undefined,
media,
}); });


let colorMap: Array<[number, number, number, number]> = []; let colorMap: Array<[number, number, number, number]> = [];
@@ -130,10 +142,10 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon
} }
waveSurferInstance.registerPlugin( waveSurferInstance.registerPlugin(
Spectrogram.create({ Spectrogram.create({
container: containerRef.current,
container,
labels: true, labels: true,
labelsColor: 'rgb(0 0 0/0)', labelsColor: 'rgb(0 0 0/0)',
height: containerRef.current.clientHeight,
height: container.clientHeight,
colorMap, colorMap,
}), }),
) )
@@ -147,12 +159,17 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon
} }
dummyContainer.remove(); dummyContainer.remove();
}); });
await waveSurferInstance.load(ref.current.currentSrc);
waveSurferInstance.setTime(ref.current.currentTime);
waveSurferRef.current = waveSurferInstance;
media!.addEventListener('timeupdate', handleTimeUpdate);
await waveSurferInstance.load(media.currentSrc);
waveSurferInstance.setTime(media.currentTime);
media.addEventListener('timeupdate', handleTimeUpdate);
return waveSurferInstance;
}; };
void load(ref);
const { current: waveSurferCurrent } = waveSurferRef;

void load(media, container).then((i) => {
waveSurferRef.current = i;
});

return () => { return () => {
if (waveSurferCurrent) { if (waveSurferCurrent) {
(waveSurferCurrent as unknown as Record<string, Function>).destroy(); (waveSurferCurrent as unknown as Record<string, Function>).destroy();
@@ -160,13 +177,12 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon
if (container) { if (container) {
container.innerHTML = ''; container.innerHTML = '';
} }
if (!media) {
return;
if (media) {
media.removeEventListener('timeupdate', handleTimeUpdate);
} }
media.removeEventListener('timeupdate', handleTimeUpdate);
}; };
}, [ }, [
ref,
audioRef,
autoPlay, autoPlay,
waveColor, waveColor,
progressColor, progressColor,
@@ -188,6 +204,7 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon
splitChannels, splitChannels,
normalize, normalize,
cursorWidth, cursorWidth,
containerRef,
]); ]);


return ( return (


+ 43
- 26
packages/web-kitchensink-reactnext/src/packages/react-wavesurfer/WaveformCanvas/index.tsx View File

@@ -3,13 +3,15 @@ import {WaveSurferOptions} from 'wavesurfer.js';
import clsx from 'clsx'; import clsx from 'clsx';
import {getFormValues} from '@theoryofnekomata/formxtra'; import {getFormValues} from '@theoryofnekomata/formxtra';


type SpectrogramCanvasDerivedComponent = HTMLAudioElement;
type WaveformCanvasDerivedElement = HTMLDivElement;


export interface WaveformCanvasProps export interface WaveformCanvasProps
extends React.HTMLProps<SpectrogramCanvasDerivedComponent>,
Omit<WaveSurferOptions, 'plugins' | 'height' | 'media' | 'container' | 'fillParent' | 'url' | 'autoplay' | 'renderFunction'> {}
extends React.HTMLProps<WaveformCanvasDerivedElement>,
Omit<WaveSurferOptions, 'plugins' | 'height' | 'media' | 'container' | 'fillParent' | 'url' | 'autoplay' | 'renderFunction'> {
audioRef?: React.Ref<HTMLAudioElement>;
}


export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent, WaveformCanvasProps>(({
export const WaveformCanvas = React.forwardRef<WaveformCanvasDerivedElement, WaveformCanvasProps>(({
className, className,
children, children,
controls, controls,
@@ -34,16 +36,15 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
sampleRate, sampleRate,
splitChannels, splitChannels,
normalize, normalize,
audioRef,
...etcProps ...etcProps
}, forwardedRef) => { }, forwardedRef) => {
const [isPlaying, setIsPlaying] = React.useState(false); const [isPlaying, setIsPlaying] = React.useState(false);
const defaultRef = React.useRef<HTMLAudioElement>(null);
const ref = forwardedRef ?? defaultRef;
const containerRef = React.useRef<HTMLDivElement>(null);
const defaultRef = React.useRef<WaveformCanvasDerivedElement>(null);
const containerRef = forwardedRef ?? defaultRef;
const waveSurferRef = React.useRef<any>(null); const waveSurferRef = React.useRef<any>(null);


const handleAction: React.FormEventHandler<HTMLFormElement> = (e) => { const handleAction: React.FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
e.preventDefault(); e.preventDefault();
const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement }; const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement };
const formData = getFormValues( const formData = getFormValues(
@@ -63,21 +64,26 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
}; };


React.useEffect(() => { React.useEffect(() => {
const { current: container } = containerRef;
const media = typeof ref === 'object' ? ref?.current : null;
const { current: waveSurferCurrent } = waveSurferRef;
if (!(typeof audioRef === 'object' && audioRef)) {
return;
}
const { current: media } = audioRef;
if (!media) {
return;
}


const load = async (ref: React.Ref<HTMLAudioElement>) => {
if (!(typeof ref === 'object' && ref?.current)) {
return;
}
if (!(typeof containerRef === 'object' && containerRef)) {
return;
}
const { current: container } = containerRef;
if (!container) {
return;
}


if (!(typeof containerRef === 'object' && containerRef?.current)) {
return;
}
const { default: WaveSurfer } = await import('wavesurfer.js');
const load = async (media: HTMLAudioElement, container: HTMLElement) => {
const {default: WaveSurfer} = await import('wavesurfer.js');
const waveSurferInstance = WaveSurfer.create({ const waveSurferInstance = WaveSurfer.create({
container: containerRef.current,
container,
height: 'auto', height: 'auto',
autoplay: autoPlay, autoplay: autoPlay,
fillParent: true, fillParent: true,
@@ -101,7 +107,8 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
splitChannels, splitChannels,
normalize, normalize,
cursorWidth, cursorWidth,
media: media ?? undefined,
plugins: [],
media,
}); });
waveSurferInstance.on('ready', () => { waveSurferInstance.on('ready', () => {
if (!container) { if (!container) {
@@ -111,11 +118,20 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
container.removeChild(container.children[0]); container.removeChild(container.children[0]);
} }
}); });
await waveSurferInstance.load(ref.current.currentSrc);
waveSurferInstance.setTime(ref.current.currentTime);
waveSurferRef.current = waveSurferInstance;
await waveSurferInstance.load(media.currentSrc);
waveSurferInstance.setTime(media.currentTime);
return waveSurferInstance;
}; };
void load(ref);

const { current: waveSurferCurrent } = waveSurferRef;
load(media, container)
.then((i) => {
waveSurferRef.current = i;
})
.catch((error) => {
console.log(error);
});

return () => { return () => {
if (waveSurferCurrent) { if (waveSurferCurrent) {
(waveSurferCurrent as unknown as Record<string, Function>).destroy(); (waveSurferCurrent as unknown as Record<string, Function>).destroy();
@@ -125,7 +141,7 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
} }
}; };
}, [ }, [
ref,
audioRef,
autoPlay, autoPlay,
waveColor, waveColor,
progressColor, progressColor,
@@ -147,6 +163,7 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
splitChannels, splitChannels,
normalize, normalize,
cursorWidth, cursorWidth,
containerRef,
]); ]);


return ( return (


+ 9
- 9
packages/web-kitchensink-reactnext/src/pages/categories/blob/index.tsx View File

@@ -169,15 +169,15 @@ const BlobPage: NextPage = () => {
</Section> </Section>
<Section title="FileSelectBox"> <Section title="FileSelectBox">
<Subsection title="Single File"> <Subsection title="Single File">
{/*<BlobReact.FileSelectBox*/}
{/* border*/}
{/* enhanced*/}
{/* label="Primary Image"*/}
{/* hint="Select any files here"*/}
{/* block*/}
{/* className="h-64"*/}
{/* onChange={(e) => console.log(e.currentTarget.files)}*/}
{/*/>*/}
<BlobReact.FileSelectBox
border
enhanced
label="Primary Image"
hint="Select any files here"
block
className="sm:h-96"
onChange={(e) => console.log(e.currentTarget.files)}
/>
</Subsection> </Subsection>
</Section> </Section>
</DefaultLayout> </DefaultLayout>


+ 9
- 6
packages/web-kitchensink-reactnext/src/pages/categories/number/index.tsx View File

@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { NextPage } from 'next'; import { NextPage } from 'next';
import {Slider} from '@/categories/number/react';
import * as TesseractNumber from '@tesseract-design/web-number-react';
import {Section, Subsection} from '@/components/Section'; import {Section, Subsection} from '@/components/Section';


const NumberPage: NextPage = () => { const NumberPage: NextPage = () => {
@@ -8,21 +8,24 @@ const NumberPage: NextPage = () => {
<main className="my-16 md:my-32"> <main className="my-16 md:my-32">
<Section title="Spinner"> <Section title="Spinner">
<Subsection title="Default"> <Subsection title="Default">
TODO

<input type="number" />
<TesseractNumber.Spinner
min={-100}
max={100}
step="any"
label="Step"
/>
</Subsection> </Subsection>
</Section> </Section>
<Section title="Slider"> <Section title="Slider">
<Subsection title="Default"> <Subsection title="Default">
<Slider
<TesseractNumber.Slider
min={-100} min={-100}
max={100} max={100}
tickMarks={[{ label: 'low', value: 25, }, 50]} tickMarks={[{ label: 'low', value: 25, }, 50]}
/> />
</Subsection> </Subsection>
<Subsection title="Vertical"> <Subsection title="Vertical">
<Slider
<TesseractNumber.Slider
min={-100} min={-100}
max={100} max={100}
tickMarks={[{ label: 'low', value: 25, }, 50]} tickMarks={[{ label: 'low', value: 25, }, 50]}


+ 2
- 21
packages/web-kitchensink-reactnext/src/utils/blob.ts View File

@@ -1,7 +1,7 @@
import * as mimeTypes from 'mime-types'; import * as mimeTypes from 'mime-types';
import {getTextMetadata, TextMetadata} from '@modal-soft/text-utils'; import {getTextMetadata, TextMetadata} from '@modal-soft/text-utils';
import {getImageMetadata, ImageMetadata} from '@modal-soft/image-utils'; import {getImageMetadata, ImageMetadata} from '@modal-soft/image-utils';
import {AudioMetadata, getAudioMetadata} from '@modal-soft/audio-utils';
import {getAudioMetadata, AudioMetadata} from '@modal-soft/audio-utils';
import {getVideoMetadata, VideoFileMetadata} from '@modal-soft/video-utils'; import {getVideoMetadata, VideoFileMetadata} from '@modal-soft/video-utils';


const MIME_TYPE_DESCRIPTIONS = { const MIME_TYPE_DESCRIPTIONS = {
@@ -137,7 +137,7 @@ export const addDataUrl = async (f: Partial<File>): Promise<Partial<FileWithData
return f; return f;
} }


interface FileWithResolvedContentType extends Partial<FileWithDataUrl> {
export interface FileWithResolvedContentType extends Partial<FileWithDataUrl> {
resolvedType: ContentType; resolvedType: ContentType;
} }


@@ -204,22 +204,3 @@ export const augmentVideoFile = async <T extends Partial<FileWithDataUrl>>(file:
fileMutable.metadata = await getVideoMetadata(file.url); fileMutable.metadata = await getVideoMetadata(file.url);
return fileMutable as unknown as VideoFile; return fileMutable as unknown as VideoFile;
}; };

export type AugmentedFile = TextFile | ImageFile | AudioFile | VideoFile | BinaryFile;

export const augmentFile = async (f: File): Promise<AugmentedFile> => {
const contentType = getContentType(f.type, f.name);
switch (contentType) {
case ContentType.TEXT:
return augmentTextFile(f);
case ContentType.IMAGE:
return augmentImageFile(f);
case ContentType.AUDIO:
return augmentAudioFile(f);
case ContentType.VIDEO:
return augmentVideoFile(f);
default:
break;
}
return augmentBinaryFile(f);
};

+ 1
- 0
packages/web-kitchensink-reactnext/tailwind.config.js View File

@@ -34,6 +34,7 @@ module.exports = {
'code-global': 'rgb(var(--color-code-global))', 'code-global': 'rgb(var(--color-code-global))',
'current': 'currentcolor', 'current': 'currentcolor',
'inherit': 'inherit', 'inherit': 'inherit',
'transparent': 'transparent',
}, },
extend: { extend: {
fontSize: { fontSize: {


Loading…
Cancel
Save