feat(dees-input-fileupload): redesign the file upload dropzone with dees-tile integration and themed file list styling

This commit is contained in:
2026-04-05 00:00:35 +00:00
parent a7a710b320
commit 465f7585ac
5 changed files with 147 additions and 178 deletions

View File

@@ -1,5 +1,12 @@
# Changelog # Changelog
## 2026-04-05 - 3.57.0 - feat(dees-input-fileupload)
redesign the file upload dropzone with dees-tile integration and themed file list styling
- Replace the custom dropzone container with dees-tile and move actions and metadata into header and footer slots
- Add an explicit empty state for the file list and simplify file list layout and interaction handling
- Adopt shared theme tokens in the file upload styles and introduce a reusable row hover color token
## 2026-04-04 - 3.56.1 - fix(dees-input-dropdown) ## 2026-04-04 - 3.56.1 - fix(dees-input-dropdown)
improve dropdown popup lifecycle with window layer overlay and animated visibility transitions improve dropdown popup lifecycle with window layer overlay and animated visibility transitions

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@design.estate/dees-catalog', name: '@design.estate/dees-catalog',
version: '3.56.1', version: '3.57.0',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
} }

View File

@@ -3,6 +3,7 @@ import { demoFunc } from './demo.js';
import { fileuploadStyles } from './styles.js'; import { fileuploadStyles } from './styles.js';
import '../../00group-utility/dees-icon/dees-icon.js'; import '../../00group-utility/dees-icon/dees-icon.js';
import '../../00group-layout/dees-label/dees-label.js'; import '../../00group-layout/dees-label/dees-label.js';
import '../../00group-layout/dees-tile/dees-tile.js';
import { import {
customElement, customElement,
@@ -75,14 +76,13 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
.description=${this.description} .description=${this.description}
.required=${this.required} .required=${this.required}
></dees-label> ></dees-label>
<div <dees-tile
class="dropzone ${this.state === 'dragOver' ? 'dropzone--active' : ''} ${this.disabled ? 'dropzone--disabled' : ''} ${this.value.length > 0 ? 'dropzone--has-files' : ''}" class="${this.state === 'dragOver' ? 'dragover' : ''}"
role="button" @click=${this.handleDropzoneClick}
@keydown=${this.handleDropzoneKeydown}
tabindex=${this.disabled ? -1 : 0} tabindex=${this.disabled ? -1 : 0}
aria-disabled=${this.disabled} aria-disabled=${this.disabled}
aria-label=${`Select files${acceptedSummary ? ` (${acceptedSummary})` : ''}`} aria-label=${`Select files${acceptedSummary ? ` (${acceptedSummary})` : ''}`}
@click=${this.handleDropzoneClick}
@keydown=${this.handleDropzoneKeydown}
> >
<input <input
class="file-input" class="file-input"
@@ -94,32 +94,23 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
@change=${this.handleFileInputChange} @change=${this.handleFileInputChange}
tabindex="-1" tabindex="-1"
/> />
<div class="dropzone__body"> <div slot="header" class="dropzone-header">
<div class="dropzone__icon"> ${this.isLoading
${this.isLoading ? html`<span class="dropzone-loader" aria-hidden="true"></span>`
? html`<span class="dropzone__loader" aria-hidden="true"></span>` : html`<dees-icon icon="lucide:Upload"></dees-icon>`}
: html`<dees-icon icon="lucide:FolderOpen"></dees-icon>`} <span class="dropzone-title">Drop files here or</span>
</div> <button
<div class="dropzone__content"> type="button"
<span class="dropzone__headline">${this.buttonText || 'Select files'}</span> class="dropzone-browse"
<span class="dropzone__subline"> @click=${(e: MouseEvent) => { e.stopPropagation(); this.openFileSelector(); }}
Drag and drop files here or ?disabled=${this.disabled}
<button >browse</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.renderFileList()}
</div> <div slot="footer" class="dropzone-footer">
${metaEntries.map((entry) => html`<span class="meta-chip">${entry}</span>`)}
</div>
</dees-tile>
${this.validationMessage ${this.validationMessage
? html`<div class="validation-message" aria-live="polite">${this.validationMessage}</div>` ? html`<div class="validation-message" aria-live="polite">${this.validationMessage}</div>`
: html``} : html``}
@@ -129,20 +120,21 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
private renderFileList(): TemplateResult { private renderFileList(): TemplateResult {
if (this.value.length === 0) { if (this.value.length === 0) {
return html``; return html`
<div class="file-list-empty">
<dees-icon icon="lucide:FileStack"></dees-icon>
<span>No files selected</span>
</div>
`;
} }
return html` return html`
<div class="file-list"> <div class="file-list">
<div class="file-list__header"> <div class="file-list-header">
<span>${this.value.length} file${this.value.length === 1 ? '' : 's'} selected</span> <span>${this.value.length} file${this.value.length === 1 ? '' : 's'} selected</span>
${this.value.length > 0 <button type="button" class="file-list-clear" @click=${this.handleClearAll}>Clear ${this.value.length > 1 ? 'all' : ''}</button>
? 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>
${this.value.map((file) => this.renderFileRow(file))}
</div> </div>
`; `;
} }
@@ -193,21 +185,14 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
if (this.disabled) { if (this.disabled) {
return; return;
} }
// Don't open file selector if clicking on the browse button or file list // Don't open file selector when clicking file list items or actions
if ((event.target as HTMLElement).closest('.dropzone__browse, .file-list')) { const target = event.target as HTMLElement;
if (target.closest('.file-list, .dropzone-header, .dropzone-footer')) {
return; return;
} }
this.openFileSelector(); this.openFileSelector();
}; };
private handleBrowseClick = (event: MouseEvent) => {
if (this.disabled) {
return;
}
event.stopPropagation(); // Stop propagation to prevent double trigger
this.openFileSelector();
};
private handleDropzoneKeydown = (event: KeyboardEvent) => { private handleDropzoneKeydown = (event: KeyboardEvent) => {
if (this.disabled) { if (this.disabled) {
return; return;
@@ -280,7 +265,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
} }
private rebindInteractiveElements(): void { private rebindInteractiveElements(): void {
const newDropArea = this.shadowRoot?.querySelector('.dropzone') as HTMLElement | null; const newDropArea = this.shadowRoot?.querySelector('dees-tile') as HTMLElement | null;
if (newDropArea !== this.dropArea) { if (newDropArea !== this.dropArea) {
this.detachDropListeners(); this.detachDropListeners();

View File

@@ -1,201 +1,158 @@
import { css, cssManager } from '@design.estate/dees-element'; import { css, cssManager } from '@design.estate/dees-element';
import { DeesInputBase } from '../dees-input-base/dees-input-base.js'; import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
import { themeDefaultStyles } from '../../00theme.js';
export const fileuploadStyles = [ export const fileuploadStyles = [
cssManager.defaultStyles, themeDefaultStyles,
...DeesInputBase.baseStyles, ...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css` css`
:host { :host {
position: relative; position: relative;
display: block; display: block;
} }
.input-wrapper { .input-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
} }
.dropzone { /* ── Tile integration ── */
position: relative; dees-tile {
padding: 20px; cursor: default;
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;
} }
.dropzone:focus-visible { dees-tile:hover::part(outer) {
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20% 12%)')}, border-color: var(--dees-color-border-strong);
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%)')};
} }
.dropzone--active { dees-tile.dragover::part(outer) {
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')}; border-color: var(--dees-color-accent-primary);
box-shadow: 0 12px 32px ${cssManager.bdTheme('rgba(15, 23, 42, 0.12)', 'rgba(0, 0, 0, 0.35)')}; box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(217 91% 60% / 0.15)', 'hsl(213 93% 68% / 0.15)')};
background: ${cssManager.bdTheme('hsl(217 91% 60% / 0.06)', 'hsl(213 93% 68% / 0.12)')};
} }
.dropzone--has-files { :host([disabled]) dees-tile {
background: ${cssManager.bdTheme('hsl(0 0% 99%)', 'hsl(215 20% 11%)')};
}
.dropzone--disabled {
opacity: 0.6; opacity: 0.6;
pointer-events: none;
cursor: not-allowed; cursor: not-allowed;
pointer-events: none;
} }
.dropzone__body { /* ── Header slot: sleek toolbar ── */
.dropzone-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 8px;
height: 32px;
padding: 0 12px;
} }
.dropzone__icon { .dropzone-header dees-icon {
width: 48px; width: 14px;
height: 48px; height: 14px;
border-radius: 16px; color: var(--dees-color-text-muted);
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;
} }
.dropzone__icon dees-icon { .dropzone-loader {
font-size: 22px; width: 14px;
} height: 14px;
border-radius: var(--dees-radius-full);
.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: 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%)')}; border-top-color: var(--dees-color-accent-primary);
animation: loader-spin 0.6s linear infinite; animation: loader-spin 0.6s linear infinite;
} }
.dropzone__content { .dropzone-title {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.dropzone__headline {
font-size: 15px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(222 47% 11%)', 'hsl(210 20% 96%)')};
}
.dropzone__subline {
font-size: 13px; font-size: 13px;
color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')}; color: var(--dees-color-text-muted);
} }
.dropzone__browse { .dropzone-browse {
appearance: none; appearance: none;
border: none; border: none;
background: none; background: none;
padding: 0; padding: 0;
margin-left: 4px; font-size: 13px;
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')}; font-family: inherit;
font-weight: 600; font-weight: 600;
color: var(--dees-color-accent-primary);
cursor: pointer; cursor: pointer;
text-decoration: none;
} }
.dropzone__browse:hover { .dropzone-browse:hover {
text-decoration: underline; text-decoration: underline;
} }
.dropzone__browse:disabled { .dropzone-browse:disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.6; opacity: 0.5;
} }
.dropzone__meta { /* ── Content slot: file list in rounded inset ── */
margin-top: 14px; .file-list-empty {
display: flex; display: flex;
flex-wrap: wrap; flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px; gap: 8px;
font-size: 12px; padding: 24px 16px;
color: ${cssManager.bdTheme('hsl(215 16% 50%)', 'hsl(215 16% 72%)')}; color: var(--dees-color-text-muted);
font-size: 13px;
} }
.dropzone__meta span { .file-list-empty dees-icon {
padding: 4px 10px; font-size: 24px;
border-radius: 999px; opacity: 0.4;
background: ${cssManager.bdTheme('hsl(217 91% 95%)', 'hsl(213 93% 18%)')};
border: 1px solid ${cssManager.bdTheme('hsl(217 91% 90%)', 'hsl(213 93% 24%)')};
} }
.file-list { .file-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid ${cssManager.bdTheme('hsl(217 91% 90%)', 'hsl(213 93% 24%)')};
} }
.file-list__header { .file-list-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
font-size: 13px; padding: 8px 12px;
font-size: 12px;
font-weight: 500; font-weight: 500;
color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 68%)')}; color: var(--dees-color-text-muted);
} }
.file-list__clear { .file-list-clear {
appearance: none; appearance: none;
border: none; border: none;
background: none; background: none;
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')}; color: var(--dees-color-accent-primary);
cursor: pointer; cursor: pointer;
font-weight: 500; font-weight: 500;
font-size: 13px; font-size: 12px;
padding: 0; padding: 0;
font-family: inherit;
} }
.file-list__clear:hover { .file-list-clear:hover {
text-decoration: underline; text-decoration: underline;
} }
.file-list__items {
display: flex;
flex-direction: column;
gap: 12px;
}
.file-row { .file-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
padding: 10px 12px; padding: 6px 12px;
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.5)', 'hsl(215 20% 16% / 0.5)')}; transition: background var(--dees-transition-fast) ease;
border: 1px solid ${cssManager.bdTheme('hsl(213 27% 92%)', 'hsl(217 25% 26%)')};
border-radius: 8px;
transition: background 0.15s ease;
} }
.file-row:hover { .file-row:hover {
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.8)', 'hsl(215 20% 16% / 0.8)')}; background: var(--dees-color-row-hover);
} }
.file-thumb { .file-thumb {
width: 36px; width: 32px;
height: 36px; height: 32px;
border-radius: 8px; border-radius: var(--dees-radius-sm);
background: ${cssManager.bdTheme('hsl(214 31% 92%)', 'hsl(217 32% 18%)')}; background: var(--dees-color-bg-tertiary);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -204,16 +161,15 @@ export const fileuploadStyles = [
} }
.file-thumb dees-icon { .file-thumb dees-icon {
font-size: 18px; font-size: 16px;
color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 70%)')}; color: var(--dees-color-text-muted);
display: block; display: block;
width: 18px; width: 16px;
height: 18px; height: 16px;
line-height: 1; line-height: 1;
flex-shrink: 0; flex-shrink: 0;
} }
.thumb-image { .thumb-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -223,14 +179,14 @@ export const fileuploadStyles = [
.file-meta { .file-meta {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 2px;
min-width: 0; min-width: 0;
} }
.file-name { .file-name {
font-weight: 600; font-weight: 500;
font-size: 14px; font-size: 13px;
color: ${cssManager.bdTheme('hsl(222 47% 11%)', 'hsl(210 20% 96%)')}; color: var(--dees-color-text-primary);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -241,8 +197,8 @@ export const fileuploadStyles = [
align-items: center; align-items: center;
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
font-size: 12px; font-size: 11px;
color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')}; color: var(--dees-color-text-muted);
} }
.file-size { .file-size {
@@ -250,39 +206,40 @@ export const fileuploadStyles = [
} }
.file-type { .file-type {
padding: 2px 8px; padding: 1px 6px;
border-radius: 999px; border-radius: var(--dees-radius-full);
border: 1px solid ${cssManager.bdTheme('hsl(214 31% 86%)', 'hsl(217 32% 28%)')}; border: 1px solid var(--dees-color-border-default);
color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')}; color: var(--dees-color-text-muted);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
line-height: 1; line-height: 1;
font-size: 10px;
} }
.file-actions { .file-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px;
margin-left: auto; margin-left: auto;
} }
.remove-button { .remove-button {
width: 28px; width: 24px;
height: 28px; height: 24px;
border-radius: 6px; border-radius: var(--dees-radius-xs);
background: transparent; background: transparent;
border: none; border: none;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: background 0.15s ease, transform 0.15s ease, color 0.15s ease; transition: background var(--dees-transition-fast) ease,
color: ${cssManager.bdTheme('hsl(215 16% 52%)', 'hsl(215 16% 68%)')}; color var(--dees-transition-fast) ease;
color: var(--dees-color-text-muted);
} }
.remove-button:hover { .remove-button:hover {
background: ${cssManager.bdTheme('hsl(0 72% 50% / 0.08)', 'hsl(0 62% 32% / 0.15)')}; background: ${cssManager.bdTheme('hsl(0 72% 50% / 0.08)', 'hsl(0 62% 32% / 0.15)')};
color: ${cssManager.bdTheme('hsl(0 72% 46%)', 'hsl(0 70% 70%)')}; color: var(--dees-color-accent-error);
} }
.remove-button:active { .remove-button:active {
@@ -298,9 +255,28 @@ export const fileuploadStyles = [
flex-shrink: 0; flex-shrink: 0;
} }
/* ── Footer slot: meta chips ── */
.dropzone-footer {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 6px 12px;
align-items: center;
}
.meta-chip {
font-size: 11px;
padding: 2px 8px;
border-radius: var(--dees-radius-full);
color: var(--dees-color-text-muted);
background: var(--dees-color-bg-tertiary);
border: 1px solid var(--dees-color-border-subtle);
}
/* ── Validation ── */
.validation-message { .validation-message {
font-size: 13px; font-size: 13px;
color: ${cssManager.bdTheme('hsl(0 72% 40%)', 'hsl(0 70% 68%)')}; color: var(--dees-color-accent-error);
line-height: 1.5; line-height: 1.5;
} }

View File

@@ -253,6 +253,7 @@ export const themeDefaultStyles: CSSResult = css`
--dees-color-hover: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.06)')}; --dees-color-hover: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.06)')};
--dees-color-active: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.08)')}; --dees-color-active: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.08)')};
--dees-color-pressed: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')}; --dees-color-pressed: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
--dees-color-row-hover: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.06)', 'hsl(217.2 91.2% 59.8% / 0.08)')};
/* ======================================== /* ========================================
* Colors — Focus Ring * Colors — Focus Ring