Bläddra i källkod

Implement other previews

Implement previews for audio and text.
pull/1/head
TheoryOfNekomata 1 år sedan
förälder
incheckning
b611a5dac8
4 ändrade filer med 1100 tillägg och 13 borttagningar
  1. +121
    -2
      packages/web/base/blob/src/index.ts
  2. +6
    -1
      packages/web/kitchen-sink/react-next/package.json
  3. +948
    -10
      packages/web/kitchen-sink/react-next/src/pages/categories/blob/index.tsx
  4. +25
    -0
      packages/web/kitchen-sink/react-next/yarn.lock

+ 121
- 2
packages/web/base/blob/src/index.ts Visa fil

@@ -1,8 +1,127 @@
import { css } from '@tesseract-design/goofy-goober';

export interface BlobBaseArgs {

border: boolean,
block: boolean,
}

export const Root = ({}: BlobBaseArgs) => css.cx(
export const Root = ({
block,
}: BlobBaseArgs) => css.cx(
css`
vertical-align: middle;
position: relative;
border-radius: 0.25rem;
font-family: var(--font-family-base, sans-serif);
max-width: 100%;
overflow: hidden;
min-height: 20rem;
min-width: 20rem;
&:focus-within {
--color-accent: var(--color-hover, red);
}
& > span {
border-color: var(--color-accent, blue);
box-sizing: border-box;
display: inline-block;
border-width: 0.125rem;
border-style: solid;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
z-index: 2;
pointer-events: none;
transition-property: border-color;
}
& > span::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: '';
border-radius: 0.125rem;
opacity: 0.5;
pointer-events: none;
box-shadow: 0 0 0 0 var(--color-accent, blue);
transition-property: box-shadow;
transition-duration: 150ms;
transition-timing-function: linear;
}
&:focus-within > span::before {
box-shadow: 0 0 0 0.375rem var(--color-accent, blue);
}
`,
css.dynamic({
display: block ? 'block' : 'inline-block',
}),
);

export const Border = ({
border
}: BlobBaseArgs): string => css.cx(
css.if (border) (
css`
border-color: var(--color-accent, blue);
box-sizing: border-box;
display: inline-block;
border-width: 0.125rem;
border-style: solid;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
pointer-events: none;
z-index: 10;
&::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: '';
border-radius: 0.125rem;
opacity: 0.5;
pointer-events: none;
}
`
),
);


export const LabelWrapper = ({
border,
}: BlobBaseArgs): string => css.cx(
css`
color: var(--color-accent, blue);
box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bolder;
z-index: 1;
pointer-events: none;
transition-property: color;
line-height: 0.65;
user-select: none;
font-size: 0.725em;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
`,
css.if (border) (
css`
background-color: var(--color-background, white);
`
),
)

+ 6
- 1
packages/web/kitchen-sink/react-next/package.json Visa fil

@@ -16,12 +16,17 @@
"@tesseract-design/web-information-react": "link:../../categories/information/react",
"@tesseract-design/web-navigation-react": "link:../../categories/navigation/react",
"@tesseract-design/web-option-react": "link:../../categories/option/react",
"language-map": "^1.5.0",
"languagedetect": "^2.0.0",
"next": "12.2.4",
"prismjs": "^1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0"
"react-dom": "18.2.0",
"wavesurfer.js": "^7.0.0-beta.6"
},
"devDependencies": {
"@types/node": "18.6.4",
"@types/prismjs": "^1.26.0",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"autoprefixer": "^10.4.14",


+ 948
- 10
packages/web/kitchen-sink/react-next/src/pages/categories/blob/index.tsx Visa fil

@@ -1,19 +1,957 @@
import {NextPage} from 'next';
import * as React from 'react';
import * as BlobReact from '@tesseract-design/web-blob-react';
//import * as BlobReact from '@tesseract-design/web-blob-react';

import * as BlobBase from '@tesseract-design/web-base-blob';
import * as ButtonBase from '@tesseract-design/web-base-button';
import WaveSurfer from 'wavesurfer.js';
import LanguageDetect from 'languagedetect';
import Prism from 'prismjs';
import { languages } from 'prismjs/components';

console.log(languages);

interface FileWithPreview extends File {
metadata?: Record<string, string | number>;
internal?: Record<string, unknown>;
}

export interface FileButtonProps extends Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'type' | 'style' | 'label' | 'list'> {
/**
* Should the component display a border?
*/
border?: boolean,
/**
* Should the component occupy the whole width of its parent?
*/
block?: boolean,
/**
* Short textual description indicating the nature of the component's value.
*/
label?: React.ReactNode,
/**
* Short textual description as guidelines for valid input values.
*/
hint?: React.ReactNode,
enhanced?: boolean,
/**
* Is the label hidden?
*/
hiddenLabel?: boolean,
}

const formatNumeral = (n?: number) => {
if (typeof n !== 'number') {
return '';
}

if (!Number.isFinite(n)) {
return '';
}

return new Intl.NumberFormat().format(n);
}

const formatFileSize = (size?: number) => {
if (typeof size !== 'number') {
return '';
}

if (!Number.isFinite(size)) {
return '';
}

if (size < (2 ** 10)) {
return `${formatNumeral(size)} bytes`;
}

if (size < (2 ** 20)) {
return `${(size / (2 ** 10)).toFixed(3)} kiB`;
}

if (size < (2 ** 30)) {
return `${(size / (2 ** 20)).toFixed(3)} MiB`;
}

if (size < (2 ** 40)) {
return `${(size / (2 ** 30)).toFixed(3)} GiB`;
}
};

const MIME_TYPE_DESCRIPTIONS = {
'image/jpeg': 'JPEG Image',
'image/png': 'PNG Image',
'image/tiff': 'TIFF Image',
'image/svg+xml': 'SVG Image',
'audio/wav': 'WAVE Audio',
'audio/ogg': 'OGG Audio',
'audio/mpeg': 'MPEG Audio',
'application/json': 'JSON Data',
'application/xml': 'XML Data',
} as const;

const getMimeTypeDescription = (type?: string) => {
if (typeof type !== 'string') {
return '';
}

if (type === 'application/octet-stream') {
return type;
}

const {
[type as keyof typeof MIME_TYPE_DESCRIPTIONS]: description = type,
} = MIME_TYPE_DESCRIPTIONS;

return description;
}

const formatSecondsDuration = (seconds: number) => {
const secondsInt = Math.floor(seconds);
const secondsFrac = seconds - secondsInt;
const hh = Math.floor(secondsInt / 3600).toString().padStart(2, '0');
const mm = Math.floor(secondsInt / 60 % 60).toString().padStart(2, '0');
const ss = (secondsInt % 60).toString().padStart(2, '0');
const sss = Math.floor(secondsFrac * 1000).toString().padStart(3, '0');
return `${hh}:${mm}:${ss}.${sss}`;
};

enum ContentType {
TEXT = 'text',
AUDIO = 'audio',
VIDEO = 'video',
IMAGE = 'image',
BINARY = 'binary',
}

const getContentType = (mimeType?: string) => {
if (typeof mimeType !== 'string') {
return ContentType.BINARY;
}

if (
mimeType === 'application/json'
|| mimeType === 'application/xml'
|| mimeType.startsWith('text/')
) {
return ContentType.TEXT;
}

if (mimeType.startsWith('video/')) {
return ContentType.VIDEO;
}

if (mimeType.startsWith('audio/')) {
return ContentType.AUDIO;
}

if (mimeType.startsWith('image/')) {
return ContentType.IMAGE;
}

return ContentType.BINARY;
}

const useFilePreviews = (fileList?: FileList) => {
const [selectedFiles, setSelectedFiles] = React.useState([] as Partial<FileWithPreview>[]);
React.useEffect(() => {
const loadFilePreviews = async (fileList?: FileList) => {
if (!fileList) {
return;
}
const files = Array.from(fileList);
const fileResult = await Promise.all(
files.map((f) => new Promise<Partial<FileWithPreview>>((resolve, reject) => {
const contentType = getContentType(f.type);

switch (contentType) {
case ContentType.TEXT: {
const reader = new FileReader();
reader.addEventListener('error', () => {
reject();
});

reader.addEventListener('load', async (loadEvent) => {
const target = loadEvent.target as FileReader;
const contents = target.result as string;
const resolvedAliases = Object.fromEntries(
Object
.entries(languages)
.reduce(
(resolved, [languageId, languageDefinition]) => {
if (Array.isArray(languageDefinition.alias)) {
return [
...resolved,
...(languageDefinition.alias.map((a: string) => [a, { title: languageDefinition.title, extension: `.${a}`} ])),
[languageId, { title: languageDefinition.title, extension: `.${languageId}`}],
];
}

if (typeof languageDefinition.alias === 'string') {
return [
...resolved,
[languageDefinition.alias, { title: languageDefinition.title, extension: `.${languageDefinition.alias}`}],
[languageId, { title: languageDefinition.title, extension: `.${languageId}`}],
];
}

return [
...resolved,
[languageId, { title: languageDefinition.title, extension: `.${languageId}`}],
];
},
[] as [string, { title: string, extension: string }][]
)
);
const metadata = Object
.entries(resolvedAliases)
.reduce(
(theMetadata, [key, value]) => {
if (typeof theMetadata.scheme === 'undefined' && f.name.endsWith(value.extension)) {
return {
...theMetadata,
};
}

return theMetadata;
},
{
contents,
} as Record<string, number | string>
);

if (typeof metadata.scheme !== 'string') {
const naturalLanguageDetector = new LanguageDetect();
const probableLanguages = naturalLanguageDetector.detect(contents);
const [languageName, probability] = probableLanguages[0];
metadata.language = languageName;
} else {
await import(`prismjs/components/prism-${metadata.scheme}`);
}

resolve({
...f,
name: f.name,
type: f.type,
size: f.size,
lastModified: f.lastModified,
metadata,
});
});

reader.readAsText(f);
return;
}
case ContentType.IMAGE: {
const reader = new FileReader();
reader.addEventListener('error', () => {
reject();
});

reader.addEventListener('load', (loadEvent) => {
const target = loadEvent.target as FileReader;
const previewUrl = target.result as string;
const image = new Image();
image.addEventListener('load', (imageLoadEvent) => {
const thisImage = imageLoadEvent.currentTarget as HTMLImageElement;
resolve({
...f,
name: f.name,
type: f.type,
size: f.size,
lastModified: f.lastModified,
metadata: {
previewUrl,
width: thisImage.naturalWidth,
height: thisImage.naturalHeight,
},
});
});

image.addEventListener('error', () => {
reject();
});

image.src = previewUrl;
});

reader.readAsDataURL(f);
return;
}
case ContentType.AUDIO: {
const reader = new FileReader();
reader.addEventListener('error', () => {
reject();
});

reader.addEventListener('load', (loadEvent) => {
const target = loadEvent.target as FileReader;
const previewUrl = target.result as string;
let mediaInstance = null as (WaveSurfer | null);

const waveSurferInstance = WaveSurfer.create({
container: window.document.createElement('div'),
});

waveSurferInstance.on('ready', async () => {
const metadata = {
duration: waveSurferInstance.getDuration(),
};

waveSurferInstance.destroy();
resolve({
...f,
name: f.name,
type: f.type,
size: f.size,
lastModified: f.lastModified,
metadata,
internal: {
createMediaInstance(container: HTMLElement) {
if (mediaInstance instanceof WaveSurfer) {
mediaInstance.destroy();
}
mediaInstance = WaveSurfer.create({
container,
url: previewUrl,
height: container.clientHeight,
interact: false,
cursorWidth: 0,
barWidth: 2,
barGap: 2,
barRadius: 1,
normalize: true,
});
},
destroyMediaInstance() {
if (!(mediaInstance instanceof WaveSurfer)) {
return;
}
mediaInstance.destroy();
},
play() {
if (!(mediaInstance instanceof WaveSurfer)) {
return;
}
mediaInstance.play();
},
pause() {
if (!(mediaInstance instanceof WaveSurfer)) {
return;
}
mediaInstance.pause();
},
stop() {
if (!(mediaInstance instanceof WaveSurfer)) {
return;
}
mediaInstance.stop();
},
playPause() {
if (!(mediaInstance instanceof WaveSurfer)) {
return;
}
mediaInstance.playPause();
},
},
})
});

waveSurferInstance.load(previewUrl);
});

reader.readAsDataURL(f);
return;
}
default:
break;
}
resolve(f);
}))
);

setSelectedFiles(fileResult);
}

loadFilePreviews(fileList);
}, [fileList]);

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

const FilePreview = ({
fileList: fileList,
}: { fileList?: FileList }) => {
const { files } = useFilePreviews(fileList);
const mediaContainerRef = React.useRef<HTMLDivElement>(null);

const playMedia = () => {
if (files.length < 1) {
return;
}
const [theFile] = files;
if (typeof theFile.internal?.playPause === 'function') {
theFile.internal.playPause();
}
};

React.useEffect(() => {
if (files.length < 1) {
return;
}

const [theFile] = files;
if (typeof theFile.internal?.createMediaInstance === 'function' && mediaContainerRef.current) {
theFile.internal.createMediaInstance(mediaContainerRef.current);
}

return () => {
if (typeof theFile.internal?.destroyMediaInstance === 'function') {
theFile.internal.destroyMediaInstance();
}
}
}, [files, mediaContainerRef]);

if (files.length < 1) {
return null;
}

const f = files[0];
const contentType = getContentType(f.type);

console.log(f);

const BlobPage: NextPage = () => {
return (
<div className="z-10 pb-12">
<BlobReact.FileSelectBox
border
enhanced
label="vro"
hint="Select any files here"
multiple
/>
<div
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`}>
{
contentType === ContentType.TEXT
&& (
<div className="flex gap-4 w-full h-full relative">
<div className={`h-full w-1/3 flex-shrink-0`}>
{
typeof f.metadata?.contents === 'string'
&& typeof f.metadata?.scheme === 'string'
&& (
<div
data-testid="preview"
role="presentation"
className="w-full h-full select-none overflow-hidden"
>
<pre className="overflow-visible">
{
f.metadata.scheme
&& (
<code
dangerouslySetInnerHTML={{
__html: Prism.highlight(
f.metadata.contents,
Prism.languages[f.metadata.scheme],
f.metadata.scheme,
),
}}
style={{
tabSize: 2,
}}
/>
)
}
{
!f.metadata.scheme
&& (
<code>
{f.metadata.contents}
</code>
)
}
</pre>
</div>
)
}
</div>
<dl className="w-2/3 flex-shrink-0 m-0" data-testid="infoBox">
<div className="w-full">
<dt className="sr-only">
Name
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
title={f.name}
>
{f.name}
</dd>
</div>
<div className="w-full">
<dt className="sr-only">
Type
</dt>
<dd
title={f.type}
className="m-0 w-full text-ellipsis overflow-hidden"
>
{typeof f.metadata?.schemeTitle === 'string' ? `${f.metadata.schemeTitle} Source` : 'Text File'}
</dd>
</div>
<div className="w-full">
<dt className="sr-only">
Size
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
title={`${formatNumeral(f.size ?? 0)} bytes`}
>
{formatFileSize(f.size)}
</dd>
</div>
{
typeof f.metadata?.language === 'string'
&& (
<div>
<dt className="sr-only">
Language
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
>
{f.metadata.language.slice(0, 1).toUpperCase()}
{f.metadata.language.slice(1)}
</dd>
</div>
)
}
</dl>
</div>
)
}
{
contentType === ContentType.IMAGE
&& (
<div className="flex gap-4 w-full h-full relative">
<div className={`h-full w-1/3 flex-shrink-0`}>
{
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"
/>
)
}
</div>
<dl className="w-2/3 flex-shrink-0 m-0" data-testid="infoBox">
<div className="w-full">
<dt className="sr-only">
Name
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
title={f.name}
>
{f.name}
</dd>
</div>
<div className="w-full">
<dt className="sr-only">
Type
</dt>
<dd
title={f.type}
className="m-0 w-full text-ellipsis overflow-hidden"
>
{getMimeTypeDescription(f.type)}
</dd>
</div>
<div className="w-full">
<dt className="sr-only">
Size
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
title={`${formatNumeral(f.size ?? 0)} bytes`}
>
{formatFileSize(f.size)}
</dd>
</div>
{
typeof f.metadata?.width === 'number'
&& typeof f.metadata?.height === 'number'
&& (
<div>
<dt className="sr-only">
Pixel Dimensions
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
>
{formatNumeral(f.metadata.width)}&times;{formatNumeral(f.metadata.height)} pixels
</dd>
</div>
)
}
</dl>
</div>
)
}
{
contentType === ContentType.AUDIO
&& (
<div className="flex flex-col gap-4 w-full h-full relative">
<div
className={`h-2/5 flex-shrink-0 cursor-pointer`}
ref={mediaContainerRef}
onClick={playMedia}
/>
<dl className="h-3/5 flex-shrink-0 m-0 flex flex-col items-end" data-testid="infoBox">
<div className="w-full">
<dt className="sr-only">
Name
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
title={f.name}
>
{f.name}
</dd>
</div>
<div className="w-full">
<dt className="sr-only">
Type
</dt>
<dd
title={f.type}
className="m-0 w-full text-ellipsis overflow-hidden"
>
{getMimeTypeDescription(f.type)}
</dd>
</div>
<div className="w-full">
<dt className="sr-only">
Size
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
title={`${formatNumeral(f.size ?? 0)} bytes`}
>
{formatFileSize(f.size)}
</dd>
</div>
{
typeof f.metadata?.duration === 'number'
&& (
<div className="w-full">
<dt className="sr-only">
Duration
</dt>
<dd
className="m-0 w-full text-ellipsis overflow-hidden"
title={`${formatNumeral(f.metadata.duration)} seconds`}
>
{formatSecondsDuration(f.metadata.duration)}
</dd>
</div>
)
}
</dl>
</div>
)
}
</div>
</div>
)
}

const FilePreviewGrid = ({
fileList,
}: { fileList?: FileList }) => {
const { files } = useFilePreviews(fileList);
const mediaContainerRef = React.useRef<HTMLDivElement>(null);

const playMedia = (index: number) => () => {
if (files.length < 1) {
return;
}
const theFile = files[index];
if (typeof theFile.internal?.playPause === 'function') {
theFile.internal.playPause();
}
};

React.useEffect(() => {
if (files.length < 1) {
return;
}

files.forEach((theFile, i) => {
if (typeof theFile.internal?.createMediaInstance === 'function' && mediaContainerRef.current) {
theFile.internal.createMediaInstance(mediaContainerRef.current.children[i].children[0]);
}
});

return () => {
files.forEach((theFile) => {
if (typeof theFile.internal?.destroyMediaInstance === 'function') {
theFile.internal.destroyMediaInstance();
}
});
}
}, [files, mediaContainerRef]);

return (
<div className="w-full h-full overflow-auto -mx-4 px-4">
<div
className={`w-full grid gap-4 grid-cols-3`}
ref={mediaContainerRef}
>
{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.type?.startsWith('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.type?.startsWith('audio/')
&& (
<div
className="absolute top-0 left-0 w-full h-full cursor-pointer"
onClick={playMedia(i)}
/>
)
}
</div>
))}
</div>
</div>
)
}

export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps>(
(
{
label = '',
hint = '',
border = false,
block = false,
enhanced = false,
hiddenLabel = false,
multiple = false,
onChange,
disabled = false,
className: _className,
placeholder: _placeholder,
as: _as,
...etcProps
}: FileButtonProps,
forwardedRef,
) => {
const [isEnhanced, setIsEnhanced] = React.useState(false);
const [fileList, setFileList] = React.useState<FileList>();
const defaultRef = React.useRef<HTMLInputElement>(null);
const ref = forwardedRef ?? defaultRef;

const addFile: React.ChangeEventHandler<HTMLInputElement> = (e) => {
if (!enhanced) {
onChange?.(e);
return;
}

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

const deleteFiles: React.MouseEventHandler<HTMLButtonElement> = () => {
if (typeof ref === 'object' && ref.current) {
ref.current.value = '';
setFileList(undefined);
}
};

const cancelEvent = (e: React.DragEvent) => {
e.stopPropagation();
e.preventDefault();
}

const handleDropZone: React.DragEventHandler<HTMLDivElement> = async (e) => {
cancelEvent(e);
const { dataTransfer } = e;
if (typeof ref === 'object' && ref.current) {
const { files } = dataTransfer;
setFileList(ref.current.files = files);
ref.current.dispatchEvent(new Event('change'));
}
}

React.useEffect(() => {
setIsEnhanced(enhanced);
}, [enhanced]);

return (
<div
className={BlobBase.Root({
border,
block,
})}
onDragEnter={cancelEvent}
onDragOver={cancelEvent}
onDrop={handleDropZone}
data-testid="root"
>
<label
className="block absolute top-0 left-0 w-full h-full cursor-pointer"
data-testid="clickArea"
>
<input
{...etcProps}
disabled={disabled}
ref={ref}
type="file"
className={`${enhanced ? 'sr-only' : ''}`}
onChange={addFile}
multiple={multiple}
data-testid="input"
/>
</label>
{
border && (
<span
data-testid="border"
className={
BlobBase.Border({
border,
block,
})
}
/>
)
}
{
label
&& !hiddenLabel
&& (
<div
data-testid="label"
className={BlobBase.LabelWrapper({
border,
block,
})}
>
{label}
</div>
)
}
{
(fileList?.length ?? 0) < 1
&& isEnhanced
&& hint
&& (
<div
data-testid="hint"
className="absolute top-0 left-0 w-full h-full pointer-events-none box-border overflow-hidden pt-4"
>
<div className="flex items-center justify-center w-full h-full">
{hint}
</div>
</div>
)
}
{
(fileList?.length ?? 0) > 0
&& isEnhanced
&& (
<>
<div className={`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`}>
{
multiple
&& (
<FilePreviewGrid fileList={fileList} />
)
}
{
!multiple
&& (
<FilePreview fileList={fileList} />
)
}
</div>
</div>
<div className="pointer-events-none absolute bottom-0 left-0 w-full text-center h-12 box-border flex">
<div className="w-0 flex-auto flex flex-col items-center justify-center h-full">
<span
className={ButtonBase.Button({
size: ButtonBase.ButtonSize.MEDIUM,
border: false,
block: true,
variant: ButtonBase.ButtonVariant.OUTLINE,
disabled,
compact: false,
menuItem: false,
})}
>
Reselect
</span>
</div>
<div className="pointer-events-auto w-0 flex-auto flex flex-col items-center justify-center h-full">
<button
data-testid="clear"
type="button"
onClick={deleteFiles}
className={ButtonBase.Button({
size: ButtonBase.ButtonSize.MEDIUM,
border: false,
block: true,
variant: ButtonBase.ButtonVariant.OUTLINE,
disabled,
compact: false,
menuItem: false,
})}
>
{multiple ? 'Clear' : 'Delete'}
</button>
</div>
</div>
</>
)
}
</div>
);
}
);

FileSelectBox.displayName = 'FileSelectBox';

const BlobReact = {
FileSelectBox
}

const BlobPage: NextPage = () => {
return (
<BlobReact.FileSelectBox
border
enhanced
label="Primary Image"
hint="Select any files here"
block
/>
)
}

export default BlobPage;

+ 25
- 0
packages/web/kitchen-sink/react-next/yarn.lock Visa fil

@@ -261,6 +261,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.4.tgz#fd26723a8a3f8f46729812a7f9b4fc2d1608ed39"
integrity sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==
"@types/prismjs@^1.26.0":
version "1.26.0"
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.0.tgz#a1c3809b0ad61c62cac6d4e0c56d610c910b7654"
integrity sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==
"@types/prop-types@*":
version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
@@ -1534,6 +1539,11 @@ json5@^1.0.2:
array-includes "^3.1.5"
object.assign "^4.1.3"
language-map@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/language-map/-/language-map-1.5.0.tgz#65c6d2c7493efa6708585386f0c2799d544fa367"
integrity sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg==
language-subtag-registry@~0.3.2:
version "0.3.22"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d"
@@ -1546,6 +1556,11 @@ language-tags@=1.0.5:
dependencies:
language-subtag-registry "~0.3.2"
languagedetect@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/languagedetect/-/languagedetect-2.0.0.tgz#4b8fa2b7593b2a3a02fb1100891041c53238936c"
integrity sha512-AZb/liiQ+6ZoTj4f1J0aE6OkzhCo8fyH+tuSaPfSo8YHCWLFJrdSixhtO2TYdIkjcDQNaR4RmGaV2A5FJklDMQ==
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
@@ -1910,6 +1925,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prismjs@^1.29.0:
version "1.29.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"
integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
@@ -2331,6 +2351,11 @@ v8-compile-cache@^2.0.3:
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
wavesurfer.js@^7.0.0-beta.6:
version "7.0.0-beta.6"
resolved "https://registry.yarnpkg.com/wavesurfer.js/-/wavesurfer.js-7.0.0-beta.6.tgz#5ba26d10015e46aae3bf279438ee0903ea4e63fd"
integrity sha512-vB8J1ppZ58vozmBDmqADDdKBYY6bebSYKUgIaDXB56Qo/CpPdExSlg91tN1FAubN5swZ1IUyB8Z9xOY/TsYRoA==
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"


Laddar…
Avbryt
Spara