mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
code: fakemui,tsx,timepicker (6 files)
This commit is contained in:
111
fakemui/fakemui/inputs/ColorPicker.tsx
Normal file
111
fakemui/fakemui/inputs/ColorPicker.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { forwardRef, useState, useCallback } from 'react'
|
||||
import { FormLabel } from './FormLabel'
|
||||
import { FormHelperText } from './FormHelperText'
|
||||
|
||||
export interface ColorPickerProps {
|
||||
label?: React.ReactNode
|
||||
helperText?: React.ReactNode
|
||||
error?: boolean
|
||||
value?: string
|
||||
onChange?: (value: string) => void
|
||||
disabled?: boolean
|
||||
required?: boolean
|
||||
className?: string
|
||||
showInput?: boolean
|
||||
presetColors?: string[]
|
||||
alpha?: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_PRESET_COLORS = [
|
||||
'#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff',
|
||||
'#ffff00', '#ff00ff', '#00ffff', '#ff8000', '#8000ff',
|
||||
'#0080ff', '#ff0080', '#808080', '#c0c0c0', '#400000',
|
||||
'#004000', '#000040', '#404000', '#400040', '#004040'
|
||||
]
|
||||
|
||||
export const ColorPicker = forwardRef<HTMLInputElement, ColorPickerProps>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
helperText,
|
||||
error,
|
||||
value = '#000000',
|
||||
onChange,
|
||||
disabled,
|
||||
required,
|
||||
className = '',
|
||||
showInput = true,
|
||||
presetColors = DEFAULT_PRESET_COLORS,
|
||||
alpha = false,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [localValue, setLocalValue] = useState(value)
|
||||
|
||||
const handleColorChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value
|
||||
setLocalValue(newValue)
|
||||
onChange?.(newValue)
|
||||
}, [onChange])
|
||||
|
||||
const handleTextChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value
|
||||
setLocalValue(newValue)
|
||||
if (/^#[0-9A-Fa-f]{6}$/.test(newValue) || /^#[0-9A-Fa-f]{8}$/.test(newValue)) {
|
||||
onChange?.(newValue)
|
||||
}
|
||||
}, [onChange])
|
||||
|
||||
const handlePresetClick = useCallback((color: string) => {
|
||||
setLocalValue(color)
|
||||
onChange?.(color)
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<div className={`color-picker ${error ? 'color-picker--error' : ''} ${disabled ? 'color-picker--disabled' : ''} ${className}`}>
|
||||
{label && <FormLabel required={required}>{label}</FormLabel>}
|
||||
<div className="color-picker__controls">
|
||||
<input
|
||||
ref={ref}
|
||||
type="color"
|
||||
value={localValue.slice(0, 7)}
|
||||
onChange={handleColorChange}
|
||||
disabled={disabled}
|
||||
className="color-picker__input"
|
||||
{...props}
|
||||
/>
|
||||
{showInput && (
|
||||
<input
|
||||
type="text"
|
||||
value={localValue}
|
||||
onChange={handleTextChange}
|
||||
disabled={disabled}
|
||||
className="color-picker__text"
|
||||
placeholder="#000000"
|
||||
maxLength={alpha ? 9 : 7}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{presetColors.length > 0 && (
|
||||
<div className="color-picker__presets">
|
||||
{presetColors.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
className={`color-picker__preset ${localValue === color ? 'color-picker__preset--active' : ''}`}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={() => handlePresetClick(color)}
|
||||
disabled={disabled}
|
||||
aria-label={`Select color ${color}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{helperText && <FormHelperText error={error}>{helperText}</FormHelperText>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ColorPicker.displayName = 'ColorPicker'
|
||||
66
fakemui/fakemui/inputs/DatePicker.tsx
Normal file
66
fakemui/fakemui/inputs/DatePicker.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { forwardRef, useState, useRef, useEffect } from 'react'
|
||||
import { FormLabel } from './FormLabel'
|
||||
import { FormHelperText } from './FormHelperText'
|
||||
import { Input } from './Input'
|
||||
|
||||
export interface DatePickerProps {
|
||||
label?: React.ReactNode
|
||||
helperText?: React.ReactNode
|
||||
error?: boolean
|
||||
value?: string
|
||||
onChange?: (value: string) => void
|
||||
min?: string
|
||||
max?: string
|
||||
disabled?: boolean
|
||||
required?: boolean
|
||||
className?: string
|
||||
format?: 'date' | 'datetime-local' | 'month' | 'week'
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
helperText,
|
||||
error,
|
||||
value,
|
||||
onChange,
|
||||
min,
|
||||
max,
|
||||
disabled,
|
||||
required,
|
||||
className = '',
|
||||
format = 'date',
|
||||
placeholder,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange?.(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`date-picker ${error ? 'date-picker--error' : ''} ${className}`}>
|
||||
{label && <FormLabel required={required}>{label}</FormLabel>}
|
||||
<Input
|
||||
ref={ref}
|
||||
type={format}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
min={min}
|
||||
max={max}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
error={error}
|
||||
placeholder={placeholder}
|
||||
{...props}
|
||||
/>
|
||||
{helperText && <FormHelperText error={error}>{helperText}</FormHelperText>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
DatePicker.displayName = 'DatePicker'
|
||||
196
fakemui/fakemui/inputs/FileUpload.tsx
Normal file
196
fakemui/fakemui/inputs/FileUpload.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import React, { forwardRef, useCallback, useRef, useState } from 'react'
|
||||
import { FormLabel } from './FormLabel'
|
||||
import { FormHelperText } from './FormHelperText'
|
||||
|
||||
export interface FileUploadProps {
|
||||
label?: React.ReactNode
|
||||
helperText?: React.ReactNode
|
||||
error?: boolean
|
||||
value?: File | File[]
|
||||
onChange?: (files: File[]) => void
|
||||
onRemove?: (file: File) => void
|
||||
disabled?: boolean
|
||||
required?: boolean
|
||||
className?: string
|
||||
accept?: string
|
||||
multiple?: boolean
|
||||
maxSize?: number
|
||||
maxFiles?: number
|
||||
dragDrop?: boolean
|
||||
showPreview?: boolean
|
||||
variant?: 'default' | 'button' | 'dropzone'
|
||||
}
|
||||
|
||||
export const FileUpload = forwardRef<HTMLInputElement, FileUploadProps>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
helperText,
|
||||
error,
|
||||
onChange,
|
||||
onRemove,
|
||||
disabled,
|
||||
required,
|
||||
className = '',
|
||||
accept,
|
||||
multiple = false,
|
||||
maxSize,
|
||||
maxFiles,
|
||||
dragDrop = true,
|
||||
showPreview = true,
|
||||
variant = 'default',
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
|
||||
const validateFiles = useCallback((fileList: File[]): File[] => {
|
||||
let validFiles = fileList
|
||||
|
||||
if (maxSize) {
|
||||
validFiles = validFiles.filter((file) => {
|
||||
if (file.size > maxSize) {
|
||||
setErrorMessage(`File ${file.name} exceeds max size of ${maxSize / 1024 / 1024}MB`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
if (maxFiles && validFiles.length > maxFiles) {
|
||||
setErrorMessage(`Maximum ${maxFiles} files allowed`)
|
||||
validFiles = validFiles.slice(0, maxFiles)
|
||||
}
|
||||
|
||||
return validFiles
|
||||
}, [maxSize, maxFiles])
|
||||
|
||||
const handleFiles = useCallback((fileList: FileList | null) => {
|
||||
if (!fileList) return
|
||||
setErrorMessage(null)
|
||||
|
||||
const newFiles = Array.from(fileList)
|
||||
const validFiles = validateFiles(newFiles)
|
||||
|
||||
if (multiple) {
|
||||
const combined = [...files, ...validFiles]
|
||||
const limited = maxFiles ? combined.slice(0, maxFiles) : combined
|
||||
setFiles(limited)
|
||||
onChange?.(limited)
|
||||
} else {
|
||||
setFiles(validFiles.slice(0, 1))
|
||||
onChange?.(validFiles.slice(0, 1))
|
||||
}
|
||||
}, [files, multiple, maxFiles, onChange, validateFiles])
|
||||
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
handleFiles(e.target.files)
|
||||
}, [handleFiles])
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
if (!disabled && dragDrop) {
|
||||
setIsDragging(true)
|
||||
}
|
||||
}, [disabled, dragDrop])
|
||||
|
||||
const handleDragLeave = useCallback(() => {
|
||||
setIsDragging(false)
|
||||
}, [])
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(false)
|
||||
if (!disabled && dragDrop) {
|
||||
handleFiles(e.dataTransfer.files)
|
||||
}
|
||||
}, [disabled, dragDrop, handleFiles])
|
||||
|
||||
const handleRemove = useCallback((file: File) => {
|
||||
const newFiles = files.filter((f) => f !== file)
|
||||
setFiles(newFiles)
|
||||
onChange?.(newFiles)
|
||||
onRemove?.(file)
|
||||
}, [files, onChange, onRemove])
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
inputRef.current?.click()
|
||||
}, [])
|
||||
|
||||
const displayError = errorMessage || (error ? helperText : null)
|
||||
|
||||
return (
|
||||
<div className={`file-upload file-upload--${variant} ${error || errorMessage ? 'file-upload--error' : ''} ${disabled ? 'file-upload--disabled' : ''} ${className}`}>
|
||||
{label && <FormLabel required={required}>{label}</FormLabel>}
|
||||
|
||||
<div
|
||||
className={`file-upload__dropzone ${isDragging ? 'file-upload__dropzone--dragging' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<input
|
||||
ref={(el) => {
|
||||
if (typeof ref === 'function') ref(el)
|
||||
else if (ref) ref.current = el
|
||||
;(inputRef as React.MutableRefObject<HTMLInputElement | null>).current = el
|
||||
}}
|
||||
type="file"
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
className="file-upload__input"
|
||||
{...props}
|
||||
/>
|
||||
<div className="file-upload__content">
|
||||
<span className="file-upload__icon">📁</span>
|
||||
<span className="file-upload__text">
|
||||
{isDragging
|
||||
? 'Drop files here'
|
||||
: dragDrop
|
||||
? 'Drag and drop or click to upload'
|
||||
: 'Click to upload'}
|
||||
</span>
|
||||
{accept && <span className="file-upload__hint">Accepted: {accept}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showPreview && files.length > 0 && (
|
||||
<div className="file-upload__preview">
|
||||
{files.map((file, index) => (
|
||||
<div key={`${file.name}-${index}`} className="file-upload__file">
|
||||
<span className="file-upload__filename">{file.name}</span>
|
||||
<span className="file-upload__size">
|
||||
{(file.size / 1024).toFixed(1)} KB
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="file-upload__remove"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleRemove(file)
|
||||
}}
|
||||
disabled={disabled}
|
||||
aria-label={`Remove ${file.name}`}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{displayError && <FormHelperText error>{displayError}</FormHelperText>}
|
||||
{!displayError && helperText && <FormHelperText>{helperText}</FormHelperText>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
FileUpload.displayName = 'FileUpload'
|
||||
67
fakemui/fakemui/inputs/TimePicker.tsx
Normal file
67
fakemui/fakemui/inputs/TimePicker.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { forwardRef } from 'react'
|
||||
import { FormLabel } from './FormLabel'
|
||||
import { FormHelperText } from './FormHelperText'
|
||||
import { Input } from './Input'
|
||||
|
||||
export interface TimePickerProps {
|
||||
label?: React.ReactNode
|
||||
helperText?: React.ReactNode
|
||||
error?: boolean
|
||||
value?: string
|
||||
onChange?: (value: string) => void
|
||||
min?: string
|
||||
max?: string
|
||||
disabled?: boolean
|
||||
required?: boolean
|
||||
className?: string
|
||||
step?: number
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export const TimePicker = forwardRef<HTMLInputElement, TimePickerProps>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
helperText,
|
||||
error,
|
||||
value,
|
||||
onChange,
|
||||
min,
|
||||
max,
|
||||
disabled,
|
||||
required,
|
||||
className = '',
|
||||
step,
|
||||
placeholder,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange?.(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`time-picker ${error ? 'time-picker--error' : ''} ${className}`}>
|
||||
{label && <FormLabel required={required}>{label}</FormLabel>}
|
||||
<Input
|
||||
ref={ref}
|
||||
type="time"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
min={min}
|
||||
max={max}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
error={error}
|
||||
step={step}
|
||||
placeholder={placeholder}
|
||||
{...props}
|
||||
/>
|
||||
{helperText && <FormHelperText error={error}>{helperText}</FormHelperText>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
TimePicker.displayName = 'TimePicker'
|
||||
@@ -21,3 +21,7 @@ export { Autocomplete } from './Autocomplete'
|
||||
export { Rating } from './Rating'
|
||||
export { ButtonBase, InputBase, FilledInput, OutlinedInput } from './InputBase'
|
||||
export { FormField } from './FormField'
|
||||
export { DatePicker } from './DatePicker'
|
||||
export { TimePicker } from './TimePicker'
|
||||
export { ColorPicker } from './ColorPicker'
|
||||
export { FileUpload } from './FileUpload'
|
||||
|
||||
305
fakemui/styles/components/Pickers.module.scss
Normal file
305
fakemui/styles/components/Pickers.module.scss
Normal file
@@ -0,0 +1,305 @@
|
||||
// DatePicker styles
|
||||
.date-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs, 4px);
|
||||
|
||||
&--error {
|
||||
.input {
|
||||
border-color: var(--color-error, #d32f2f);
|
||||
}
|
||||
}
|
||||
|
||||
input[type="date"],
|
||||
input[type="datetime-local"],
|
||||
input[type="month"],
|
||||
input[type="week"] {
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TimePicker styles
|
||||
.time-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs, 4px);
|
||||
|
||||
&--error {
|
||||
.input {
|
||||
border-color: var(--color-error, #d32f2f);
|
||||
}
|
||||
}
|
||||
|
||||
input[type="time"] {
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ColorPicker styles
|
||||
.color-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
|
||||
&--error {
|
||||
.color-picker__text {
|
||||
border-color: var(--color-error, #d32f2f);
|
||||
}
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
padding: 0;
|
||||
border: 2px solid var(--color-border, #e0e0e0);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
|
||||
&::-webkit-color-swatch-wrapper {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
}
|
||||
|
||||
&::-moz-color-swatch {
|
||||
border: none;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary, #1976d2);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary, #1976d2);
|
||||
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
padding: var(--spacing-sm, 8px) var(--spacing-md, 12px);
|
||||
border: 1px solid var(--color-border, #e0e0e0);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: var(--font-size-sm, 14px);
|
||||
background: var(--color-bg, #ffffff);
|
||||
color: var(--color-text, #000000);
|
||||
text-transform: uppercase;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary, #1976d2);
|
||||
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&__presets {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-xs, 4px);
|
||||
padding: var(--spacing-xs, 4px);
|
||||
border: 1px solid var(--color-border, #e0e0e0);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
background: var(--color-bg-subtle, #f5f5f5);
|
||||
}
|
||||
|
||||
&__preset {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&--active {
|
||||
border-color: var(--color-primary, #1976d2);
|
||||
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.3);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary, #1976d2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FileUpload styles
|
||||
.file-upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
|
||||
&--error {
|
||||
.file-upload__dropzone {
|
||||
border-color: var(--color-error, #d32f2f);
|
||||
background: rgba(211, 47, 47, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__dropzone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-xl, 32px);
|
||||
border: 2px dashed var(--color-border, #e0e0e0);
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
background: var(--color-bg-subtle, #f5f5f5);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary, #1976d2);
|
||||
background: rgba(25, 118, 210, 0.05);
|
||||
}
|
||||
|
||||
&--dragging {
|
||||
border-color: var(--color-primary, #1976d2);
|
||||
background: rgba(25, 118, 210, 0.1);
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 48px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: var(--font-size-md, 16px);
|
||||
color: var(--color-text, #000000);
|
||||
}
|
||||
|
||||
&__hint {
|
||||
font-size: var(--font-size-sm, 14px);
|
||||
color: var(--color-text-muted, #666666);
|
||||
}
|
||||
|
||||
&__preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs, 4px);
|
||||
padding: var(--spacing-sm, 8px);
|
||||
border: 1px solid var(--color-border, #e0e0e0);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
background: var(--color-bg, #ffffff);
|
||||
}
|
||||
|
||||
&__file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
padding: var(--spacing-xs, 4px) var(--spacing-sm, 8px);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
background: var(--color-bg-subtle, #f5f5f5);
|
||||
}
|
||||
|
||||
&__filename {
|
||||
flex: 1;
|
||||
font-size: var(--font-size-sm, 14px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__size {
|
||||
font-size: var(--font-size-xs, 12px);
|
||||
color: var(--color-text-muted, #666666);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--color-error, #d32f2f);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-error-dark, #b71c1c);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// Variants
|
||||
&--button {
|
||||
.file-upload__dropzone {
|
||||
flex-direction: row;
|
||||
padding: var(--spacing-sm, 8px) var(--spacing-md, 12px);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.file-upload__content {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.file-upload__icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user