Browse Source

Fix more components

Streamline mounting of components for file preview components.
pull/1/head
TheoryOfNekomata 11 months 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 {Slider} from '@/categories/number/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;
disabled?: boolean;
enhanced?: boolean;
}

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

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

if (!fileWithMetadata) {
return null;
@@ -90,11 +87,10 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
&& (
<div
className="w-full h-full bg-black flex flex-col items-stretch"
data-testid="preview"
key={`${fileWithMetadata.url}:${fileWithMetadata.type}`}
>
<div
className="w-full flex-auto relative aspect-video sm:aspect-auto"
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`}
>
<audio
{...etcProps}
@@ -104,9 +100,9 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
onDurationChange={refreshControls}
onEnded={reset}
onTimeUpdate={updateSeekFromPlayback}
data-testid="preview"
>
<source
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`}
src={fileWithMetadata.url}
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',
'peer-checked/waveform:opacity-100',
)}
ref={mediaControllerRef}
audioRef={mediaControllerRef}
data-testid="preview"
barWidth={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',
'peer-checked/waveform:opacity-100',
)}
ref={mediaControllerRef}
audioRef={mediaControllerRef}
data-testid="preview"
barWidth={1}
barGap={1}
@@ -216,7 +212,7 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
)}
</div>
{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
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
className="bg-negative text-base flex-auto"
className="text-base flex-auto"
ref={seekRef}
min={0}
max={durationDisplay}
onMouseDown={startSeek}
onMouseUp={endSeek}
onChange={setSeek}
@@ -342,6 +340,7 @@ export const AudioFilePreview = React.forwardRef<AudioFilePreviewDerivedComponen
className: 'font-bold',
valueProps: {
ref: filenameRef,
title: 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 {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 {
mountRef,
playMedia,
mediaControllerRef,
refreshControls,
reset,
updateSeekFromPlayback,
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 (
<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 {KeyValueTable} from '@/categories/information/react';
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;
enhanced?: boolean;
}

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

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

if (!fileWithMetadata) {
return null;
@@ -42,6 +39,7 @@ export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedCompon
className,
)}
style={style}
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`}
>
<div className="h-full relative">
<div className="absolute top-0 left-0 w-full h-full">
@@ -54,6 +52,7 @@ export const BinaryFilePreview = React.forwardRef<BinaryFilePreviewDerivedCompon
role="presentation"
className="w-full h-full select-none overflow-hidden text-xs"
ref={forwardedRef}
key={`${fileWithMetadata.url}:${fileWithMetadata.type}`}
>
<BinaryDataCanvas
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 { VideoFilePreview } from '../VideoFilePreview';
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> = {
[ContentType.TEXT]: TextFilePreview,
@@ -14,43 +14,16 @@ const FILE_PREVIEW_COMPONENTS: Record<ContentType, React.ElementType> = {
[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 {
fileList?: FileList;
className?: string;
}

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

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

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 {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 {AudioMiniFilePreview} from '@tesseract-design/web-blob-react';
import {delegateTriggerChangeEvent} from '@/utils/event';
import clsx from 'clsx';
import {useEnhanced} from '@modal-soft/react-utils';

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,
}

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 = ({
fileList,
fileList: files,
}: { fileList?: FileList }) => {
const { files } = useFilePreviews(fileList);
if (!files) {
return null;
}

return (
<div className="w-full h-full overflow-auto -mx-4 px-4">
<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>
)
@@ -105,7 +83,7 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>
hint = '',
border = false,
block = false,
enhanced = false,
enhanced: enhancedProp = false,
hiddenLabel = false,
multiple = false,
onChange,
@@ -116,34 +94,39 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>
}: FileButtonProps,
forwardedRef,
) => {
const [renderEnhanced, setRenderEnhanced] = React.useState(false);
const { enhanced } = useEnhanced({ enhanced: enhancedProp });
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 ref = forwardedRef ?? defaultRef;
const labelId = React.useId();
const defaultId = React.useId();
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);
};

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) => {
@@ -153,20 +136,26 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>

const handleDropZone: React.DragEventHandler<HTMLDivElement> = async (e) => {
cancelEvent(e);
if (!(typeof ref === 'object' && ref)) {
return;
}
const { current } = ref;
if (!current) {
return;
}
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 (
<div
@@ -195,10 +184,10 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>
className={clsx(
'peer',
{
'sr-only': renderEnhanced,
'sr-only': enhanced,
}
)}
onChange={addFile}
onChange={doSetFileList}
multiple={multiple}
data-testid="input"
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
&& (
<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
className={`pointer-events-auto w-full h-full px-4 pb-4 box-border`}
key={lastSelectedFileAt}
>
{
multiple
@@ -259,11 +247,10 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>
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 className="relative">
<FilePreviewComponent
fileList={fileList}
/>
</div>
<FilePreview
className="w-full h-full relative"
fileList={fileList}
/>
</div>
</div>
)
@@ -288,18 +275,18 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>
<button
data-testid="clear"
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"
>
<span
className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
>
{multiple ? 'Clear' : 'Delete'}
Clear
</span>
</button>
</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 {useFileMetadata, useFileUrl, useImageControls} from '@/categories/blob/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;
disabled?: boolean;
enhanced?: boolean;
}

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

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

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

if (!(fileWithMetadata)) {
return null;
@@ -66,7 +162,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
className={clsx(
'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,
},
)}
@@ -94,6 +190,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
className: 'font-bold',
valueProps: {
ref: filenameRef,
title: fileWithMetadata.name,
children: fileWithMetadata.name,
},
},
@@ -128,47 +225,22 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
&& {
key: 'Palette',
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"
value="toggleFullScreen"
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',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
@@ -210,7 +282,7 @@ export const ImageFilePreview = React.forwardRef<ImageFilePreviewDerivedComponen
name="action"
value="download"
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',
'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 clsx from 'clsx';
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;

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

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

if (!fileWithMetadata) {
return null;
@@ -54,12 +51,12 @@ export const TextFilePreview = React.forwardRef<TextFilePreviewDerivedComponent,
role="presentation"
className="w-full h-full select-none overflow-hidden text-xs"
ref={forwardedRef}
key={`${fileWithMetadata.url}:${fileWithMetadata.type}`}
>
<Refractor
code={fileWithMetadata.metadata.contents}
language={fileWithMetadata.metadata.scheme}
lineNumbers={Boolean(fileWithMetadata.metadata.scheme)}
maxLineNumber={20}
/>
</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 {Slider} from '@tesseract-design/web-number-react';
import {KeyValueTable} from '@/categories/information/react';
import {useEnhanced} from '@modal-soft/react-utils';

type VideoFilePreviewDerivedComponent = HTMLVideoElement;

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

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

if (!fileWithMetadata) {
return null;
@@ -78,10 +76,10 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
<div
className="w-full h-full bg-black flex flex-col items-stretch"
data-testid="preview"
key={`${fileWithMetadata.url}:${fileWithMetadata.type}`}
>
<div
className="w-full flex-auto relative"
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`}
>
<video
{...etcProps}
@@ -95,10 +93,10 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
controls={!enhanced}
>
<source
key={`${fileWithMetadata?.url ?? ''}:${fileWithMetadata?.type ?? ''}`}
src={fileWithMetadata.url}
type={fileWithMetadata.type}
/>
Video playback not supported.
</video>
<button
className="absolute w-full h-full top-0 left-0"
@@ -114,7 +112,7 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
</button>
</div>
{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
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
className="flex-auto bg-negative text-base"
className="flex-auto text-base"
ref={seekRef}
min={0}
max={durationDisplay}
onMouseDown={startSeek}
onMouseUp={endSeek}
onChange={setSeek}
@@ -242,6 +242,7 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
className: 'font-bold',
valueProps: {
ref: filenameRef,
title: fileWithMetadata.name,
children: fileWithMetadata.name,
},
},
@@ -294,7 +295,7 @@ export const VideoFilePreview = React.forwardRef<VideoFilePreviewDerivedComponen
name="action"
value="download"
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',
'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(() => {
if (!file) {
setFileWithUrl(undefined);
if (fileWithUrl) {
setFileWithUrl(undefined);
}
setLoading(false);
return;
}

setFileWithUrl(undefined);
setLoading(true);
addDataUrl(file)
.then((fileWithUrl) => {
@@ -24,7 +27,6 @@ export const useFileUrl = (options: UseFileUrlOptions) => {
setLoading(false);
})
.catch(() => {
setFileWithUrl(file);
setLoading(false);
});
}, [file]);
@@ -40,7 +42,7 @@ export interface UseFileMetadataOptions<T extends Partial<File> = Partial<File>,
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 [fileWithMetadata, setFileWithMetadata] = React.useState<T | undefined>(file as T | undefined);
const [loading, setLoading] = React.useState(false);
@@ -48,11 +50,14 @@ export const useFileMetadata = <T extends Partial<File>>(options: UseFileMetadat

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

setFileWithMetadata(undefined);
setLoading(true);
setError(undefined);
augmentFunction(file)
@@ -61,6 +66,7 @@ export const useFileMetadata = <T extends Partial<File>>(options: UseFileMetadat
setLoading(false);
})
.catch((error) => {
setFileWithMetadata(file as T);
setError(error);
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 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(() => {
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>({
controllerRef: forwardedRef,
actionFormKey = 'action' as const,
visualizationMode: initialVisualizationMode,
}: UseMediaControlsOptions<T>) => {
const defaultRef = React.useRef<T>(null);
const ref = forwardedRef ?? defaultRef;
@@ -18,6 +17,7 @@ export const useMediaControls = <T extends HTMLMediaElement>({
const volumeRef = React.useRef<HTMLInputElement>(null);
const filenameRef = React.useRef<HTMLElement>(null);
const visualizationId = React.useId();
const formId = React.useId();
const [isPlaying, setIsPlaying] = React.useState(false);
const [isSeeking, setIsSeeking] = React.useState(false);
const [currentTimeDisplay, setCurrentTimeDisplay] = React.useState<number>();
@@ -45,10 +45,9 @@ export const useMediaControls = <T extends HTMLMediaElement>({
}, []);

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

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

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

const { current: mediaController } = ref;

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

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

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

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

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

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.remove();
});
@@ -136,7 +141,6 @@ export const useMediaControls = <T extends HTMLMediaElement>({
}), [togglePlayback, toggleSeekTimeCountMode, download]);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

return React.useMemo(() => ({
seekRef,
@@ -243,6 +236,7 @@ export const useMediaControls = <T extends HTMLMediaElement>({
handleAction,
filenameRef,
visualizationId,
formId,
}), [
refreshControls,
isPlaying,
@@ -261,5 +255,6 @@ export const useMediaControls = <T extends HTMLMediaElement>({
handleAction,
filenameRef,
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/AudioMiniFilePreview';
export * from './components/AudioMiniFilePreview';
export * from './components/BinaryFilePreview';
//export * from './components/FileSelectBox';
export * from './components/FileSelectBox';
export * from './components/ImageFilePreview';
export * from './components/TextFilePreview';
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.
*
* This component supports multiline input and adjusts its layout accordingly.
*/
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 TextControlBase from '@tesseract-design/web-base-textcontrol';
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.
*/
@@ -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,
@@ -57,7 +56,7 @@ export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, M
hiddenLabel = false,
className,
...etcProps
}: MaskedTextInputProps,
}: SpinnerProps,
ref,
) => {
const labelId = React.useId();
@@ -78,12 +77,13 @@ export const MaskedTextInput = React.forwardRef<MaskedTextInputDerivedElement, M
{...etcProps}
ref={ref}
aria-labelledby={labelId}
type="password"
type="number"
data-testid="input"
className={clsx(
'bg-negative rounded-inherit w-full peer block',
'bg-negative rounded-inherit w-full peer block tabular-nums',
'focus:outline-0',
'disabled:opacity-50 disabled:cursor-not-allowed',
styles['spinner'],
{
'text-xxs': size === 'small',
'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/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}
</div>
{actions}

</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 {getFormValues} from '@theoryofnekomata/formxtra';

type SpectrogramCanvasDerivedComponent = HTMLAudioElement;
type SpectrogramCanvasDerivedElement = HTMLDivElement;

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

export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent, SpectrogramCanvasProps>(({
export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedElement, SpectrogramCanvasProps>(({
className,
children,
controls,
@@ -36,17 +37,16 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon
sampleRate,
splitChannels,
normalize,
audioRef,
...etcProps
}, forwardedRef) => {
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 cursorRef = React.useRef<HTMLDivElement>(null);

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

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 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 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: Spectrogram, } = await import('wavesurfer.js/dist/plugins/spectrogram');
const dummyContainer = window.document.createElement('div');
@@ -115,7 +127,7 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon
normalize,
plugins: [],
cursorWidth,
media: media ?? undefined,
media,
});

let colorMap: Array<[number, number, number, number]> = [];
@@ -130,10 +142,10 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon
}
waveSurferInstance.registerPlugin(
Spectrogram.create({
container: containerRef.current,
container,
labels: true,
labelsColor: 'rgb(0 0 0/0)',
height: containerRef.current.clientHeight,
height: container.clientHeight,
colorMap,
}),
)
@@ -147,12 +159,17 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon
}
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 () => {
if (waveSurferCurrent) {
(waveSurferCurrent as unknown as Record<string, Function>).destroy();
@@ -160,13 +177,12 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon
if (container) {
container.innerHTML = '';
}
if (!media) {
return;
if (media) {
media.removeEventListener('timeupdate', handleTimeUpdate);
}
media.removeEventListener('timeupdate', handleTimeUpdate);
};
}, [
ref,
audioRef,
autoPlay,
waveColor,
progressColor,
@@ -188,6 +204,7 @@ export const SpectrogramCanvas = React.forwardRef<SpectrogramCanvasDerivedCompon
splitChannels,
normalize,
cursorWidth,
containerRef,
]);

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 {getFormValues} from '@theoryofnekomata/formxtra';

type SpectrogramCanvasDerivedComponent = HTMLAudioElement;
type WaveformCanvasDerivedElement = HTMLDivElement;

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,
children,
controls,
@@ -34,16 +36,15 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
sampleRate,
splitChannels,
normalize,
audioRef,
...etcProps
}, forwardedRef) => {
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 handleAction: React.FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
e.preventDefault();
const nativeEvent = e.nativeEvent as unknown as { submitter: HTMLElement };
const formData = getFormValues(
@@ -63,21 +64,26 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
};

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({
container: containerRef.current,
container,
height: 'auto',
autoplay: autoPlay,
fillParent: true,
@@ -101,7 +107,8 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
splitChannels,
normalize,
cursorWidth,
media: media ?? undefined,
plugins: [],
media,
});
waveSurferInstance.on('ready', () => {
if (!container) {
@@ -111,11 +118,20 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
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 () => {
if (waveSurferCurrent) {
(waveSurferCurrent as unknown as Record<string, Function>).destroy();
@@ -125,7 +141,7 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
}
};
}, [
ref,
audioRef,
autoPlay,
waveColor,
progressColor,
@@ -147,6 +163,7 @@ export const WaveformCanvas = React.forwardRef<SpectrogramCanvasDerivedComponent
splitChannels,
normalize,
cursorWidth,
containerRef,
]);

return (


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

@@ -169,15 +169,15 @@ const BlobPage: NextPage = () => {
</Section>
<Section title="FileSelectBox">
<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>
</Section>
</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 { NextPage } from 'next';
import {Slider} from '@/categories/number/react';
import * as TesseractNumber from '@tesseract-design/web-number-react';
import {Section, Subsection} from '@/components/Section';

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

<input type="number" />
<TesseractNumber.Spinner
min={-100}
max={100}
step="any"
label="Step"
/>
</Subsection>
</Section>
<Section title="Slider">
<Subsection title="Default">
<Slider
<TesseractNumber.Slider
min={-100}
max={100}
tickMarks={[{ label: 'low', value: 25, }, 50]}
/>
</Subsection>
<Subsection title="Vertical">
<Slider
<TesseractNumber.Slider
min={-100}
max={100}
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 {getTextMetadata, TextMetadata} from '@modal-soft/text-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';

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

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

@@ -204,22 +204,3 @@ export const augmentVideoFile = async <T extends Partial<FileWithDataUrl>>(file:
fileMutable.metadata = await getVideoMetadata(file.url);
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))',
'current': 'currentcolor',
'inherit': 'inherit',
'transparent': 'transparent',
},
extend: {
fontSize: {


Loading…
Cancel
Save