diff --git a/packages/web-kitchensink-reactnext/src/categories/action/react/components/ActionButton/index.tsx b/packages/web-kitchensink-reactnext/src/categories/action/react/components/ActionButton/index.tsx index ff52b0e..e00a2b6 100644 --- a/packages/web-kitchensink-reactnext/src/categories/action/react/components/ActionButton/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/action/react/components/ActionButton/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import clsx from 'clsx'; import * as ButtonBase from '@tesseract-design/web-base-button'; -type ActionButtonDerivedElement = HTMLButtonElement; +export type ActionButtonDerivedElement = HTMLButtonElement; export interface ActionButtonProps extends Omit, 'type' | 'size'> { type?: ButtonBase.ButtonType; @@ -90,11 +90,16 @@ export const ActionButton = React.forwardRef {badge && ( - - {badge} - + <> + + {' - '} + + + {badge} + + )} {menuItem && ( = Partial> { + file?: F; + disabled?: boolean; + enhanced?: boolean; +} + +export type FilePreviewComponent = (props: T) => React.ReactNode; diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx index c658fbc..6827d8f 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/AudioFilePreview/index.tsx @@ -15,14 +15,12 @@ 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'; +import {CommonPreviewProps} from '@/categories/blob/react/common'; -type AudioFilePreviewDerivedElement = HTMLAudioElement; +export type AudioFilePreviewDerivedElement = HTMLAudioElement; -export interface AudioFilePreviewProps = Partial> extends Omit, 'controls'> { - file?: F; - disabled?: boolean; - enhanced?: boolean; -} +export interface AudioFilePreviewProps = Partial> + extends Omit, 'controls'>, CommonPreviewProps {} export const AudioFilePreview = React.forwardRef(({ file, diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/BinaryFilePreview/index.tsx b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/BinaryFilePreview/index.tsx index 94d431c..be10dcc 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/BinaryFilePreview/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/BinaryFilePreview/index.tsx @@ -6,13 +6,12 @@ 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'; +import {CommonPreviewProps} from '@/categories/blob/react/common'; -type BinaryFilePreviewDerivedElement = HTMLDivElement; +export type BinaryFilePreviewDerivedElement = HTMLDivElement; -export interface BinaryFilePreviewProps = Partial> extends React.HTMLProps { - file?: F; - enhanced?: boolean; -} +export interface BinaryFilePreviewProps = Partial> + extends React.HTMLProps, CommonPreviewProps {} export const BinaryFilePreview = React.forwardRef(({ file, diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/FileSelectBox/index.tsx b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/FileSelectBox/index.tsx index 3adead1..cd8acd1 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/FileSelectBox/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/FileSelectBox/index.tsx @@ -1,13 +1,28 @@ import * as React from 'react'; -import {ContentType, FileWithResolvedContentType, getMimeTypeDescription} from '@/utils/blob'; -import { FilePreview } from '../FilePreview'; +import {ContentType, FileWithResolvedContentType, getContentType, getMimeTypeDescription} from '@/utils/blob'; import {formatFileSize} from '@/utils/numeral'; -import {AudioMiniFilePreview} from '@tesseract-design/web-blob-react'; +import {AudioFilePreview} from '../AudioFilePreview'; +import {BinaryFilePreview} from '../BinaryFilePreview'; +import {ImageFilePreview} from '../ImageFilePreview'; +import {VideoFilePreview} from '../VideoFilePreview'; +import {TextFilePreview} from '../TextFilePreview'; +import {AudioMiniFilePreview} from '../AudioMiniFilePreview'; import {delegateTriggerChangeEvent} from '@/utils/event'; import clsx from 'clsx'; import {useEnhanced} from '@modal-soft/react-utils'; +import {FilePreviewComponent} from '@/categories/blob/react/common'; -export interface FileButtonProps extends Omit, 'size' | 'type' | 'style' | 'label' | 'list'> { +const FILE_PREVIEW_COMPONENTS: Record = { + [ContentType.IMAGE]: ImageFilePreview, + [ContentType.AUDIO]: AudioFilePreview, + [ContentType.VIDEO]: VideoFilePreview, + [ContentType.BINARY]: BinaryFilePreview, + [ContentType.TEXT]: TextFilePreview, +}; + +export type FileSelectBoxDerivedElement = HTMLInputElement; + +export interface FileSelectBoxProps extends Omit, 'size' | 'type' | 'style' | 'label' | 'list'> { /** * Should the component display a border? */ @@ -31,52 +46,7 @@ export interface FileButtonProps extends Omit, hiddenLabel?: boolean, } -const FilePreviewGrid = ({ - fileList: files, -}: { fileList?: FileList }) => { - if (!files) { - return null; - } - - return ( -
-
- {Array.from(files).map((file: File, i) => { - const f = file as unknown as FileWithResolvedContentType; - return ( -
- { - f.resolvedType === ContentType.IMAGE - && typeof f?.url === 'string' - && ( - {f.name} - ) - } - { - f.resolvedType === ContentType.AUDIO - && ( - - ) - } -
- ); - })} -
-
- ) -} - -export const FileSelectBox = React.forwardRef( +export const FileSelectBox = React.forwardRef( ( { label = '', @@ -91,7 +61,7 @@ export const FileSelectBox = React.forwardRef className, id: idProp, ...etcProps - }: FileButtonProps, + }: FileSelectBoxProps, forwardedRef, ) => { const { enhanced } = useEnhanced({ enhanced: enhancedProp }); @@ -155,7 +125,7 @@ export const FileSelectBox = React.forwardRef }); } - const filesCount = React.useMemo(() => fileList?.length ?? 0, [fileList]); + const filesCount = fileList?.length ?? 0; return (
data-testid="clickArea" htmlFor={id} /> - { multiple && ( - +
+
+ {Array.from(fileList ?? []).map((file: File) => { + const f = file as unknown as FileWithResolvedContentType; + const fileContentType = getContentType(file.type, file.name); + return ( +
+ { + fileContentType === ContentType.IMAGE + && typeof f?.url === 'string' + && ( + {f.name} + ) + } + { + fileContentType === ContentType.AUDIO + && ( + + ) + } +
+ ); + })} +
+
) } { !multiple - && ( -
-
- + && Array.from(fileList ?? []).map((file) => { + const f = file as unknown as FileWithResolvedContentType; + const fileContentType = getContentType(file.type, file.name); + const { [fileContentType]: FilePreviewComponent = BinaryFilePreview } = FILE_PREVIEW_COMPONENTS; + return ( +
+
+ +
-
- ) + ) + }) }
diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx index 9711c51..72c2197 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/ImageFilePreview/index.tsx @@ -5,111 +5,13 @@ 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'; +import {CommonPreviewProps} from '@/categories/blob/react/common'; +import {Swatch} from '@tesseract-design/web-color-react'; -type RgbTuple = [number, number, number]; +export type ImageFilePreviewDerivedElement = HTMLImageElement; -type SwatchDerivedElement = HTMLInputElement; - -export interface SwatchProps extends Omit, 'color'> { - color: RgbTuple; - mode?: 'rgb' | 'hsl'; -} - -export const useSwatchControls = () => { - const id = React.useId(); - const copyColor: React.ReactEventHandler = 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(({ - color, - mode = 'rgb', - className, - style, - ...etcProps -}, forwardedRef) => { - const { id, copyColor } = useSwatchControls(); - const colorValue = `${mode}(${color.join(', ')})`; - - return ( - - - - - ); -}); - -Swatch.displayName = 'Swatch'; - -type ImageFilePreviewDerivedElement = HTMLImageElement; - -export interface ImageFilePreviewProps = Partial> extends Omit, 'src' | 'alt'> { - file?: F; - disabled?: boolean; - enhanced?: boolean; -} +export interface ImageFilePreviewProps = Partial> + extends Omit, 'src' | 'alt'>, CommonPreviewProps {} export const ImageFilePreview = React.forwardRef(({ file, diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/TextFilePreview/index.tsx b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/TextFilePreview/index.tsx index f48928c..9df76a9 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/TextFilePreview/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/TextFilePreview/index.tsx @@ -6,13 +6,12 @@ import clsx from 'clsx'; import {KeyValueTable} from '@/categories/information/react'; import {Refractor} from '@modal-soft/react-refractor'; import {useEnhanced} from '@modal-soft/react-utils'; +import {CommonPreviewProps} from '@/categories/blob/react/common'; type TextFilePreviewDerivedComponent = HTMLDivElement; -export interface TextFilePreviewProps = Partial> extends React.HTMLProps { - file?: F; - enhanced?: boolean; -} +export interface TextFilePreviewProps = Partial> + extends React.HTMLProps, CommonPreviewProps {} export const TextFilePreview = React.forwardRef(({ file, diff --git a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx index 3fc5924..71031e4 100644 --- a/packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/blob/react/components/VideoFilePreview/index.tsx @@ -6,14 +6,12 @@ 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'; +import {CommonPreviewProps} from '@/categories/blob/react/common'; -type VideoFilePreviewDerivedComponent = HTMLVideoElement; +export type VideoFilePreviewDerivedComponent = HTMLVideoElement; -export interface VideoFilePreviewProps = Partial> extends Omit, 'controls'> { - file?: F; - disabled?: boolean; - enhanced?: boolean; -} +export interface VideoFilePreviewProps = Partial> + extends Omit, 'controls'>, CommonPreviewProps {} export const VideoFilePreview = React.forwardRef(({ file, diff --git a/packages/web-kitchensink-reactnext/src/categories/color/react/components/Swatch/index.tsx b/packages/web-kitchensink-reactnext/src/categories/color/react/components/Swatch/index.tsx new file mode 100644 index 0000000..b98629f --- /dev/null +++ b/packages/web-kitchensink-reactnext/src/categories/color/react/components/Swatch/index.tsx @@ -0,0 +1,99 @@ +import * as React from 'react'; +import clsx from 'clsx'; + +type RgbTuple = [number, number, number]; + +export type SwatchDerivedElement = HTMLInputElement; + +export interface SwatchProps extends Omit, 'color'> { + color: RgbTuple; + mode?: 'rgb' | 'hsl'; +} + +export const useSwatchControls = () => { + const id = React.useId(); + const copyColor: React.ReactEventHandler = 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(({ + color, + mode = 'rgb', + className, + style, + ...etcProps +}, forwardedRef) => { + const { id, copyColor } = useSwatchControls(); + const colorValue = `${mode}(${color.join(', ')})`; + + return ( + + + + + ); +}); + +Swatch.displayName = 'Swatch'; diff --git a/packages/web-kitchensink-reactnext/src/categories/color/react/index.ts b/packages/web-kitchensink-reactnext/src/categories/color/react/index.ts new file mode 100644 index 0000000..a436d9f --- /dev/null +++ b/packages/web-kitchensink-reactnext/src/categories/color/react/index.ts @@ -0,0 +1 @@ +export * from './components/Swatch'; diff --git a/packages/web-kitchensink-reactnext/src/categories/navigation/react/components/LinkButton/index.tsx b/packages/web-kitchensink-reactnext/src/categories/navigation/react/components/LinkButton/index.tsx index 610763e..88af8e1 100644 --- a/packages/web-kitchensink-reactnext/src/categories/navigation/react/components/LinkButton/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/navigation/react/components/LinkButton/index.tsx @@ -74,20 +74,30 @@ export const LinkButton = React.forwardRef {subtext && ( - - {subtext} - + <> + + {' - '} + + + {subtext} + + )}
{badge && ( - - {badge} - + <> + + {' - '} + + + {badge} + + )} {menuItem && ( ( variant = 'default' as const, hiddenLabel = false, className, + children, ...etcProps }: ComboBoxProps, ref, ) => { const labelId = React.useId(); + const datalistId = React.useId(); return ( -
- + + {children} + +
- { - label && ( - - ) - } - {hint && ( -
+ + /> + { + label && ( + + ) + } + {hint && (
- {hint} +
+ {hint} +
-
- )} - {indicator && ( -
- {indicator} -
- )} - { - border && ( - - ) - } -
+ )} + {indicator && ( +
+ {indicator} +
+ )} + { + border && ( + + ) + } +
+ ); } ); diff --git a/packages/web-kitchensink-reactnext/src/categories/option/react/components/MenuSelect/index.tsx b/packages/web-kitchensink-reactnext/src/categories/option/react/components/MenuSelect/index.tsx index db9c7e1..65164c5 100644 --- a/packages/web-kitchensink-reactnext/src/categories/option/react/components/MenuSelect/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/option/react/components/MenuSelect/index.tsx @@ -1,23 +1,212 @@ import * as React from 'react'; +import * as TextControlBase from '@tesseract-design/web-base-textcontrol'; +import clsx from 'clsx'; type MenuSelectDerivedElement = HTMLSelectElement; -export interface MenuSelectProps extends React.HTMLProps { - +export interface MenuSelectProps extends Omit, 'size' | 'style' | 'label'> { + /** + * 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, + /** + * Size of the component. + */ + size?: TextControlBase.TextControlSize, + /** + * Additional description, usually graphical, indicating the nature of the component's value. + */ + indicator?: React.ReactNode, + /** + * Should the component display a border? + */ + border?: boolean, + /** + * Should the component occupy the whole width of its parent? + */ + block?: boolean, + /** + * Style of the component. + */ + variant?: TextControlBase.TextControlVariant, + /** + * Is the label hidden? + */ + hiddenLabel?: boolean, } -export const MenuSelect = React.forwardRef(({ - children, - ...etcProps -}, forwardedRef) => { - return ( -
- + { + label && ( + + ) + } + {hint && ( +
+
+ {hint} +
+
+ )} + {indicator && ( +
+ {indicator} +
+ )} + { + border && ( + + ) + } +
+ ); + } +); MenuSelect.displayName = 'MenuSelect'; diff --git a/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioButton/index.tsx b/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioButton/index.tsx index e15412e..f6f9c59 100644 --- a/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioButton/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioButton/index.tsx @@ -1,3 +1,145 @@ -export const RadioButton = () => ( - -); +import * as React from 'react'; +import clsx from 'clsx'; +import * as ButtonBase from '@tesseract-design/web-base-button'; +import styles from './style.module.css'; + +export type RadioButtonDerivedElement = HTMLInputElement; + +export interface RadioButtonProps extends Omit, 'type' | 'size'> { + block?: boolean; + compact?: boolean; + size?: ButtonBase.ButtonSize; + subtext?: React.ReactNode; + badge?: React.ReactNode; + variant: ButtonBase.ButtonVariant; +} + +export const RadioButton = React.forwardRef(({ + children, + block = false, + compact = false, + size = 'medium', + id: idProp, + className, + subtext, + badge, + variant, + style, + ...etcProps +}, forwardedRef) => { + const defaultId = React.useId(); + const id = idProp ?? defaultId; + return ( + <> + + + + ) +}); + +RadioButton.displayName = 'RadioButton'; diff --git a/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioButton/style.module.css b/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioButton/style.module.css new file mode 100644 index 0000000..0f0f79b --- /dev/null +++ b/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioButton/style.module.css @@ -0,0 +1,7 @@ +.radio-button + label > :first-child > :first-child { + display: none; +} + +.radio-button:checked + label > :first-child > :first-child { + display: block; +} diff --git a/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioTickBox/index.tsx b/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioTickBox/index.tsx index 417d395..8822da3 100644 --- a/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioTickBox/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioTickBox/index.tsx @@ -1,3 +1,88 @@ -export const RadioTickBox = () => ( - -); +import * as React from 'react'; +import clsx from 'clsx'; +import styles from './style.module.css'; + +export type RadioTickBoxDerivedElement = HTMLInputElement; + +export interface RadioTickBoxProps extends Omit, 'type' | 'size'> { + block?: boolean; + subtext?: React.ReactNode; + badge?: React.ReactNode; +} + +export const RadioTickBox = React.forwardRef(({ + children, + block = false, + id: idProp, + className, + subtext, + badge, + style, + ...etcProps +}, forwardedRef) => { + const defaultId = React.useId(); + const id = idProp ?? defaultId; + return ( +
+ + + + {subtext && ( +
+ {subtext} +
+ )} +
+ ) +}); + +RadioTickBox.displayName = 'RadioTickBox'; diff --git a/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioTickBox/style.module.css b/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioTickBox/style.module.css new file mode 100644 index 0000000..b7655b7 --- /dev/null +++ b/packages/web-kitchensink-reactnext/src/categories/option/react/components/RadioTickBox/style.module.css @@ -0,0 +1,7 @@ +.radio-tick-box + label + label > :first-child > :first-child { + display: none; +} + +.radio-tick-box:checked + label + label > :first-child > :first-child { + display: block; +} diff --git a/packages/web-kitchensink-reactnext/src/categories/option/react/components/TagInput/index.tsx b/packages/web-kitchensink-reactnext/src/categories/option/react/components/TagInput/index.tsx index d355256..712bd4e 100644 --- a/packages/web-kitchensink-reactnext/src/categories/option/react/components/TagInput/index.tsx +++ b/packages/web-kitchensink-reactnext/src/categories/option/react/components/TagInput/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import * as TextControlBase from '@tesseract-design/web-base-textcontrol'; import clsx from 'clsx'; +import {useEnhanced} from '@/packages/react-utils'; type TagInputDerivedElement = HTMLInputElement; @@ -41,6 +42,7 @@ export interface TagInputProps extends Omit( variant = 'default' as const, hiddenLabel = false, className, + enhanced: enhancedProp = false, ...etcProps }: TagInputProps, - ref, + forwardedRef, ) => { + const {enhanced} = useEnhanced({ enhanced: enhancedProp }); + const defaultRef = React.useRef(null); + const ref = forwardedRef ?? defaultRef; const labelId = React.useId(); + const dummyInputRef = React.useRef(null); + + const handleFocus: React.FocusEventHandler = (e) => { + if (!(typeof ref === 'object' && ref)) { + return; + } + const { current: input } = ref; + if (!input) { + return; + } + input.focus(); + }; + + const handleChange: React.ChangeEventHandler = (e) => { + if (!(typeof dummyInputRef === 'object' && dummyInputRef)) { + return; + } + const { current: input } = dummyInputRef; + if (!input) { + return; + } + input.value = e.currentTarget.value; + }; return (
( type={type} data-testid="input" className={clsx( - 'bg-negative rounded-inherit w-full peer block', - 'focus:outline-0', - 'disabled:opacity-50 disabled:cursor-not-allowed', - { - 'text-xxs': size === 'small', - 'text-xs': size === 'medium', - }, - { - 'pl-4': variant === 'default', - 'pl-1.5': variant === 'alternate', - }, - { - 'pt-4': variant === 'alternate', - }, - { - 'pr-4': variant === 'default' && !indicator, - 'pr-1.5': variant === 'alternate' && !indicator, - }, - { - 'pr-10': indicator && size === 'small', - 'pr-12': indicator && size === 'medium', - 'pr-16': indicator && size === 'large', - }, - { - 'h-10': size === 'small', - 'h-12': size === 'medium', - 'h-16': size === 'large', - }, + 'peer', + enhanced && 'sr-only', )} + onChange={handleChange} /> + {enhanced && ( + + )} { label && (