Refactor dees-input-fileupload component and styles

- Updated demo.ts to enhance layout and styling, including renaming classes and adjusting spacing.
- Removed unused template rendering logic from template.ts.
- Simplified index.ts by removing the export of renderFileupload.
- Revamped styles in styles.ts for improved design consistency and responsiveness.
- Enhanced file upload functionality with better descriptions and validation messages.
This commit is contained in:
2025-09-19 17:31:26 +00:00
parent 987ae70e7a
commit 12b0aa0aad
5 changed files with 866 additions and 682 deletions

View File

@@ -1,15 +1,15 @@
import { DeesContextmenu } from '../dees-contextmenu.js';
import { DeesInputBase } from '../dees-input-base.js';
import { demoFunc } from './demo.js';
import { fileuploadStyles } from './styles.js';
import { renderFileupload } from './template.js';
import '../dees-icon.js';
import '../dees-label.js';
import {
customElement,
type TemplateResult,
html,
property,
state,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
@@ -22,22 +22,17 @@ declare global {
export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
public static demo = demoFunc;
@property({
attribute: false,
})
@property({ attribute: false })
public value: File[] = [];
@property()
@state()
public state: 'idle' | 'dragOver' | 'dropped' | 'uploading' | 'completed' = 'idle';
@property({ type: Boolean })
@state()
public isLoading: boolean = false;
@property({
type: String,
})
public buttonText: string = 'Upload File...';
@property({ type: String })
public buttonText: string = 'Select files';
@property({ type: String })
public accept: string = '';
@@ -54,24 +49,258 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
@property({ type: String, reflect: true })
public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
constructor() {
super();
}
public validationMessage: string = '';
private previewUrlMap: WeakMap<File, string> = new WeakMap();
private dropArea: HTMLElement | null = null;
public static styles = fileuploadStyles;
public render(): TemplateResult {
return renderFileupload(this);
const acceptedSummary = this.getAcceptedSummary();
const metaEntries: string[] = [
this.multiple ? 'Multiple files supported' : 'Single file only',
this.maxSize > 0 ? `Max ${this.formatFileSize(this.maxSize)}` : 'No size limit',
];
if (acceptedSummary) {
metaEntries.push(`Accepts ${acceptedSummary}`);
}
return html`
<div class="input-wrapper">
<dees-label
.label=${this.label}
.description=${this.description}
.required=${this.required}
></dees-label>
<div
class="dropzone ${this.state === 'dragOver' ? 'dropzone--active' : ''} ${this.disabled ? 'dropzone--disabled' : ''}"
role="button"
tabindex=${this.disabled ? -1 : 0}
aria-disabled=${this.disabled}
aria-label=${`Select files${acceptedSummary ? ` (${acceptedSummary})` : ''}`}
@click=${this.handleDropzoneClick}
@keydown=${this.handleDropzoneKeydown}
>
<input
class="file-input"
style="position: absolute; opacity: 0; pointer-events: none; width: 1px; height: 1px; top: 0; left: 0; overflow: hidden;"
type="file"
?multiple=${this.multiple}
accept=${this.accept || ''}
?disabled=${this.disabled}
@change=${this.handleFileInputChange}
tabindex="-1"
/>
<div class="dropzone__body">
<div class="dropzone__icon">
${this.isLoading
? html`<span class="dropzone__loader" aria-hidden="true"></span>`
: html`<dees-icon icon="lucide:FolderOpen"></dees-icon>`}
</div>
<div class="dropzone__content">
<span class="dropzone__headline">${this.buttonText || 'Select files'}</span>
<span class="dropzone__subline">
Drag and drop files here or
<button
type="button"
class="dropzone__browse"
@click=${this.handleBrowseClick}
?disabled=${this.disabled}
>
browse
</button>
</span>
</div>
</div>
<div class="dropzone__meta">
${metaEntries.map((entry) => html`<span>${entry}</span>`)}
</div>
</div>
${this.renderFileList()}
${this.validationMessage
? html`<div class="validation-message" aria-live="polite">${this.validationMessage}</div>`
: html``}
</div>
`;
}
public validationMessage: string = '';
private renderFileList(): TemplateResult {
if (this.value.length === 0) {
return html`
<div class="file-empty">
<div class="file-empty__title">No files selected</div>
<div class="file-empty__hint">
Selected files will be listed here
</div>
</div>
`;
}
return html`
<div class="file-list">
<div class="file-list__header">
<span>${this.value.length} file${this.value.length === 1 ? '' : 's'} selected</span>
${this.value.length > 0
? html`<button type="button" class="file-list__clear" @click=${this.handleClearAll}>Clear ${this.value.length > 1 ? 'all' : ''}</button>`
: html``}
</div>
<div class="file-list__items">
${this.value.map((file) => this.renderFileRow(file))}
</div>
</div>
`;
}
private renderFileRow(file: File): TemplateResult {
const fileType = this.getFileType(file);
const previewUrl = this.canShowPreview(file) ? this.getPreviewUrl(file) : null;
return html`
<div class="file-row ${fileType}-file">
<div class="file-thumb" aria-hidden="true">
${previewUrl
? html`<img class="thumb-image" src=${previewUrl} alt=${`Preview of ${file.name}`}>`
: html`<dees-icon icon=${this.getFileIcon(file)}></dees-icon>`}
</div>
<div class="file-meta">
<div class="file-name" title=${file.name}>${file.name}</div>
<div class="file-details">
<span class="file-size">${this.formatFileSize(file.size)}</span>
${fileType !== 'file' ? html`<span class="file-type">${fileType}</span>` : html``}
</div>
</div>
<div class="file-actions">
<button
type="button"
class="remove-button"
@click=${() => this.removeFile(file)}
aria-label=${`Remove ${file.name}`}
>
<dees-icon icon="lucide:X"></dees-icon>
</button>
</div>
</div>
`;
}
private handleFileInputChange = async (event: Event) => {
this.isLoading = false;
const target = event.target as HTMLInputElement;
const files = Array.from(target.files ?? []);
if (files.length > 0) {
await this.addFiles(files);
}
target.value = '';
};
private handleDropzoneClick = (event: MouseEvent) => {
if (this.disabled) {
return;
}
// Don't prevent default - let the click bubble
if ((event.target as HTMLElement).closest('.dropzone__browse')) {
return;
}
this.openFileSelector();
};
private handleBrowseClick = (event: MouseEvent) => {
if (this.disabled) {
return;
}
event.stopPropagation(); // Stop propagation to prevent double trigger
this.openFileSelector();
};
private handleDropzoneKeydown = (event: KeyboardEvent) => {
if (this.disabled) {
return;
}
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.openFileSelector();
}
};
private handleClearAll = (event: MouseEvent) => {
event.preventDefault();
this.clearAll();
};
private handleDragEvent = async (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
if (this.disabled) {
return;
}
if (event.type === 'dragenter' || event.type === 'dragover') {
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy';
}
this.state = 'dragOver';
return;
}
if (event.type === 'dragleave') {
if (!this.dropArea) {
this.state = 'idle';
return;
}
const rect = this.dropArea.getBoundingClientRect();
const { clientX = 0, clientY = 0 } = event;
if (clientX <= rect.left || clientX >= rect.right || clientY <= rect.top || clientY >= rect.bottom) {
this.state = 'idle';
}
return;
}
if (event.type === 'drop') {
this.state = 'idle';
const files = Array.from(event.dataTransfer?.files ?? []);
if (files.length > 0) {
await this.addFiles(files);
}
}
};
private attachDropListeners(): void {
if (!this.dropArea) {
return;
}
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
this.dropArea!.addEventListener(eventName, this.handleDragEvent);
});
}
private detachDropListeners(): void {
if (!this.dropArea) {
return;
}
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
this.dropArea!.removeEventListener(eventName, this.handleDragEvent);
});
}
private rebindInteractiveElements(): void {
const newDropArea = this.shadowRoot?.querySelector('.dropzone') as HTMLElement | null;
if (newDropArea !== this.dropArea) {
this.detachDropListeners();
this.dropArea = newDropArea;
this.attachDropListeners();
}
}
// Utility methods
public formatFileSize(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const units = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
const size = bytes / Math.pow(1024, exponent);
return `${Math.round(size * 100) / 100} ${units[exponent]}`;
}
public getFileType(file: File): string {
@@ -88,176 +317,257 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
}
public getFileIcon(file: File): string {
const type = this.getFileType(file);
const iconMap = {
'image': 'lucide:image',
'pdf': 'lucide:file-text',
'doc': 'lucide:file-text',
'spreadsheet': 'lucide:table',
'presentation': 'lucide:presentation',
'video': 'lucide:video',
'audio': 'lucide:music',
'archive': 'lucide:archive',
'file': 'lucide:file'
const fileType = this.getFileType(file);
const iconMap: Record<string, string> = {
image: 'lucide:FileImage',
pdf: 'lucide:FileText',
doc: 'lucide:FileText',
spreadsheet: 'lucide:FileSpreadsheet',
presentation: 'lucide:FileBarChart',
video: 'lucide:FileVideo',
audio: 'lucide:FileAudio',
archive: 'lucide:FileArchive',
file: 'lucide:File',
};
return iconMap[type] || 'lucide:file';
return iconMap[fileType] ?? 'lucide:File';
}
public canShowPreview(file: File): boolean {
return file.type.startsWith('image/') && file.size < 5 * 1024 * 1024; // 5MB limit for previews
return file.type.startsWith('image/') && file.size < 5 * 1024 * 1024;
}
private validateFile(file: File): boolean {
// Check file size
if (this.maxSize > 0 && file.size > this.maxSize) {
this.validationMessage = `File "${file.name}" exceeds maximum size of ${this.formatFileSize(this.maxSize)}`;
this.validationMessage = `File "${file.name}" exceeds the maximum size of ${this.formatFileSize(this.maxSize)}`;
this.validationState = 'invalid';
return false;
}
// Check file type
if (this.accept) {
const acceptedTypes = this.accept.split(',').map(s => s.trim());
let isAccepted = false;
for (const acceptType of acceptedTypes) {
if (acceptType.startsWith('.')) {
// Extension check
if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) {
const acceptedTypes = this.accept
.split(',')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
if (acceptedTypes.length > 0) {
let isAccepted = false;
for (const acceptType of acceptedTypes) {
if (acceptType.startsWith('.')) {
if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) {
isAccepted = true;
break;
}
} else if (acceptType.endsWith('/*')) {
const prefix = acceptType.slice(0, -2);
if (file.type.startsWith(prefix)) {
isAccepted = true;
break;
}
} else if (file.type === acceptType) {
isAccepted = true;
break;
}
} else if (acceptType.endsWith('/*')) {
// MIME type wildcard check
const mimePrefix = acceptType.slice(0, -2);
if (file.type.startsWith(mimePrefix)) {
isAccepted = true;
break;
}
} else if (file.type === acceptType) {
// Exact MIME type check
isAccepted = true;
break;
}
}
if (!isAccepted) {
this.validationMessage = `File type not accepted. Please upload: ${this.accept}`;
this.validationState = 'invalid';
return false;
if (!isAccepted) {
this.validationMessage = `File type not accepted. Allowed: ${acceptedTypes.join(', ')}`;
this.validationState = 'invalid';
return false;
}
}
}
return true;
}
private getPreviewUrl(file: File): string {
let url = this.previewUrlMap.get(file);
if (!url) {
url = URL.createObjectURL(file);
this.previewUrlMap.set(file, url);
}
return url;
}
private releasePreview(file: File): void {
const url = this.previewUrlMap.get(file);
if (url) {
URL.revokeObjectURL(url);
this.previewUrlMap.delete(file);
}
}
private getAcceptedSummary(): string | null {
if (!this.accept) {
return null;
}
const formatted = Array.from(
new Set(
this.accept
.split(',')
.map((token) => token.trim())
.filter((token) => token.length > 0)
.map((token) => this.formatAcceptToken(token))
)
).filter(Boolean);
if (formatted.length === 0) {
return null;
}
if (formatted.length === 1) {
return formatted[0];
}
if (formatted.length === 2) {
return `${formatted[0]}, ${formatted[1]}`;
}
return `${formatted.slice(0, 2).join(', ')}`;
}
private formatAcceptToken(token: string): string {
if (token === '*/*') {
return 'All files';
}
if (token.endsWith('/*')) {
const family = token.split('/')[0];
if (!family) {
return 'All files';
}
return `${family.charAt(0).toUpperCase()}${family.slice(1)} files`;
}
if (token.startsWith('.')) {
return token.slice(1).toUpperCase();
}
if (token.includes('pdf')) return 'PDF';
if (token.includes('zip')) return 'ZIP';
if (token.includes('json')) return 'JSON';
if (token.includes('msword')) return 'DOC';
if (token.includes('wordprocessingml')) return 'DOCX';
if (token.includes('excel')) return 'XLS';
if (token.includes('presentation')) return 'PPT';
const segments = token.split('/');
const lastSegment = segments.pop() ?? token;
return lastSegment.toUpperCase();
}
private attachLifecycleListeners(): void {
this.rebindInteractiveElements();
}
public firstUpdated(changedProperties: Map<string, unknown>) {
super.firstUpdated(changedProperties);
this.attachLifecycleListeners();
}
public updated(changedProperties: Map<string, unknown>) {
super.updated(changedProperties);
if (changedProperties.has('value')) {
void this.validate();
}
this.rebindInteractiveElements();
}
public async disconnectedCallback(): Promise<void> {
this.detachDropListeners();
this.value.forEach((file) => this.releasePreview(file));
this.previewUrlMap = new WeakMap();
await super.disconnectedCallback();
}
public async openFileSelector() {
if (this.disabled || this.isLoading) return;
// Set loading state
if (this.disabled || this.isLoading) {
return;
}
this.isLoading = true;
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
// Set up a focus handler to detect when the dialog is closed without selection
// Ensure we have the latest reference to the file input
const inputFile = this.shadowRoot?.querySelector('.file-input') as HTMLInputElement | null;
if (!inputFile) {
this.isLoading = false;
return;
}
const handleFocus = () => {
setTimeout(() => {
// Check if no file was selected
if (!inputFile.files || inputFile.files.length === 0) {
this.isLoading = false;
}
window.removeEventListener('focus', handleFocus);
}, 300);
};
window.addEventListener('focus', handleFocus);
// Click the input to open file selector
inputFile.click();
}
public removeFile(file: File) {
const index = this.value.indexOf(file);
if (index > -1) {
this.releasePreview(file);
this.value.splice(index, 1);
this.requestUpdate();
this.validate();
this.requestUpdate('value');
void this.validate();
this.changeSubject.next(this);
}
}
public clearAll() {
const existingFiles = [...this.value];
this.value = [];
this.requestUpdate();
this.validate();
existingFiles.forEach((file) => this.releasePreview(file));
this.requestUpdate('value');
void this.validate();
this.changeSubject.next(this);
this.buttonText = 'Select files';
}
public async updateValue(eventArg: Event) {
const target: any = eventArg.target;
this.value = target.value;
const target = eventArg.target as HTMLInputElement;
this.value = Array.from(target.files ?? []);
this.changeSubject.next(this);
}
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
super.firstUpdated(_changedProperties);
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
inputFile.addEventListener('change', async (event: Event) => {
const target = event.target as HTMLInputElement;
const newFiles = Array.from(target.files);
// Always reset loading state when file dialog interaction completes
this.isLoading = false;
await this.addFiles(newFiles);
// Reset the input value to allow selecting the same file again if needed
target.value = '';
});
public setValue(value: File[]): void {
this.value.forEach((file) => this.releasePreview(file));
this.value = value;
if (value.length > 0) {
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
} else {
this.buttonText = 'Select files';
}
this.requestUpdate('value');
void this.validate();
}
// Handle drag and drop
const dropArea = this.shadowRoot.querySelector('.maincontainer');
const handlerFunction = async (eventArg: DragEvent) => {
eventArg.preventDefault();
eventArg.stopPropagation();
switch (eventArg.type) {
case 'dragenter':
case 'dragover':
this.state = 'dragOver';
break;
case 'dragleave':
// Check if we're actually leaving the drop area
const rect = dropArea.getBoundingClientRect();
const x = eventArg.clientX;
const y = eventArg.clientY;
if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) {
this.state = 'idle';
}
break;
case 'drop':
this.state = 'idle';
const files = Array.from(eventArg.dataTransfer.files);
await this.addFiles(files);
break;
}
};
dropArea.addEventListener('dragenter', handlerFunction, false);
dropArea.addEventListener('dragleave', handlerFunction, false);
dropArea.addEventListener('dragover', handlerFunction, false);
dropArea.addEventListener('drop', handlerFunction, false);
public getValue(): File[] {
return this.value;
}
private async addFiles(files: File[]) {
const filesToAdd: File[] = [];
for (const file of files) {
if (this.validateFile(file)) {
filesToAdd.push(file);
}
}
if (filesToAdd.length === 0) return;
if (filesToAdd.length === 0) {
this.isLoading = false;
return;
}
// Check max files limit
if (this.maxFiles > 0) {
const totalFiles = this.value.length + filesToAdd.length;
if (totalFiles > this.maxFiles) {
@@ -265,6 +575,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
if (allowedCount <= 0) {
this.validationMessage = `Maximum ${this.maxFiles} files allowed`;
this.validationState = 'invalid';
this.isLoading = false;
return;
}
filesToAdd.splice(allowedCount);
@@ -273,62 +584,43 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
}
}
// Add files
if (!this.multiple && filesToAdd.length > 0) {
this.value.forEach((file) => this.releasePreview(file));
this.value = [filesToAdd[0]];
} else {
this.value.push(...filesToAdd);
}
this.requestUpdate();
this.validate();
this.validationMessage = '';
this.validationState = null;
this.requestUpdate('value');
await this.validate();
this.changeSubject.next(this);
// Update button text
this.isLoading = false;
if (this.value.length > 0) {
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
} else {
this.buttonText = 'Select files';
}
}
public async validate(): Promise<boolean> {
this.validationMessage = '';
if (this.required && this.value.length === 0) {
this.validationState = 'invalid';
this.validationMessage = 'Please select at least one file';
return false;
}
// Validate all files
for (const file of this.value) {
if (!this.validateFile(file)) {
return false;
}
}
this.validationState = 'valid';
this.validationState = this.value.length > 0 ? 'valid' : null;
return true;
}
public getValue(): File[] {
return this.value;
}
public setValue(value: File[]): void {
this.value = value;
this.requestUpdate();
if (value.length > 0) {
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
} else {
this.buttonText = 'Upload File...';
}
}
public updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('value')) {
this.validate();
}
}
}

