Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions submissions/examples/file-upload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# File Upload Component

A drag-and-drop file upload zone with distinct animated states: idle, drag-over, uploading (indeterminate progress bar), success (checkmark pop), and error (shake). Includes file type validation and size checking with inline JavaScript.

## Classes

| Class | Description |
|---|---|
| `ease-file-upload` | Main container (click to browse, drag target) |
| `ease-file-upload--drag-over` | File hovering over the drop zone |
| `ease-file-upload--uploading` | Upload in progress |
| `ease-file-upload--success` | Upload completed successfully |
| `ease-file-upload--error` | Upload failed |
| `ease-upload-icon` | State icon (upload arrow / check / error) |
| `ease-upload-text` | State description text |
| `ease-upload-hint` | Supported file types hint |
| `ease-upload-progress` | Progress bar track |
| `ease-upload-progress-bar` | Animated indeterminate progress fill |

## Usage

```html
<div class="ease-file-upload" role="button" tabindex="0">
<svg class="ease-upload-icon" ...>...</svg>
<span class="ease-upload-text">Drag & drop files here or click to browse</span>
<span class="ease-upload-hint">Supports PNG, JPG, WebP, PDF (max 5MB)</span>
<div class="ease-upload-progress">
<div class="ease-upload-progress-bar"></div>
</div>
</div>
```

A minimal JS setup handles drag events, file validation, and state transitions (see `demo.html`).

## Why it fits EaseMotion CSS

Five animated visual states with smooth transitions, floating icon animation, indeterminate progress bar, pop-in checkmark, and error shake β€” all using `ease-` prefixed keyframes. Respects `prefers-reduced-motion`.
139 changes: 139 additions & 0 deletions submissions/examples/file-upload/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>File Upload β€” EaseMotion CSS</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>

<div class="ease-file-upload" role="button" tabindex="0">
<svg class="ease-upload-icon" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
<span class="ease-upload-text">Drag & drop files here or click to browse</span>
<span class="ease-upload-hint">Supports PNG, JPG, WebP, PDF (max 5MB)</span>
<div class="ease-upload-progress">
<div class="ease-upload-progress-bar"></div>
</div>
</div>

<script>
const uploadZone = document.querySelector('.ease-file-upload');
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/png,image/jpeg,image/webp,application/pdf';
fileInput.multiple = true;
fileInput.tabIndex = -1;
document.body.appendChild(fileInput);

const icon = uploadZone.querySelector('.ease-upload-icon');
const text = uploadZone.querySelector('.ease-upload-text');

const validTypes = ['image/png', 'image/jpeg', 'image/webp', 'application/pdf'];
const maxSize = 5 * 1024 * 1024;
let errorTimeout = null;

function resetState() {
uploadZone.classList.remove(
'ease-file-upload--drag-over',
'ease-file-upload--uploading',
'ease-file-upload--success',
'ease-file-upload--error'
);
text.textContent = 'Drag & drop files here or click to browse';
icon.innerHTML =
'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />' +
'<polyline points="17 8 12 3 7 8" />' +
'<line x1="12" y1="3" x2="12" y2="15" />';
}

function showError(message) {
clearTimeout(errorTimeout);
uploadZone.classList.remove(
'ease-file-upload--drag-over',
'ease-file-upload--uploading',
'ease-file-upload--success'
);
uploadZone.classList.add('ease-file-upload--error');
text.textContent = message;
icon.innerHTML =
'<circle cx="12" cy="12" r="10" opacity="0.2" />' +
'<line x1="15" y1="9" x2="9" y2="15" />' +
'<line x1="9" y1="9" x2="15" y2="15" />';
errorTimeout = setTimeout(resetState, 2500);
}

function handleFiles(files) {
if (!files.length) return;

for (const file of files) {
if (!validTypes.includes(file.type)) {
showError('Invalid file type. Please upload PNG, JPG, WebP, or PDF.');
return;
}
if (file.size > maxSize) {
showError('File too large (max 5MB).');
return;
}
}

uploadZone.classList.remove('ease-file-upload--drag-over');
uploadZone.classList.add('ease-file-upload--uploading');
text.textContent = `Uploading ${files.length} file(s)...`;
icon.innerHTML =
'<circle cx="12" cy="12" r="10" opacity="0.15" />';

setTimeout(() => {
uploadZone.classList.remove('ease-file-upload--uploading');
uploadZone.classList.add('ease-file-upload--success');
text.textContent = `${files.length} file(s) uploaded successfully`;
icon.innerHTML =
'<circle cx="12" cy="12" r="10" opacity="0.12" />' +
'<polyline points="9 12 11 14 15 10" stroke-width="2" />';
}, 2000);
}

uploadZone.addEventListener('click', () => fileInput.click());

uploadZone.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
fileInput.click();
}
});

['dragenter', 'dragover'].forEach(event => {
uploadZone.addEventListener(event, (e) => {
e.preventDefault();
if (!uploadZone.classList.contains('ease-file-upload--uploading') &&
!uploadZone.classList.contains('ease-file-upload--success')) {
uploadZone.classList.add('ease-file-upload--drag-over');
}
});
});

['dragleave', 'drop'].forEach(event => {
uploadZone.addEventListener(event, (e) => {
e.preventDefault();
uploadZone.classList.remove('ease-file-upload--drag-over');
});
});

