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 { DeesInputBase } from '../dees-input-base.js';
import { demoFunc } from './demo.js'; import { demoFunc } from './demo.js';
import { fileuploadStyles } from './styles.js'; import { fileuploadStyles } from './styles.js';
import { renderFileupload } from './template.js';
import '../dees-icon.js'; import '../dees-icon.js';
import '../dees-label.js'; import '../dees-label.js';
import { import {
customElement, customElement,
type TemplateResult, html,
property, property,
state,
type TemplateResult,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
declare global { declare global {
@@ -22,22 +22,17 @@ declare global {
export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> { export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
public static demo = demoFunc; public static demo = demoFunc;
@property({ attribute: false })
@property({
attribute: false,
})
public value: File[] = []; public value: File[] = [];
@property() @state()
public state: 'idle' | 'dragOver' | 'dropped' | 'uploading' | 'completed' = 'idle'; public state: 'idle' | 'dragOver' | 'dropped' | 'uploading' | 'completed' = 'idle';
@property({ type: Boolean }) @state()
public isLoading: boolean = false; public isLoading: boolean = false;
@property({ @property({ type: String })
type: String, public buttonText: string = 'Select files';
})
public buttonText: string = 'Upload File...';
@property({ type: String }) @property({ type: String })
public accept: string = ''; public accept: string = '';
@@ -54,24 +49,258 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
@property({ type: String, reflect: true }) @property({ type: String, reflect: true })
public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null; public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
constructor() { public validationMessage: string = '';
super();
} private previewUrlMap: WeakMap<File, string> = new WeakMap();
private dropArea: HTMLElement | null = null;
public static styles = fileuploadStyles; public static styles = fileuploadStyles;
public render(): TemplateResult { 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 { public formatFileSize(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB']; const units = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes'; if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024)); const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; const size = bytes / Math.pow(1024, exponent);
return `${Math.round(size * 100) / 100} ${units[exponent]}`;
} }
public getFileType(file: File): string { public getFileType(file: File): string {
@@ -88,81 +317,187 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
} }
public getFileIcon(file: File): string { public getFileIcon(file: File): string {
const type = this.getFileType(file); const fileType = this.getFileType(file);
const iconMap = { const iconMap: Record<string, string> = {
'image': 'lucide:image', image: 'lucide:FileImage',
'pdf': 'lucide:file-text', pdf: 'lucide:FileText',
'doc': 'lucide:file-text', doc: 'lucide:FileText',
'spreadsheet': 'lucide:table', spreadsheet: 'lucide:FileSpreadsheet',
'presentation': 'lucide:presentation', presentation: 'lucide:FileBarChart',
'video': 'lucide:video', video: 'lucide:FileVideo',
'audio': 'lucide:music', audio: 'lucide:FileAudio',
'archive': 'lucide:archive', archive: 'lucide:FileArchive',
'file': 'lucide:file' file: 'lucide:File',
}; };
return iconMap[type] || 'lucide:file'; return iconMap[fileType] ?? 'lucide:File';
} }
public canShowPreview(file: File): boolean { 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 { private validateFile(file: File): boolean {
// Check file size
if (this.maxSize > 0 && file.size > this.maxSize) { 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'; this.validationState = 'invalid';
return false; return false;
} }
// Check file type
if (this.accept) { if (this.accept) {
const acceptedTypes = this.accept.split(',').map(s => s.trim()); const acceptedTypes = this.accept
let isAccepted = false; .split(',')
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
for (const acceptType of acceptedTypes) { if (acceptedTypes.length > 0) {
if (acceptType.startsWith('.')) { let isAccepted = false;
// Extension check for (const acceptType of acceptedTypes) {
if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) { 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; isAccepted = true;
break; 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) { if (!isAccepted) {
this.validationMessage = `File type not accepted. Please upload: ${this.accept}`; this.validationMessage = `File type not accepted. Allowed: ${acceptedTypes.join(', ')}`;
this.validationState = 'invalid'; this.validationState = 'invalid';
return false; return false;
}
} }
} }
return true; return true;
} }
public async openFileSelector() { private getPreviewUrl(file: File): string {
if (this.disabled || this.isLoading) return; 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
this.isLoading = true; this.isLoading = true;
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]'); // 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;
}
// Set up a focus handler to detect when the dialog is closed without selection
const handleFocus = () => { const handleFocus = () => {
setTimeout(() => { setTimeout(() => {
// Check if no file was selected
if (!inputFile.files || inputFile.files.length === 0) { if (!inputFile.files || inputFile.files.length === 0) {
this.isLoading = false; this.isLoading = false;
} }
@@ -171,79 +506,52 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
}; };
window.addEventListener('focus', handleFocus); window.addEventListener('focus', handleFocus);
// Click the input to open file selector
inputFile.click(); inputFile.click();
} }
public removeFile(file: File) { public removeFile(file: File) {
const index = this.value.indexOf(file); const index = this.value.indexOf(file);
if (index > -1) { if (index > -1) {
this.releasePreview(file);
this.value.splice(index, 1); this.value.splice(index, 1);
this.requestUpdate(); this.requestUpdate('value');
this.validate(); void this.validate();
this.changeSubject.next(this); this.changeSubject.next(this);
} }
} }
public clearAll() { public clearAll() {
const existingFiles = [...this.value];
this.value = []; this.value = [];
this.requestUpdate(); existingFiles.forEach((file) => this.releasePreview(file));
this.validate(); this.requestUpdate('value');
void this.validate();
this.changeSubject.next(this); this.changeSubject.next(this);
this.buttonText = 'Select files';
} }
public async updateValue(eventArg: Event) { public async updateValue(eventArg: Event) {
const target: any = eventArg.target; const target = eventArg.target as HTMLInputElement;
this.value = target.value; this.value = Array.from(target.files ?? []);
this.changeSubject.next(this); this.changeSubject.next(this);
} }
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) { public setValue(value: File[]): void {
super.firstUpdated(_changedProperties); this.value.forEach((file) => this.releasePreview(file));
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]'); this.value = value;
inputFile.addEventListener('change', async (event: Event) => { if (value.length > 0) {
const target = event.target as HTMLInputElement; this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
const newFiles = Array.from(target.files); } else {
this.buttonText = 'Select files';
}
this.requestUpdate('value');
void this.validate();
}
// Always reset loading state when file dialog interaction completes public getValue(): File[] {
this.isLoading = false; return this.value;
await this.addFiles(newFiles);
// Reset the input value to allow selecting the same file again if needed
target.value = '';
});
// 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);
} }
private async addFiles(files: File[]) { private async addFiles(files: File[]) {
@@ -255,9 +563,11 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
} }
} }
if (filesToAdd.length === 0) return; if (filesToAdd.length === 0) {
this.isLoading = false;
return;
}
// Check max files limit
if (this.maxFiles > 0) { if (this.maxFiles > 0) {
const totalFiles = this.value.length + filesToAdd.length; const totalFiles = this.value.length + filesToAdd.length;
if (totalFiles > this.maxFiles) { if (totalFiles > this.maxFiles) {
@@ -265,6 +575,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
if (allowedCount <= 0) { if (allowedCount <= 0) {
this.validationMessage = `Maximum ${this.maxFiles} files allowed`; this.validationMessage = `Maximum ${this.maxFiles} files allowed`;
this.validationState = 'invalid'; this.validationState = 'invalid';
this.isLoading = false;
return; return;
} }
filesToAdd.splice(allowedCount); filesToAdd.splice(allowedCount);
@@ -273,20 +584,24 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
} }
} }
// Add files
if (!this.multiple && filesToAdd.length > 0) { if (!this.multiple && filesToAdd.length > 0) {
this.value.forEach((file) => this.releasePreview(file));
this.value = [filesToAdd[0]]; this.value = [filesToAdd[0]];
} else { } else {
this.value.push(...filesToAdd); this.value.push(...filesToAdd);
} }
this.requestUpdate(); this.validationMessage = '';
this.validate(); this.validationState = null;
this.requestUpdate('value');
await this.validate();
this.changeSubject.next(this); this.changeSubject.next(this);
this.isLoading = false;
// Update button text
if (this.value.length > 0) { if (this.value.length > 0) {
this.buttonText = this.multiple ? 'Add more files' : 'Replace file'; this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
} else {
this.buttonText = 'Select files';
} }
} }
@@ -299,36 +614,13 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
return false; return false;
} }
// Validate all files
for (const file of this.value) { for (const file of this.value) {
if (!this.validateFile(file)) { if (!this.validateFile(file)) {
return false; return false;
} }
} }
this.validationState = 'valid'; this.validationState = this.value.length > 0 ? 'valid' : null;
return true; 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 './component.js';
import '../dees-panel.js'; import '../dees-panel.js';
@@ -6,199 +6,152 @@ export const demoFunc = () => html`
<dees-demowrapper> <dees-demowrapper>
<style> <style>
${css` ${css`
.demo-container { .demo-shell {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 32px;
padding: 24px; padding: 24px;
max-width: 1200px; max-width: 1160px;
margin: 0 auto; margin: 0 auto;
} }
.upload-grid { .demo-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px; gap: 24px;
} }
@media (max-width: 768px) { @media (min-width: 960px) {
.upload-grid { .demo-grid--two {
grid-template-columns: 1fr; grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
.upload-box { .demo-stack {
padding: 16px; display: flex;
background: ${cssManager.bdTheme('#fff', '#2a2a2a')}; flex-direction: column;
border-radius: 4px; gap: 18px;
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444')};
} }
.upload-box h4 { .demo-note {
margin-top: 0; margin-top: 16px;
margin-bottom: 16px; padding: 16px;
color: ${cssManager.bdTheme('#333', '#fff')}; border-radius: 12px;
font-size: 16px; 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;
} }
.info-section { .demo-note strong {
margin-top: 32px; color: ${cssManager.bdTheme('hsl(217 91% 45%)', 'hsl(213 93% 68%)')};
padding: 16px; font-weight: 600;
background: ${cssManager.bdTheme('#fff3cd', '#332701')};
border: 1px solid ${cssManager.bdTheme('#ffeaa7', '#664400')};
border-radius: 4px;
color: ${cssManager.bdTheme('#856404', '#ffecb5')};
} }
`} `}
</style> </style>
<div class="demo-container"> <div class="demo-shell">
<dees-panel .title=${'1. Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}> <dees-panel
<dees-input-fileupload .title=${'Modern file uploader'}
.label=${'Attachments'} .subtitle=${'Shadcn-inspired layout with drag & drop, previews and validation'}
.description=${'Upload any files by clicking or dragging them here'} >
></dees-input-fileupload> <div class="demo-grid demo-grid--two">
<div class="demo-stack">
<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>
<dees-input-fileupload <dees-input-fileupload
.label=${'Profile Picture'} .label=${'Attachments'}
.description=${'JPG, PNG or GIF (max 5MB)'} .description=${'Upload supporting documents for your request'}
.accept=${'image/jpeg,image/png,image/gif'} .accept=${'image/*,.pdf,.zip'}
.maxSize=${5 * 1024 * 1024} .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} .multiple=${false}
.buttonText=${'Select Image'} .maxSize=${5 * 1024 * 1024}
.buttonText=${'Select cover image'}
></dees-input-fileupload> ></dees-input-fileupload>
</div> </div>
<div class="upload-box"> <div class="demo-stack">
<h4>Documents Only</h4>
<dees-input-fileupload <dees-input-fileupload
.label=${'Resume'} .label=${'Audio uploads'}
.description=${'PDF or Word documents only'} .description=${'Share podcast drafts (MP3/WAV, max 25MB each)'}
.accept=${".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"} .accept=${'audio/*'}
.buttonText=${'Select Document'} .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> ></dees-input-fileupload>
</div> </div>
</div> </div>
</dees-panel> </dees-panel>
<dees-panel .title=${'3. Validation & Limits'} .subtitle=${'File size limits and validation examples'}> <dees-panel
<dees-input-fileupload .title=${'Form integration'}
.label=${'Small Files Only'} .subtitle=${'Combine file uploads with the rest of the DEES form ecosystem'}
.description=${'Maximum file size: 1MB'} >
.maxSize=${1024 * 1024} <div class="demo-grid">
.buttonText=${'Upload Small File'} <dees-form>
></dees-input-fileupload> <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-fileupload <dees-input-text
.label=${'Limited Upload'} .label=${'Contact email'}
.description=${'Maximum 3 files, each up to 2MB'} .inputType=${'email'}
.maxFiles=${3} .required=${true}
.maxSize=${2 * 1024 * 1024} .key=${'contactEmail'}
></dees-input-fileupload> ></dees-input-text>
<dees-input-fileupload <dees-input-fileupload
.label=${'Required Upload'} .label=${'Statement of work'}
.description=${'This field is required'} .description=${'Upload a signed statement of work (PDF, max 15MB)'}
.required=${true} .required=${true}
></dees-input-fileupload> .accept=${'application/pdf'}
</dees-panel> .maxSize=${15 * 1024 * 1024}
.multiple=${false}
.key=${'sow'}
></dees-input-fileupload>
<dees-panel .title=${'4. States & Styling'} .subtitle=${'Different states and validation feedback'}> <dees-input-fileupload
<dees-input-fileupload .label=${'Creative references'}
.label=${'Disabled Upload'} .description=${'Optional. Upload up to five visual references'}
.description=${'File upload is currently disabled'} .accept=${'image/*'}
.disabled=${true} .maxFiles=${5}
></dees-input-fileupload> .maxSize=${8 * 1024 * 1024}
.key=${'references'}
></dees-input-fileupload>
<dees-input-fileupload <dees-input-text
.label=${'Pre-filled Example'} .label=${'Notes'}
.description=${'Component with pre-loaded files'} .description=${'Add optional context for reviewers'}
.value=${[ .inputType=${'textarea'}
new File(['Hello World'], 'example.txt', { type: 'text/plain' }), .key=${'notes'}
new File(['Test Data'], 'data.json', { type: 'application/json' }) ></dees-input-text>
]}
></dees-input-fileupload>
</dees-panel>
<dees-panel .title=${'5. Form Integration'} .subtitle=${'Complete form with various file upload scenarios'}> <dees-form-submit .text=${'Submit briefing'}></dees-form-submit>
<dees-form> </div>
<h3 style="margin-top: 0; margin-bottom: 24px; color: ${cssManager.bdTheme('#333', '#fff')};">Job Application Form</h3> </dees-form>
<dees-input-text <div class="demo-note">
.label=${'Full Name'} <strong>Good to know:</strong>
.required=${true} <ul>
.key=${'fullName'} <li>Drag & drop highlights the dropzone and supports keyboard activation.</li>
></dees-input-text> <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>
<dees-input-text <li>File size and file-count limits surface inline validation messages.</li>
.label=${'Email'} <li>The component stays compatible with <code>dees-form</code> value accessors.</li>
.inputType=${'email'} </ul>
.required=${true} </div>
.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>
</div> </div>
</dees-panel> </dees-panel>
</div> </div>

View File

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