View File

@@ -1,4 +1,4 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import { css, cssManager, html } from '@design.estate/dees-element';
import './component.js';
import '../dees-panel.js';
@@ -6,201 +6,154 @@ export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
.demo-shell {
display: flex;
flex-direction: column;
gap: 24px;
gap: 32px;
padding: 24px;
max-width: 1200px;
max-width: 1160px;
margin: 0 auto;
}
.upload-grid {
.demo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 768px) {
.upload-grid {
grid-template-columns: 1fr;
@media (min-width: 960px) {
.demo-grid--two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.upload-box {
padding: 16px;
background: ${cssManager.bdTheme('#fff', '#2a2a2a')};
border-radius: 4px;
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444')};
.demo-stack {
display: flex;
flex-direction: column;
gap: 18px;
}
.upload-box h4 {
margin-top: 0;
margin-bottom: 16px;
color: ${cssManager.bdTheme('#333', '#fff')};
font-size: 16px;
}
.info-section {
margin-top: 32px;
.demo-note {
margin-top: 16px;
padding: 16px;
background: ${cssManager.bdTheme('#fff3cd', '#332701')};
border: 1px solid ${cssManager.bdTheme('#ffeaa7', '#664400')};
border-radius: 4px;
color: ${cssManager.bdTheme('#856404', '#ffecb5')};
border-radius: 12px;
border: 1px solid ${cssManager.bdTheme('hsl(217 91% 90%)', 'hsl(215 20% 26%)')};
background: ${cssManager.bdTheme('hsl(213 100% 97%)', 'hsl(215 20% 12%)')};
color: ${cssManager.bdTheme('hsl(215 25% 32%)', 'hsl(215 20% 82%)')};
font-size: 13px;
line-height: 1.55;
}
.demo-note strong {
color: ${cssManager.bdTheme('hsl(217 91% 45%)', 'hsl(213 93% 68%)')};
font-weight: 600;
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'1. Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}>
<dees-input-fileupload
.label=${'Attachments'}
.description=${'Upload any files by clicking or dragging them here'}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Single File Only'}
.description=${'Only one file can be uploaded at a time'}
.multiple=${false}
.buttonText=${'Choose File'}
></dees-input-fileupload>
</dees-panel>
<dees-panel .title=${'2. File Type Restrictions'} .subtitle=${'Upload areas with specific file type requirements'}>
<div class="upload-grid">
<div class="upload-box">
<h4>Images Only</h4>
<div class="demo-shell">
<dees-panel
.title=${'Modern file uploader'}
.subtitle=${'Shadcn-inspired layout with drag & drop, previews and validation'}
>
<div class="demo-grid demo-grid--two">
<div class="demo-stack">
<dees-input-fileupload
.label=${'Profile Picture'}
.description=${'JPG, PNG or GIF (max 5MB)'}
.accept=${'image/jpeg,image/png,image/gif'}
.maxSize=${5 * 1024 * 1024}
.label=${'Attachments'}
.description=${'Upload supporting documents for your request'}
.accept=${'image/*,.pdf,.zip'}
.maxSize=${10 * 1024 * 1024}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Brand assets'}
.description=${'Upload high-resolution imagery (JPG/PNG)'}
.accept=${'image/jpeg,image/png'}
.multiple=${false}
.buttonText=${'Select Image'}
.maxSize=${5 * 1024 * 1024}
.buttonText=${'Select cover image'}
></dees-input-fileupload>
</div>
<div class="upload-box">
<h4>Documents Only</h4>
<div class="demo-stack">
<dees-input-fileupload
.label=${'Resume'}
.description=${'PDF or Word documents only'}
.accept=${".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"}
.buttonText=${'Select Document'}
.label=${'Audio uploads'}
.description=${'Share podcast drafts (MP3/WAV, max 25MB each)'}
.accept=${'audio/*'}
.maxSize=${25 * 1024 * 1024}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Disabled example'}
.description=${'Uploader is disabled while moderation is pending'}
.disabled=${true}
></dees-input-fileupload>
</div>
</div>
</dees-panel>
<dees-panel .title=${'3. Validation & Limits'} .subtitle=${'File size limits and validation examples'}>
<dees-input-fileupload
.label=${'Small Files Only'}
.description=${'Maximum file size: 1MB'}
.maxSize=${1024 * 1024}
.buttonText=${'Upload Small File'}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Limited Upload'}
.description=${'Maximum 3 files, each up to 2MB'}
.maxFiles=${3}
.maxSize=${2 * 1024 * 1024}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Required Upload'}
.description=${'This field is required'}
.required=${true}
></dees-input-fileupload>
</dees-panel>
<dees-panel .title=${'4. States & Styling'} .subtitle=${'Different states and validation feedback'}>
<dees-input-fileupload
.label=${'Disabled Upload'}
.description=${'File upload is currently disabled'}
.disabled=${true}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Pre-filled Example'}
.description=${'Component with pre-loaded files'}
.value=${[
new File(['Hello World'], 'example.txt', { type: 'text/plain' }),
new File(['Test Data'], 'data.json', { type: 'application/json' })
]}
></dees-input-fileupload>
</dees-panel>
<dees-panel .title=${'5. Form Integration'} .subtitle=${'Complete form with various file upload scenarios'}>
<dees-form>
<h3 style="margin-top: 0; margin-bottom: 24px; color: ${cssManager.bdTheme('#333', '#fff')};">Job Application Form</h3>
<dees-input-text
.label=${'Full Name'}
.required=${true}
.key=${'fullName'}
></dees-input-text>
<dees-input-text
.label=${'Email'}
.inputType=${'email'}
.required=${true}
.key=${'email'}
></dees-input-text>
<dees-input-fileupload
.label=${'Resume'}
.description=${'Required: PDF format only (max 10MB)'}
.required=${true}
.accept=${'application/pdf'}
.maxSize=${10 * 1024 * 1024}
.multiple=${false}
.key=${'resume'}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Portfolio'}
.description=${'Optional: Upload up to 5 work samples (images or PDFs, max 5MB each)'}
.accept=${'image/*,application/pdf'}
.maxFiles=${5}
.maxSize=${5 * 1024 * 1024}
.key=${'portfolio'}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'References'}
.description=${'Upload reference letters (optional)'}
.accept=${".pdf,.doc,.docx"}
.key=${'references'}
></dees-input-fileupload>
<dees-input-text
.label=${'Additional Comments'}
.inputType=${'textarea'}
.description=${'Any additional information you would like to share'}
.key=${'comments'}
></dees-input-text>
<dees-form-submit .text=${'Submit Application'}></dees-form-submit>
</dees-form>
<div class="info-section">
<h4 style="margin-top: 0;">Enhanced Features:</h4>
<ul style="margin: 0; padding-left: 20px;">
<li>Drag & drop with visual feedback</li>
<li>File type restrictions via accept attribute</li>
<li>File size validation with custom limits</li>
<li>Maximum file count restrictions</li>
<li>Image preview thumbnails</li>
<li>File type-specific icons</li>
<li>Clear all button for multiple files</li>
<li>Proper validation states and messages</li>
<li>Keyboard accessible</li>
<li>Single or multiple file modes</li>
</ul>
<dees-panel
.title=${'Form integration'}
.subtitle=${'Combine file uploads with the rest of the DEES form ecosystem'}
>
<div class="demo-grid">
<dees-form>
<div class="demo-stack">
<dees-input-text
.label=${'Project name'}
.description=${'How should we refer to this project internally?'}
.required=${true}
.key=${'projectName'}
></dees-input-text>
<dees-input-text
.label=${'Contact email'}
.inputType=${'email'}
.required=${true}
.key=${'contactEmail'}
></dees-input-text>
<dees-input-fileupload
.label=${'Statement of work'}
.description=${'Upload a signed statement of work (PDF, max 15MB)'}
.required=${true}
.accept=${'application/pdf'}
.maxSize=${15 * 1024 * 1024}
.multiple=${false}
.key=${'sow'}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Creative references'}
.description=${'Optional. Upload up to five visual references'}
.accept=${'image/*'}
.maxFiles=${5}
.maxSize=${8 * 1024 * 1024}
.key=${'references'}
></dees-input-fileupload>
<dees-input-text
.label=${'Notes'}
.description=${'Add optional context for reviewers'}
.inputType=${'textarea'}
.key=${'notes'}
></dees-input-text>
<dees-form-submit .text=${'Submit briefing'}></dees-form-submit>
</div>
</dees-form>
<div class="demo-note">
<strong>Good to know:</strong>
<ul>
<li>Drag & drop highlights the dropzone and supports keyboard activation.</li>
<li>Accepted file types are summarised automatically from the <code>accept</code> attribute.</li>
<li>Image uploads show live previews generated via <code>URL.createObjectURL</code>.</li>
<li>File size and file-count limits surface inline validation messages.</li>
<li>The component stays compatible with <code>dees-form</code> value accessors.</li>
</ul>
</div>
</div>
</dees-panel>
</div>
</dees-demowrapper>
`;
`;

View File

@@ -1,3 +1,2 @@
export * from './component.js';
export { fileuploadStyles } from './styles.js';
export { renderFileupload } from './template.js';

View File

@@ -2,308 +2,332 @@ import { css, cssManager } from '@design.estate/dees-element';
import { DeesInputBase } from '../dees-input-base.js';
export const fileuploadStyles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
:host {
position: relative;
display: block;
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
}
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
:host {
position: relative;
display: block;
}
.hidden {
display: none;
}
.input-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-wrapper {
display: flex;
flex-direction: column;
gap: 12px;
}
.maincontainer {
position: relative;
border-radius: 6px;
padding: 16px;
background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(215 20.2% 11.8%)')};
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
border: 1px solid ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')};
transition: all 0.15s ease;
}
.dropzone {
position: relative;
padding: 20px;
border-radius: 12px;
border: 1.5px dashed ${cssManager.bdTheme('hsl(215 16% 80%)', 'hsl(217 20% 25%)')};
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20% 12%)')};
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
cursor: pointer;
outline: none;
}
.maincontainer:hover {
border-color: ${cssManager.bdTheme('hsl(215 20.2% 55.1%)', 'hsl(215 20.2% 45.1%)')};
}
.dropzone:focus-visible {
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20% 12%)')},
0 0 0 4px ${cssManager.bdTheme('hsl(217 91% 60% / 0.5)', 'hsl(213 93% 68% / 0.4)')};
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
}
:host([disabled]) .maincontainer {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.dropzone--active {
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
box-shadow: 0 12px 32px ${cssManager.bdTheme('rgba(15, 23, 42, 0.12)', 'rgba(0, 0, 0, 0.35)')};
background: ${cssManager.bdTheme('hsl(217 91% 60% / 0.06)', 'hsl(213 93% 68% / 0.12)')};
}
:host([validationState="invalid"]) .maincontainer {
border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
}
.dropzone--disabled {
opacity: 0.6;
pointer-events: none;
cursor: not-allowed;
}
:host([validationState="valid"]) .maincontainer {
border-color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
}
.dropzone__body {
display: flex;
align-items: center;
gap: 16px;
}
:host([validationState="warn"]) .maincontainer {
border-color: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
}
.dropzone__icon {
width: 48px;
height: 48px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
background: ${cssManager.bdTheme('hsl(217 91% 60% / 0.12)', 'hsl(213 93% 68% / 0.12)')};
position: relative;
flex-shrink: 0;
}
.maincontainer::after {
top: 1px;
right: 1px;
left: 1px;
bottom: 1px;
transform: scale3d(0.98, 0.95, 1);
position: absolute;
content: '';
display: block;
border: 2px dashed transparent;
border-radius: 5px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
background: transparent;
}
.maincontainer.dragOver {
border-color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.05)', 'hsl(213.1 93.9% 67.8% / 0.05)')};
}
.maincontainer.dragOver::after {
transform: scale3d(1, 1, 1);
border: 2px dashed ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
}
.dropzone__icon dees-icon {
font-size: 22px;
}
.uploadButton {
position: relative;
padding: 10px 20px;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 7.8%)')};
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px;
text-align: center;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
line-height: 20px;
}
.dropzone__loader {
width: 20px;
height: 20px;
border-radius: 999px;
border: 2px solid ${cssManager.bdTheme('rgba(15, 23, 42, 0.15)', 'rgba(255, 255, 255, 0.15)')};
border-top-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
animation: loader-spin 0.6s linear infinite;
}
.uploadButton:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
}
.dropzone__content {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.uploadButton:active {
background: ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 11%)')};
}
.dropzone__headline {
font-size: 15px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(222 47% 11%)', 'hsl(210 20% 96%)')};
}
.uploadButton dees-icon {
font-size: 16px;
}
.dropzone__subline {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')};
}
.files-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.dropzone__browse {
appearance: none;
border: none;
background: none;
padding: 0;
margin-left: 4px;
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
font-weight: 600;
cursor: pointer;
text-decoration: none;
}
.uploadCandidate {
display: grid;
grid-template-columns: 40px 1fr auto;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20.2% 16.8%)')};
padding: 12px;
text-align: left;
border-radius: 6px;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
cursor: default;
transition: all 0.15s ease;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
position: relative;
overflow: hidden;
}
.dropzone__browse:hover {
text-decoration: underline;
}
.uploadCandidate:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(215 20.2% 20.8%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
}
.dropzone__browse:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.uploadCandidate .icon {
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
}
.dropzone__meta {
margin-top: 14px;
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 12px;
color: ${cssManager.bdTheme('hsl(215 16% 50%)', 'hsl(215 16% 72%)')};
}
.uploadCandidate.image-file .icon {
color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')};
}
.uploadCandidate.pdf-file .icon {
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
}
.uploadCandidate.doc-file .icon {
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
}
.dropzone__meta span {
padding: 4px 10px;
border-radius: 999px;
background: ${cssManager.bdTheme('hsl(217 91% 95%)', 'hsl(213 93% 18%)')};
border: 1px solid ${cssManager.bdTheme('hsl(217 91% 90%)', 'hsl(213 93% 24%)')};
}
.uploadCandidate .info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.uploadCandidate .filename {
font-weight: 500;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.uploadCandidate .filesize {
font-size: 12px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
}
.file-empty {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0;
}
.uploadCandidate .actions {
display: flex;
align-items: center;
gap: 8px;
}
.file-empty__title {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(215 16% 40%)', 'hsl(215 16% 70%)')};
}
.remove-button {
width: 32px;
height: 32px;
border-radius: 4px;
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
}
.file-empty__hint {
font-size: 12px;
color: ${cssManager.bdTheme('hsl(215 16% 50%)', 'hsl(215 16% 65%)')};
line-height: 1.5;
padding: 12px 16px;
border-radius: 8px;
background: ${cssManager.bdTheme('hsl(210 20% 97%)', 'hsl(215 20% 15%)')};
border: 1px solid ${cssManager.bdTheme('hsl(213 27% 92%)', 'hsl(217 20% 22%)')};
}
.remove-button:hover {
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')};
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
}
.file-list {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 0 0 0;
}
.clear-all-button {
margin-bottom: 8px;
text-align: right;
}
.file-list__header {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(215 16% 40%)', 'hsl(215 16% 70%)')};
}
.clear-all-button button {
background: none;
border: none;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
cursor: pointer;
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.15s ease;
}
.file-list__clear {
appearance: none;
border: none;
background: none;
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
cursor: pointer;
font-weight: 500;
font-size: 13px;
padding: 0;
}
.clear-all-button button:hover {
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')};
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
}
.file-list__clear:hover {
text-decoration: underline;
}
.validation-message {
font-size: 13px;
margin-top: 6px;
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
line-height: 1.5;
}
.file-list__items {
display: flex;
flex-direction: column;
gap: 12px;
}
.drop-hint {
text-align: center;
padding: 40px 20px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
font-size: 14px;
}
.file-row {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 14px;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20% 16%)')};
border: 1px solid ${cssManager.bdTheme('hsl(213 27% 90%)', 'hsl(217 25% 28%)')};
border-radius: 10px;
box-shadow: ${cssManager.bdTheme('0 1px 2px rgba(15, 23, 42, 0.04)', '0 1px 2px rgba(0, 0, 0, 0.24)')};
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
.drop-hint dees-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.2;
}
.file-row:hover {
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
box-shadow: ${cssManager.bdTheme('0 10px 24px rgba(15, 23, 42, 0.08)', '0 12px 30px rgba(0, 0, 0, 0.35)')};
transform: translateY(-1px);
}
.image-preview {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
}
.file-thumb {
width: 48px;
height: 48px;
border-radius: 12px;
background: ${cssManager.bdTheme('hsl(214 31% 92%)', 'hsl(217 32% 18%)')};
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
}
.description-text {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
margin-top: 6px;
line-height: 1.5;
}
.file-thumb dees-icon {
font-size: 20px;
color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 70%)')};
display: block;
width: 20px;
height: 20px;
line-height: 1;
flex-shrink: 0;
}
/* Loading state styles */
.uploadButton.loading {
pointer-events: none;
opacity: 0.8;
}
.uploadButton .button-content {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.thumb-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
border-top-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
.file-meta {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.file-name {
font-weight: 600;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(222 47% 11%)', 'hsl(210 20% 96%)')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.02);
opacity: 0.9;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.file-details {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
font-size: 12px;
color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')};
}
.uploadButton.loading {
animation: pulse 1s ease-in-out infinite;
}
`,
];
.file-size {
font-variant-numeric: tabular-nums;
}
.file-type {
padding: 2px 8px;
border-radius: 999px;
border: 1px solid ${cssManager.bdTheme('hsl(214 31% 86%)', 'hsl(217 32% 28%)')};
color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')};
text-transform: uppercase;
letter-spacing: 0.08em;
line-height: 1;
}
.file-actions {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.remove-button {
width: 32px;
height: 32px;
border-radius: 8px;
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s ease, transform 0.15s ease, color 0.15s ease;
color: ${cssManager.bdTheme('hsl(215 16% 52%)', 'hsl(215 16% 68%)')};
}
.remove-button:hover {
background: ${cssManager.bdTheme('hsl(0 72% 50% / 0.12)', 'hsl(0 62% 32% / 0.2)')};
color: ${cssManager.bdTheme('hsl(0 72% 46%)', 'hsl(0 70% 70%)')};
}
.remove-button:active {
transform: scale(0.96);
}
.remove-button dees-icon {
display: block;
width: 16px;
height: 16px;
font-size: 16px;
line-height: 1;
flex-shrink: 0;
}
.validation-message {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(0 72% 40%)', 'hsl(0 70% 68%)')};
line-height: 1.5;
}
@keyframes loader-spin {
to {
transform: rotate(360deg);
}
}
`,
];

View File

@@ -1,84 +0,0 @@
import { html, type TemplateResult } from '@design.estate/dees-element';
import type { DeesInputFileupload } from './component.js';
export const renderFileupload = (component: DeesInputFileupload): TemplateResult => {
const hasFiles = component.value.length > 0;
const showClearAll = hasFiles && component.value.length > 1;
return html`
<div class="input-wrapper">
${component.label ? html`
<dees-label .label=${component.label}></dees-label>
` : ''}
<div class="hidden">
<input
type="file"
?multiple=${component.multiple}
accept="${component.accept}"
>
</div>
<div class="maincontainer ${component.state === 'dragOver' ? 'dragOver' : ''}">
${hasFiles ? html`
${showClearAll ? html`
<div class="clear-all-button">
<button @click=${component.clearAll}>Clear All</button>
</div>
` : ''}
<div class="files-container">
${component.value.map((fileArg) => {
const fileType = component.getFileType(fileArg);
const isImage = fileType === 'image';
return html`
<div class="uploadCandidate ${fileType}-file">
<div class="icon">
${isImage && component.canShowPreview(fileArg) ? html`
<img class="image-preview" src="${URL.createObjectURL(fileArg)}" alt="${fileArg.name}">
` : html`
<dees-icon .icon=${component.getFileIcon(fileArg)}></dees-icon>
`}
</div>
<div class="info">
<div class="filename" title="${fileArg.name}">${fileArg.name}</div>
<div class="filesize">${component.formatFileSize(fileArg.size)}</div>
</div>
<div class="actions">
<button
class="remove-button"
@click=${() => component.removeFile(fileArg)}
title="Remove file"
>
<dees-icon .icon=${'lucide:x'}></dees-icon>
</button>
</div>
</div>
`;
})}
</div>
` : html`
<div class="drop-hint">
<dees-icon .icon=${'lucide:cloud-upload'}></dees-icon>
<div>Drag files here or click to browse</div>
</div>
`}
<div class="uploadButton ${component.isLoading ? 'loading' : ''}" @click=${component.openFileSelector}>
<div class="button-content">
${component.isLoading ? html`
<div class="loading-spinner"></div>
<span>Opening...</span>
` : html`
<dees-icon .icon=${'lucide:upload'}></dees-icon>
${component.buttonText}
`}
</div>
</div>
</div>
${component.description ? html`
<div class="description-text">${component.description}</div>
` : ''}
${component.validationState === 'invalid' && component.validationMessage ? html`
<div class="validation-message">${component.validationMessage}</div>
` : ''}
</div>
`;
};