uploadZone.addEventListener('drop', (e) => {
handleFiles(e.dataTransfer.files);
});

fileInput.addEventListener('change', (e) => {
if (e.target.files.length) {
handleFiles(e.target.files);
}
e.target.value = '';
});
</script>

</body>
</html>
187 changes: 187 additions & 0 deletions submissions/examples/file-upload/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/* ============================================================
EaseMotion CSS β€” Drag-and-Drop File Upload Component
Issue #1201
============================================================ */

/* ── Container ────────────────────────────────────────────── */

.ease-file-upload {
max-width: 480px;
margin: 2rem auto;
border: 2px dashed rgba(255, 255, 255, 0.15);
border-radius: var(--ease-radius-lg, 0.875rem);
padding: 2.5rem 2rem;
text-align: center;
cursor: pointer;
background: rgba(255, 255, 255, 0.02);
transition:
border-color var(--ease-speed-normal, 250ms) cubic-bezier(0.4, 0, 0.2, 1),
background var(--ease-speed-normal, 250ms) cubic-bezier(0.4, 0, 0.2, 1),
transform var(--ease-speed-fast, 150ms) cubic-bezier(0.4, 0, 0.2, 1),
box-shadow var(--ease-speed-normal, 250ms) cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
}

.ease-file-upload:hover {
border-color: rgba(124, 108, 255, 0.4);
background: rgba(124, 108, 255, 0.04);
}

.ease-file-upload:focus-visible {
box-shadow: 0 0 0 3px rgba(124, 108, 255, 0.3);
}

/* ── Icon ─────────────────────────────────────────────────── */

.ease-upload-icon {
display: block;
margin: 0 auto 1rem;
color: rgba(255, 255, 255, 0.3);
transition: color var(--ease-speed-normal, 250ms) cubic-bezier(0.4, 0, 0.2, 1);
}

/* ── Text ─────────────────────────────────────────────────── */

.ease-upload-text {
display: block;
font-size: var(--ease-text-sm, 0.9375rem);
color: rgba(255, 255, 255, 0.7);
margin-bottom: 0.375rem;
font-weight: 500;
}

.ease-upload-hint {
display: block;
font-size: var(--ease-text-xs, 0.75rem);
color: rgba(255, 255, 255, 0.35);
}

/* ── State: drag-over ─────────────────────────────────────── */

.ease-file-upload--drag-over {
border-color: #7c6cff;
border-style: solid;
background: rgba(124, 108, 255, 0.08);
transform: scale(1.02);
box-shadow: 0 0 0 4px rgba(124, 108, 255, 0.12);
}

.ease-file-upload--drag-over .ease-upload-icon {
color: #7c6cff;
animation: ease-kf-upload-float 1s ease-in-out infinite;
}

/* ── State: uploading ─────────────────────────────────────── */

.ease-file-upload--uploading {
pointer-events: none;
opacity: 0.8;
border-color: #7c6cff;
border-style: solid;
}

.ease-file-upload--uploading .ease-upload-icon {
color: #7c6cff;
animation: ease-kf-upload-pulse 1.2s ease-in-out infinite;
}

/* ── State: success ───────────────────────────────────────── */

.ease-file-upload--success {
border-color: #22c55e;
border-style: solid;
background: rgba(34, 197, 94, 0.05);
}

.ease-file-upload--success .ease-upload-icon {
color: #22c55e;
animation: ease-kf-upload-pop-check 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* ── State: error ─────────────────────────────────────────── */

.ease-file-upload--error {
border-color: #ef4444;
border-style: solid;
background: rgba(239, 68, 68, 0.05);
animation: ease-kf-upload-shake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97);
}

.ease-file-upload--error .ease-upload-icon {
color: #ef4444;
}

/* ── Progress bar ─────────────────────────────────────────── */

.ease-upload-progress {
display: none;
height: 5px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.08);
margin-top: 1.25rem;
overflow: hidden;
}

.ease-file-upload--uploading .ease-upload-progress {
display: block;
}

.ease-upload-progress-bar {
height: 100%;
width: 0%;
border-radius: 3px;
background: linear-gradient(90deg, #7c6cff, #a78bfa);
animation: ease-kf-upload-progress-indeterminate 1.5s ease-in-out infinite;
}

/* ── Keyframes ────────────────────────────────────────────── */

@keyframes ease-kf-upload-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}

@keyframes ease-kf-upload-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}

@keyframes ease-kf-upload-pop-check {
0% { transform: scale(0); }
50% { transform: scale(1.3); }
100% { transform: scale(1); }
}

@keyframes ease-kf-upload-shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-8px); }
40% { transform: translateX(8px); }
60% { transform: translateX(-6px); }
80% { transform: translateX(6px); }
}

@keyframes ease-kf-upload-progress-indeterminate {
0% { width: 0%; margin-left: 0; }
50% { width: 50%; margin-left: 25%; }
100% { width: 0%; margin-left: 100%; }
}

/* ── Reduced motion ──────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
.ease-file-upload--drag-over {
transform: none;
}
.ease-file-upload--drag-over .ease-upload-icon,
.ease-file-upload--uploading .ease-upload-icon,
.ease-file-upload--success .ease-upload-icon {
animation: none;
}
.ease-file-upload--error {
animation: none;
}
.ease-upload-progress-bar {
animation: none;
width: 50%;
}
}
Loading