From 12b0aa0aad50f7cf196e8d3a64e9ad448338da55 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Fri, 19 Sep 2025 17:31:26 +0000 Subject: [PATCH] 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. --- .../dees-input-fileupload/component.ts | 608 +++++++++++++----- ts_web/elements/dees-input-fileupload/demo.ts | 295 ++++----- .../elements/dees-input-fileupload/index.ts | 1 - .../elements/dees-input-fileupload/styles.ts | 560 ++++++++-------- .../dees-input-fileupload/template.ts | 84 --- 5 files changed, 866 insertions(+), 682 deletions(-) delete mode 100644 ts_web/elements/dees-input-fileupload/template.ts diff --git a/ts_web/elements/dees-input-fileupload/component.ts b/ts_web/elements/dees-input-fileupload/component.ts index 0dc8528..c3e6db3 100644 --- a/ts_web/elements/dees-input-fileupload/component.ts +++ b/ts_web/elements/dees-input-fileupload/component.ts @@ -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 { 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 { @property({ type: String, reflect: true }) public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null; - constructor() { - super(); - } + public validationMessage: string = ''; + + private previewUrlMap: WeakMap = 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` +
+ +
+ +
+
+ ${this.isLoading + ? html`` + : html``} +
+
+ ${this.buttonText || 'Select files'} + + Drag and drop files here or + + +
+
+
+ ${metaEntries.map((entry) => html`${entry}`)} +
+
+ ${this.renderFileList()} + ${this.validationMessage + ? html`
${this.validationMessage}
` + : html``} +
+ `; } - public validationMessage: string = ''; + private renderFileList(): TemplateResult { + if (this.value.length === 0) { + return html` +
+
No files selected
+
+ Selected files will be listed here +
+
+ `; + } + + return html` +
+
+ ${this.value.length} file${this.value.length === 1 ? '' : 's'} selected + ${this.value.length > 0 + ? html`` + : html``} +
+
+ ${this.value.map((file) => this.renderFileRow(file))} +
+
+ `; + } + + private renderFileRow(file: File): TemplateResult { + const fileType = this.getFileType(file); + const previewUrl = this.canShowPreview(file) ? this.getPreviewUrl(file) : null; + + return html` +
+ +
+
${file.name}
+
+ ${this.formatFileSize(file.size)} + ${fileType !== 'file' ? html`${fileType}` : html``} +
+
+
+ +
+
+ `; + } + + 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 { } 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 = { + 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) { + super.firstUpdated(changedProperties); + this.attachLifecycleListeners(); + } + + public updated(changedProperties: Map) { + super.updated(changedProperties); + if (changedProperties.has('value')) { + void this.validate(); + } + this.rebindInteractiveElements(); + } + + public async disconnectedCallback(): Promise { + 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) { - 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 { 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 { } } - // 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 { 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) { - super.updated(changedProperties); - - if (changedProperties.has('value')) { - this.validate(); - } - } } diff --git a/ts_web/elements/dees-input-fileupload/demo.ts b/ts_web/elements/dees-input-fileupload/demo.ts index ab31cf0..ae11366 100644 --- a/ts_web/elements/dees-input-fileupload/demo.ts +++ b/ts_web/elements/dees-input-fileupload/demo.ts @@ -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` - -
- - - - - - - -
-
-

Images Only

+ +
+ +
+
+ +
- -
-

Documents Only

+ +
+ +
- - - - - - - - - - - - - - - - - -

Job Application Form

- - - - - - - - - - - - - - -
- -
-

Enhanced Features:

-
    -
  • Drag & drop with visual feedback
  • -
  • File type restrictions via accept attribute
  • -
  • File size validation with custom limits
  • -
  • Maximum file count restrictions
  • -
  • Image preview thumbnails
  • -
  • File type-specific icons
  • -
  • Clear all button for multiple files
  • -
  • Proper validation states and messages
  • -
  • Keyboard accessible
  • -
  • Single or multiple file modes
  • -
+ + +
+ +
+ + + + + + + + + + + +
+
+ +
+ Good to know: +
    +
  • Drag & drop highlights the dropzone and supports keyboard activation.
  • +
  • Accepted file types are summarised automatically from the accept attribute.
  • +
  • Image uploads show live previews generated via URL.createObjectURL.
  • +
  • File size and file-count limits surface inline validation messages.
  • +
  • The component stays compatible with dees-form value accessors.
  • +
+
-`; \ No newline at end of file +`; diff --git a/ts_web/elements/dees-input-fileupload/index.ts b/ts_web/elements/dees-input-fileupload/index.ts index 89a9f3e..c748b21 100644 --- a/ts_web/elements/dees-input-fileupload/index.ts +++ b/ts_web/elements/dees-input-fileupload/index.ts @@ -1,3 +1,2 @@ export * from './component.js'; export { fileuploadStyles } from './styles.js'; -export { renderFileupload } from './template.js'; diff --git a/ts_web/elements/dees-input-fileupload/styles.ts b/ts_web/elements/dees-input-fileupload/styles.ts index fa18845..f9c6a1f 100644 --- a/ts_web/elements/dees-input-fileupload/styles.ts +++ b/ts_web/elements/dees-input-fileupload/styles.ts @@ -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); + } + } + `, +]; diff --git a/ts_web/elements/dees-input-fileupload/template.ts b/ts_web/elements/dees-input-fileupload/template.ts deleted file mode 100644 index 6ba7100..0000000 --- a/ts_web/elements/dees-input-fileupload/template.ts +++ /dev/null @@ -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` -
- ${component.label ? html` - - ` : ''} - -
- ${hasFiles ? html` - ${showClearAll ? html` -
- -
- ` : ''} -
- ${component.value.map((fileArg) => { - const fileType = component.getFileType(fileArg); - const isImage = fileType === 'image'; - return html` -
-
- ${isImage && component.canShowPreview(fileArg) ? html` - ${fileArg.name} - ` : html` - - `} -
-
-
${fileArg.name}
-
${component.formatFileSize(fileArg.size)}
-
-
- -
-
- `; - })} -
- ` : html` -
- -
Drag files here or click to browse
-
- `} -
-
- ${component.isLoading ? html` -
- Opening... - ` : html` - - ${component.buttonText} - `} -
-
-
- ${component.description ? html` -
${component.description}
- ` : ''} - ${component.validationState === 'invalid' && component.validationMessage ? html` -
${component.validationMessage}
- ` : ''} -
- `; - -};