Files
WarpBox/warpbox_uploads_win98_dark_v16.html
Daniel Legt e330fb04b3 feat(ui): add clear queue flow and expose ISO expiry
- Add `formatBrowserTime()` and include ISO-8601 `expires_at` in box status JSON and `ExpiresAtISO` in the box view for browser-friendly rendering.
- Refresh UI styling (switch to MonoCraft/PixelOperatorMono, tweak base font size) and treat `aria-disabled="true"` like `disabled` for consistent button states.
- Introduce a clear-queue action with confirmation to reset upload state, unlock controls, and provide user feedback.feat(ui): add clear queue flow and expose ISO expiry

- Add `formatBrowserTime()` and include ISO-8601 `expires_at` in box status JSON and `ExpiresAtISO` in the box view for browser-friendly rendering.
- Refresh UI styling (switch to MonoCraft/PixelOperatorMono, tweak base font size) and treat `aria-disabled="true"` like `disabled` for consistent button states.
- Introduce a clear-queue action with confirmation to reset upload state, unlock controls, and provide user feedback.
2026-04-29 02:29:49 +03:00

2450 lines
117 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WarpBox - Upload Demo</title>
<style>
@font-face { font-family: 'PixelOperator'; src: url('/static/fonts/pixel_operator/PixelOperator.ttf'); }
@font-face { font-family: 'PixelOperator'; src: url('/static/fonts/pixel_operator/PixelOperator-Bold.ttf'); font-weight: bold; }
@font-face { font-family: 'PixeloidSans'; src: url('/static/fonts/pixeloid_sans/PixeloidSans.ttf'); }
@font-face { font-family: 'PixeloidSans'; src: url('/static/fonts/pixeloid_sans/PixeloidSans-Bold.ttf'); font-weight: bold; }
@font-face { font-family: 'MonoCraft'; src: url('/static/fonts/monocraft/Monocraft.ttf'); }
:root {
font-family: 'PixelOperator', 'MS Sans Serif', Arial, sans-serif;
font-smooth: never;
image-rendering: pixelated;
--base-font-size: 14px;
--w98-blue: #000078;
--w98-blue-gradient: linear-gradient(to right, #000078, 80%, #0f80cd);
--w98-gray: #c0c0c0;
--w98-gray2: #a6a6a6;
--ok: #008000;
--danger: #800000;
--sunken: #dfdfdf;
}
* { box-sizing: border-box; }
html { font-size: var(--base-font-size); color: #fff; background: #000; }
html, body { margin: 0; padding: 0; }
html { height: 100%; }
body {
height: 100%;
min-height: 100vh;
overflow: hidden;
background: #000;
}
button, label[for], .menu-button, .win98-button:not(:disabled) { cursor: pointer; }
input, textarea, select { font-family: inherit; }
main {
height: 100vh;
min-height: 0;
display: grid;
place-items: center;
padding: 18px;
position: relative;
z-index: 1;
overflow: hidden;
}
.desktop-wrap {
--window-height: 736px;
--side-width: 440px;
width: min(1278px, 100%);
height: min(var(--window-height), calc(100vh - 36px));
max-height: calc(100vh - 36px);
display: grid;
grid-template-columns: minmax(0, 820px) var(--side-width);
grid-template-rows: minmax(0, 1fr);
align-items: stretch;
justify-content: center;
gap: 18px;
overflow: hidden;
}
.win98-window {
display: flex;
flex-direction: column;
color: #000;
background: var(--w98-gray);
border-top: 1px solid #fff;
border-left: 1px solid #fff;
border-right: 1px solid #000;
border-bottom: 1px solid #000;
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf, 4px 5px 0 rgba(0,0,0,.45);
}
.win98-titlebar {
display: flex;
align-items: center;
justify-content: space-between;
height: 22px;
margin: 2px;
padding: 2px 3px 2px 6px;
color: #fff;
background: var(--w98-blue-gradient);
user-select: none;
}
.win98-titlebar h1, .win98-titlebar h2 {
min-width: 0;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
line-height: 14px;
font-weight: bold;
}
.win98-titlebar-label { display: flex; align-items: center; min-width: 0; gap: 5px; }
.win98-titlebar-icon {
flex: 0 0 auto;
width: 16px;
height: 16px;
display: grid;
place-items: center;
color: #fff;
background: #000078;
border: 1px solid #fff;
box-shadow: inset -5px 0 0 #0f80cd, inset 0 -5px 0 #4c1ca0;
font-size: 11px;
line-height: 11px;
}
.win98-window-controls { display: flex; flex: 0 0 auto; gap: 2px; }
.win98-control {
width: 16px;
height: 14px;
display: grid;
place-items: center;
color: #000;
background: var(--w98-gray);
border-top: 1px solid #fff;
border-left: 1px solid #fff;
border-right: 1px solid #000;
border-bottom: 1px solid #000;
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf;
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
line-height: 12px;
padding: 0;
}
.win98-button {
min-width: 92px;
height: 28px;
display: grid;
place-items: center;
margin: 0;
padding: 0 10px;
color: #000;
background: var(--w98-gray);
border-top: 2px solid #fff;
border-left: 2px solid #fff;
border-right: 2px solid #000;
border-bottom: 2px solid #000;
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf;
font-family: inherit;
font-size: 13px;
line-height: 13px;
text-align: center;
appearance: none;
}
.win98-button:disabled { color: #808080; text-shadow: 1px 1px 0 #fff; cursor: default; }
.win98-button:active:not(:disabled), .win98-control:active, .menu-button[aria-expanded="true"] {
border-top-color: #000;
border-left-color: #000;
border-right-color: #fff;
border-bottom-color: #fff;
box-shadow: inset -1px -1px 0 #dfdfdf, inset 1px 1px 0 #808080;
padding-top: 1px;
}
.win98-panel {
background: #fff;
border-top: 1px solid #808080;
border-left: 1px solid #808080;
border-right: 1px solid #fff;
border-bottom: 1px solid #fff;
}
.win98-statusbar {
display: grid;
gap: 4px;
height: 22px;
padding: 0 4px 4px;
font-size: 12px;
line-height: 12px;
}
.win98-statusbar span {
display: flex;
align-items: center;
min-width: 0;
padding: 0 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-top: 1px solid #808080;
border-left: 1px solid #808080;
border-right: 1px solid #fff;
border-bottom: 1px solid #fff;
}
.menu-bar {
position: relative;
display: flex;
align-items: center;
gap: 2px;
height: 24px;
padding: 1px 6px;
font-size: 13px;
line-height: 13px;
z-index: 5;
}
.menu-item { position: relative; }
.menu-button {
height: 20px;
min-width: 54px;
padding: 0 8px;
color: #000;
background: transparent;
border: 1px solid transparent;
font-family: inherit;
font-size: 13px;
text-align: left;
}
.menu-button:hover, .menu-button:focus-visible {
border-top: 1px solid #fff;
border-left: 1px solid #fff;
border-right: 1px solid #808080;
border-bottom: 1px solid #808080;
outline: none;
}
.menu-popup {
position: absolute;
top: 22px;
left: 0;
min-width: 188px;
padding: 2px;
background: var(--w98-gray);
border-top: 2px solid #fff;
border-left: 2px solid #fff;
border-right: 2px solid #000;
border-bottom: 2px solid #000;
box-shadow: 3px 3px 0 rgba(0,0,0,.35);
display: none;
z-index: 20;
}
.menu-item.is-open .menu-popup { display: block; }
.menu-action {
width: 100%;
min-height: 22px;
display: grid;
grid-template-columns: 20px minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
padding: 2px 6px;
color: #000;
background: transparent;
border: 0;
font-family: inherit;
font-size: 12px;
text-align: left;
}
.menu-action:hover, .menu-action:focus-visible { color: #fff; background: #000078; outline: none; }
.menu-separator { height: 1px; margin: 3px 2px; background: #808080; border-bottom: 1px solid #fff; }
.shortcut { color: #555; }
.menu-action:hover .shortcut { color: #fff; }
.upload-window { width: 100%; height: 100%; min-height: 0; overflow: hidden; }
.upload-form { display: flex; flex: 1; flex-direction: column; min-height: 0; }
.upload-panel { display: flex; flex: 1; flex-direction: column; min-height: 0; margin: 0 8px 8px; padding: 12px; }
.upload-header {
display: grid;
grid-template-columns: minmax(0, 1fr) 270px;
gap: 10px;
margin-bottom: 10px;
color: #000;
}
.upload-heading { margin: 0 0 4px; font-size: 20px; line-height: 22px; font-weight: bold; }
.upload-subtext { margin: 0; color: #333; font-size: 13px; line-height: 15px; }
.upload-quota {
min-width: 0;
padding: 7px;
background: #dfdfdf;
border-top: 1px solid #fff;
border-left: 1px solid #fff;
border-right: 1px solid #808080;
border-bottom: 1px solid #808080;
font-size: 12px;
line-height: 13px;
overflow: hidden;
}
.upload-quota strong { display: block; margin-bottom: 4px; }
.upload-quota-track, .upload-overall-track, .upload-progress {
display: block;
box-sizing: border-box;
min-width: 0;
overflow: hidden;
background: #fff;
border-top: 1px solid #808080;
border-left: 1px solid #808080;
border-right: 1px solid #dfdfdf;
border-bottom: 1px solid #dfdfdf;
}
.upload-quota-track { width: 100%; max-width: 100%; height: 12px; margin-top: 6px; }
.upload-quota-bar, .upload-overall-bar, .upload-progress-bar { display: block; width: 0%; max-width: 100%; height: 100%; background: #000078; }
.upload-quota-bar { width: 0%; }
.upload-dropzone {
flex: 0 0 auto;
min-height: 154px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 18px;
text-align: center;
background: #dfdfdf;
border: 1px dotted #000;
color: #000;
}
.upload-dropzone.is-dragging, .upload-dropzone:hover { background: #c7d8f2; outline: 2px solid #000078; outline-offset: -4px; }
.upload-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
.upload-icon {
width: 34px;
height: 30px;
position: relative;
background: #fff;
border: 2px solid #000;
box-shadow: inset -3px -3px 0 #dfdfdf;
}
.upload-icon:before {
content: "";
position: absolute;
right: -2px;
top: -2px;
width: 10px;
height: 10px;
background: #dfdfdf;
border-left: 2px solid #000;
border-bottom: 2px solid #000;
}
.upload-primary { font-size: 18px; line-height: 18px; font-weight: bold; }
.upload-secondary { color: #333; font-size: 13px; line-height: 15px; }
.upload-linklike { color: #000078; text-decoration: underline; font-weight: bold; }
.upload-details {
display: flex;
align-items: center;
min-height: 28px;
margin-top: 12px;
padding: 5px 8px;
background: #fff;
border-top: 1px solid #808080;
border-left: 1px solid #808080;
border-right: 1px solid #dfdfdf;
border-bottom: 1px solid #dfdfdf;
font-size: 13px;
line-height: 13px;
}
.upload-detail-label { flex: 0 0 auto; margin-right: 6px; font-weight: bold; }
.upload-file-count { margin-left: auto; }
.upload-file-list {
flex: 1 1 auto;
min-height: 0;
margin-top: 8px;
overflow-y: auto;
background: #fff;
border-top: 2px solid #808080;
border-left: 2px solid #808080;
border-right: 2px solid #fff;
border-bottom: 2px solid #fff;
}
.upload-empty-state { margin: 0; padding: 10px 8px; color: #555; font-size: 13px; line-height: 15px; }
.upload-file-row {
display: grid;
grid-template-columns: 22px minmax(0, 1fr) 82px 30px;
grid-template-rows: 20px 8px;
align-items: center;
height: 38px;
padding: 4px 8px;
border-bottom: 1px solid #dfdfdf;
font-size: 13px;
line-height: 13px;
column-gap: 6px;
}
.upload-file-row:nth-child(even) { background: #f7f7f7; }
.upload-file-row.is-working { animation: upload-row-loading 900ms steps(2, end) infinite; }
@keyframes upload-row-loading { 0% { background-color: #fff; } 100% { background-color: #e6e6e6; } }
.upload-file-icon {
grid-row: 1 / 3;
width: 18px;
height: 18px;
display: grid;
place-items: center;
background: #fff;
border: 1px solid #808080;
color: #000078;
font-size: 8px;
line-height: 8px;
font-weight: bold;
overflow: hidden;
}
.upload-file-name, .upload-file-size { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.upload-file-size { text-align: right; color: #333; }
.upload-file-remove { grid-column: 4; grid-row: 1 / 3; justify-self: end; width: 22px; min-width: 22px; height: 22px; padding: 0; font-size: 12px; }
.upload-progress { grid-column: 2 / 4; grid-row: 2; height: 8px; width: 100%; }
.upload-file-row.is-uploaded .upload-progress-bar { background: #008000; }
.upload-file-row.is-failed .upload-progress-bar { background: #800000; width: 100%; }
.upload-result {
display: grid;
grid-template-columns: 72px minmax(0, 1fr) 72px;
align-items: center;
gap: 6px;
min-height: 36px;
margin-top: 8px;
padding: 4px 6px;
background: #dfdfdf;
border-top: 1px solid #808080;
border-left: 1px solid #808080;
border-right: 1px solid #fff;
border-bottom: 1px solid #fff;
font-size: 12px;
line-height: 12px;
}
.upload-result-label { font-weight: bold; }
.upload-result-link { min-width: 0; overflow: hidden; color: #000078; text-overflow: ellipsis; white-space: nowrap; }
.upload-result-link.is-empty { color: #555; text-decoration: none; pointer-events: none; }
.upload-share-button { min-width: 72px; width: 72px; height: 24px; font-size: 12px; line-height: 12px; }
.upload-overall {
display: grid;
grid-template-columns: minmax(0, 1fr) 42px;
align-items: center;
gap: 6px;
height: 28px;
padding: 0 8px 8px;
font-size: 12px;
line-height: 12px;
}
.upload-overall-track { height: 18px; border-width: 2px; border-color: #808080 #fff #fff #808080; }
.upload-overall-bar { background-image: repeating-linear-gradient(to right, #000078 0, #000078 10px, #c0c0c0 10px, #c0c0c0 12px); }
.upload-overall-percent { min-width: 0; text-align: right; }
.upload-actions { display: flex; justify-content: flex-end; gap: 8px; height: 40px; padding: 0 8px 8px; }
.upload-statusbar { grid-template-columns: 1fr 100px; }
.side-stack {
width: var(--side-width);
min-width: var(--side-width);
max-width: var(--side-width);
height: 100%;
min-height: 0;
display: grid;
grid-template-columns: var(--side-width);
grid-template-rows: 350px 210px 1fr;
align-content: stretch;
gap: 12px;
padding-top: 0;
overflow: hidden;
}
.side-panel {
width: var(--side-width);
min-width: var(--side-width);
max-width: var(--side-width);
min-height: 0;
resize: none;
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf, 3px 4px 0 rgba(0,0,0,.38);
}
.side-panel:first-child, .side-panel:nth-child(2) { height: 100%; }
.side-body { margin: 0 6px 6px; padding: 9px; font-size: 13px; line-height: 15px; color: #000; }
.box-options-form { display: grid; gap: 8px; }
.option-row { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: 6px; align-items: center; }
.option-check { display: flex; gap: 6px; align-items: center; }
.option-check input { width: 13px; height: 13px; margin: 0; }
.upload-select, .upload-text-input {
width: 100%;
height: 22px;
padding: 1px 4px;
color: #000;
background: #fff;
border-top: 1px solid #808080;
border-left: 1px solid #808080;
border-right: 1px solid #fff;
border-bottom: 1px solid #fff;
font-size: 12px;
line-height: 12px;
}
.terminal-box {
min-height: 132px;
padding: 10px;
color: #00ff66;
background: #000;
border: 0;
font-family: 'PixelOperator', 'Courier New', monospace;
font-size: 13px;
line-height: 16px;
white-space: pre-wrap;
overflow: auto;
}
.terminal-muted { color: #80b88f; }
.terminal-actions { display: flex; justify-content: flex-end; margin-top: 8px; }
.terminal-copy-button { min-width: 148px; height: 24px; font-size: 12px; line-height: 12px; }
.helper-window { width: var(--side-width); min-width: var(--side-width); max-width: var(--side-width); min-height: 0; overflow: hidden; }
.helper-body { margin: 0 6px 6px; height: calc(100% - 34px); min-height: 0; padding: 10px; display: flex; align-items: center; justify-content: space-around; gap: 12px; overflow: auto; }
.folder-icon-button { width: 86px; min-width: 86px; height: 68px; display: grid; grid-template-rows: 34px 1fr; place-items: center; gap: 4px; padding: 4px; color: #000; background: transparent; border: 1px solid transparent; font-family: inherit; font-size: 12px; line-height: 12px; }
.folder-icon-button:hover, .folder-icon-button:focus-visible { color: #fff; background: #000078; border: 1px dotted #fff; outline: none; }
.folder-icon { width: 34px; height: 26px; position: relative; display: block; background: #f7d66a; border-top: 1px solid #fff5bd; border-left: 1px solid #fff5bd; border-right: 1px solid #7a5b00; border-bottom: 1px solid #7a5b00; box-shadow: inset -2px -2px 0 #d0a728; }
.folder-icon:before { content: ""; position: absolute; left: 3px; top: -6px; width: 15px; height: 7px; background: #f7d66a; border-top: 1px solid #fff5bd; border-left: 1px solid #fff5bd; border-right: 1px solid #7a5b00; }
.upload-quota {
background: #c7d8f2;
border-top-color: #ffffff;
border-left-color: #ffffff;
border-right-color: #404040;
border-bottom-color: #404040;
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #e9f2ff;
}
.upload-quota strong { font-size: 13px; }
.upload-quota-track { height: 16px; border-width: 2px; border-color: #808080 #ffffff #ffffff #808080; }
.upload-quota-bar {
background-color: #000078;
background-image: repeating-linear-gradient(to right, #000078 0, #000078 8px, #0f80cd 8px, #0f80cd 11px);
}
.side-panel { display: flex; flex-direction: column; }
.side-body { flex: 1 1 auto; overflow: auto; background: #ffffff; }
.box-options-form { min-height: 100%; align-content: start; }
.option-check { position: relative; min-height: 18px; }
.option-check input[type="checkbox"] {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
margin: 0;
}
.option-check span { position: relative; padding-left: 20px; }
.option-check span:before {
content: "";
position: absolute;
left: 0;
top: 1px;
width: 13px;
height: 13px;
background: #ffffff;
border-top: 1px solid #808080;
border-left: 1px solid #808080;
border-right: 1px solid #ffffff;
border-bottom: 1px solid #ffffff;
box-shadow: inset 1px 1px 0 #000000;
}
.option-check input[type="checkbox"]:checked + span:after {
content: "✓";
position: absolute;
left: 2px;
top: -2px;
color: #000000;
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
line-height: 16px;
font-weight: bold;
}
.option-check input[type="checkbox"]:focus-visible + span { outline: 1px dotted #000000; outline-offset: 2px; }
.start-upload-cta {
min-width: 128px;
position: relative;
overflow: hidden;
isolation: isolate;
font-weight: bold;
background: transparent;
animation: start-breathe 1.55s steps(12, end) infinite, start-tilt 2.1s steps(6, end) infinite;
}
.start-upload-cta:before {
content: "";
position: absolute;
inset: -3px;
z-index: -1;
background: conic-gradient(#ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000);
animation: start-rainbow 1.1s linear infinite;
}
.start-upload-cta:after {
content: "";
position: absolute;
inset: 3px;
z-index: -1;
background: var(--w98-gray);
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf;
}
.start-upload-cta:hover:not(:disabled) {
transform: scale(1.06) rotate(-1deg);
filter: brightness(1.08);
}
.start-upload-cta:active:not(:disabled) { transform: scale(.99); }
@keyframes start-rainbow { to { transform: rotate(1turn); } }
@keyframes start-breathe { 0%, 100% { scale: 1; } 50% { scale: 1.035; } }
@keyframes start-tilt { 0%, 100% { rotate: 0deg; } 25% { rotate: -1deg; } 75% { rotate: 1deg; } }
.modal-backdrop {
position: fixed;
inset: 0;
display: none;
background: rgba(128, 128, 128, .42);
z-index: 70;
}
.modal-backdrop.is-visible { display: block; }
.popup-window { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); width: min(780px, calc(100vw - 24px)); max-height: min(760px, calc(100vh - 24px)); display: none; z-index: 80; }
.popup-window.is-visible { display: flex; }
.popup-body { margin: 0 6px 6px; padding: 12px; max-height: calc(100vh - 90px); overflow: auto; font-size: 13px; line-height: 16px; color: #000; }
.popup-body h3 { margin: 0 0 8px; font-size: 16px; line-height: 18px; }
.popup-body h4 { margin: 14px 0 6px; font-size: 14px; line-height: 16px; }
.popup-body p { margin: 0 0 8px; }
.popup-body ul, .popup-body ol { margin: 0 0 8px 18px; padding: 0; }
.popup-body li { margin: 0 0 4px; }
.popup-body pre { margin: 6px 0 10px; padding: 8px; overflow: auto; color: #00ff66; background: #000; border: 0; font-family: 'PixelOperator', 'Courier New', monospace; font-size: 12px; line-height: 15px; white-space: pre-wrap; }
.popup-close { cursor: pointer; }
.toast {
position: fixed;
right: 12px;
bottom: 52px;
max-width: min(360px, calc(100vw - 24px));
display: none;
padding: 8px 10px;
color: #000;
background: #ffffcc;
border-top: 2px solid #fff;
border-left: 2px solid #fff;
border-right: 2px solid #000;
border-bottom: 2px solid #000;
z-index: 60;
font-size: 12px;
line-height: 14px;
box-shadow: 4px 4px 0 rgba(0,0,0,.45);
}
.toast.is-visible { display: block; }
@media (max-width: 1320px) {
body { height: auto; min-height: 100vh; overflow-y: auto; }
main { height: auto; min-height: 100vh; place-items: start center; overflow: visible; }
.desktop-wrap {
--window-height: 680px;
grid-template-columns: minmax(0, 820px);
grid-template-rows: var(--window-height) auto;
width: min(820px, 100%);
max-width: 820px;
height: auto;
max-height: none;
overflow: visible;
}
.side-stack {
display: grid;
width: 100%;
min-width: 0;
max-width: none;
height: auto;
grid-template-columns: 1fr;
grid-template-rows: 350px 210px 132px;
overflow: visible;
}
.side-panel,
.helper-window {
width: 100%;
min-width: 0;
max-width: none;
}
}
@media (min-width: 1440px) {
.desktop-wrap { --window-height: 780px; }
.side-stack { grid-template-rows: 372px 230px 1fr; }
}
@media (max-width: 760px) {
body { height: auto; min-height: 100vh; overflow-y: auto; }
main { height: auto; min-height: 100dvh; place-items: stretch; align-items: stretch; padding: 0; overflow: visible; }
.desktop-wrap { --window-height: auto; width: 100%; max-width: none; height: auto; max-height: none; min-height: 100dvh; gap: 10px; grid-template-columns: 1fr; grid-template-rows: auto auto; overflow: visible; }
.upload-window { min-height: 100dvh; height: auto; width: 100vw; border-left: 0; border-right: 0; box-shadow: none; }
.side-stack { display: grid; width: 100%; min-width: 0; max-width: none; height: auto; grid-template-columns: 1fr; grid-template-rows: auto auto auto; padding: 0 6px 12px; overflow: visible; }
.side-panel, .helper-window { width: 100%; min-width: 0; max-width: none; min-height: 0; }
.side-panel:first-child { min-height: 360px; }
.side-panel:nth-child(2) { min-height: 210px; }
.helper-window { min-height: 128px; }
.upload-header { grid-template-columns: 1fr; }
.upload-panel { margin: 0 6px 8px; padding: 10px; }
.upload-dropzone { min-height: 118px; padding: 14px 10px; }
.upload-primary { font-size: 16px; }
.upload-details { flex-wrap: wrap; gap: 4px; }
.upload-file-count { margin-left: 0; width: 100%; }
.upload-file-row { grid-template-columns: 22px minmax(0, 1fr) 58px 28px; padding: 4px 5px; font-size: 12px; }
.upload-result { grid-template-columns: 1fr 72px; }
.upload-result-label { grid-column: 1 / 3; }
.upload-actions { justify-content: stretch; }
.upload-actions .win98-button { flex: 1; min-width: 0; }
.menu-bar { overflow-x: auto; }
.menu-popup { position: fixed; left: 6px; right: 6px; top: 50px; min-width: 0; }
.helper-body { justify-content: center; flex-wrap: wrap; height: auto; min-height: 88px; }
.popup-window {
left: 0;
top: 0;
transform: none;
width: 100vw;
height: 100dvh;
max-height: none;
border: 0;
box-shadow: none;
}
.popup-window .win98-titlebar { height: 32px; }
.popup-window .win98-titlebar h2 { font-size: 15px; }
.popup-close { width: 28px; height: 24px; font-size: 18px; font-weight: bold; }
.popup-body { max-height: calc(100dvh - 40px); margin: 0 6px 6px; }
.popup-window.is-visible { animation: popup-open-mobile-v10 160ms steps(5, end); }
@keyframes popup-open-mobile-v10 { from { transform: translateY(10px); opacity: .35; } to { transform: translateY(0); opacity: 1; } }
}
@media (max-width: 420px) {
:root { --base-font-size: 13px; }
.upload-window { min-width: 0; }
.win98-titlebar h1 { font-size: 13px; }
.upload-file-size { display: none; }
.upload-file-row { grid-template-columns: 22px minmax(0, 1fr) 28px; }
.upload-file-remove { grid-column: 3; }
.upload-progress { grid-column: 2 / 3; }
}
/* v9 polish and accessibility pass */
body { font-family: 'PixelOperator', 'MS Sans Serif', Arial, sans-serif; }
.terminal-box, .terminal-box pre, .terminal-box code { font-family: 'MonoCraft', 'PixelOperatorMono', 'Courier New', monospace; }
:focus-visible { outline: 2px dotted #000078; outline-offset: 2px; }
.terminal-box:focus-visible, .upload-dropzone:focus-visible { outline: 2px dotted #fff; outline-offset: 2px; }
.win98-minimize { align-items: start; padding-bottom: 2px; line-height: 9px; }
.upload-dropzone.is-current-step { animation: dropzone-attention 1500ms steps(4, end) infinite; }
@keyframes dropzone-attention { 0%, 100% { box-shadow: inset 0 0 0 0 #000078; transform: translateY(0); } 50% { box-shadow: inset 0 0 0 3px #000078; transform: translateY(-1px); } }
.start-upload-cta { animation: none; position: relative; }
.start-upload-cta.is-current-step { border-color: #fff #000 #000 #fff; box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf, 0 0 0 2px #000078; animation: start-ready-breathe 900ms steps(5, end) infinite; }
.start-upload-cta.is-current-step::after { content: ""; position: absolute; inset: -4px; pointer-events: none; background: repeating-linear-gradient(90deg, #ff004c 0 8px, #ffcc00 8px 16px, #00d26a 16px 24px, #00a2ff 24px 32px, #8c48ff 32px 40px); z-index: -1; animation: border-march 650ms steps(8, end) infinite; }
.start-upload-cta.is-current-step:hover { transform: translateY(-1px); }
@keyframes start-ready-breathe { 0%, 100% { transform: rotate(-.4deg) scale(1); } 50% { transform: rotate(.4deg) scale(1.025); } }
@keyframes border-march { to { background-position: 40px 0; } }
.upload-result.is-current-step { animation: share-ready-pulse 1100ms steps(4, end) infinite; }
@keyframes share-ready-pulse { 50% { filter: brightness(1.08); box-shadow: 0 0 0 2px #000078; } }
.upload-progress-bar.just-completed, .upload-overall-bar.just-completed { animation: progress-impact 560ms steps(5, end); }
@keyframes progress-impact { 0% { transform: scaleX(.96); filter: brightness(1); } 35% { transform: scaleX(1); filter: brightness(2); box-shadow: 0 0 0 2px #fff, 0 0 0 4px #008000; } 100% { filter: brightness(1); } }
.toast.is-visible { animation: toast-in 180ms steps(3, end), toast-buzz 700ms steps(2, end) 180ms; }
@keyframes toast-in { from { transform: translateY(12px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@keyframes toast-buzz { 0%, 100% { margin-right: 0; } 25% { margin-right: 2px; } 50% { margin-right: -2px; } }
.popup-window.is-visible { animation: popup-open 180ms steps(5, end); }
@keyframes popup-open { from { transform: translate(-50%, -48%) scale(.97); opacity: .45; } to { transform: translate(-50%, -50%) scale(1); opacity: 1; } }
.terminal-box { position: relative; white-space: pre-wrap; }
.terminal-box::after { content: "█"; display: inline-block; margin-left: 2px; color: #7dff8a; animation: terminal-cursor 1s steps(2, end) infinite; }
@keyframes terminal-cursor { 50% { opacity: 0; } }
.option-check input[type="checkbox"] { position: absolute; opacity: 0; pointer-events: none; }
.option-check span { position: relative; padding-left: 22px; min-height: 16px; display: inline-flex; align-items: center; }
.option-check span::before { content: ""; position: absolute; left: 0; top: 0; width: 14px; height: 14px; background: #fff; border-top: 2px solid #808080; border-left: 2px solid #808080; border-right: 2px solid #fff; border-bottom: 2px solid #fff; box-shadow: inset -1px -1px 0 #dfdfdf; }
.option-check input[type="checkbox"]:checked + span::after { content: "✓"; position: absolute; left: 2px; top: -3px; font-weight: bold; font-size: 18px; color: #000; }
.option-check input[type="checkbox"]:focus-visible + span { outline: 1px dotted #000; outline-offset: 3px; }
.api-key-row { display: none; }
.api-key-row.is-visible { display: grid; }
.kbd { display: inline-block; min-width: 20px; padding: 1px 5px; margin: 0 2px; background: #dfdfdf; color: #000; border-top: 1px solid #fff; border-left: 1px solid #fff; border-right: 1px solid #000; border-bottom: 1px solid #000; box-shadow: inset -1px -1px 0 #808080; font-family: 'MonoCraft', monospace; font-size: 12px; line-height: 15px; text-align: center; }
.faq-list { display: grid; gap: 10px; }
.faq-item { background: #fff; border-top: 1px solid #808080; border-left: 1px solid #808080; border-right: 1px solid #fff; border-bottom: 1px solid #fff; padding: 8px; }
.faq-q, .faq-a { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 8px; align-items: start; margin: 0; }
.faq-q { font-weight: bold; margin-bottom: 6px; }
.faq-a { color: #222; }
.faq-icon { width: 16px; height: 16px; display: grid; place-items: center; background: #c0c0c0; color: #000; border: 1px solid #000; font-weight: bold; }
.copy-fallback-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.copy-fallback-text { width: 100%; min-height: 58px; font-family: 'MonoCraft', monospace; }
.start-upload-cta { overflow: visible; isolation: auto; background: var(--w98-gray); }
.start-upload-cta::before, .start-upload-cta::after { content: none; }
.start-upload-cta.is-current-step::after { content: ""; }
/* v10 texture, depth, and old-web polish */
* { scrollbar-width: auto; scrollbar-color: #c0c0c0 #808080; }
::-webkit-scrollbar { width: 16px; height: 16px; background: #c0c0c0; }
::-webkit-scrollbar-track {
background: repeating-linear-gradient(45deg, #c0c0c0 0 2px, #b5b5b5 2px 4px);
border-top: 1px solid #808080; border-left: 1px solid #808080; border-right: 1px solid #fff; border-bottom: 1px solid #fff;
}
::-webkit-scrollbar-thumb {
background: #c0c0c0;
border-top: 2px solid #fff; border-left: 2px solid #fff; border-right: 2px solid #000; border-bottom: 2px solid #000;
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf;
}
::-webkit-scrollbar-button:single-button {
width: 16px; height: 16px; background: #c0c0c0;
border-top: 2px solid #fff; border-left: 2px solid #fff; border-right: 2px solid #000; border-bottom: 2px solid #000;
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf;
}
::-webkit-scrollbar-corner { background: #c0c0c0; }
body {
background-color: #000;
background-image: radial-gradient(circle at 18px 18px, rgba(255,255,255,.08) 1px, transparent 1px), radial-gradient(circle at 78px 42px, rgba(15,128,205,.08) 1px, transparent 1px);
background-size: 96px 96px, 132px 132px;
}
.win98-window {
background-color: #c0c0c0;
background-image: linear-gradient(180deg, rgba(255,255,255,.34), rgba(0,0,0,.06)), repeating-linear-gradient(45deg, rgba(255,255,255,.12) 0 1px, transparent 1px 5px);
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf, 5px 6px 0 rgba(0,0,0,.5);
}
.win98-titlebar {
background-image: linear-gradient(to right, #000078, #0f80cd 82%, #85c7ff), repeating-linear-gradient(90deg, rgba(255,255,255,.16) 0 1px, transparent 1px 4px);
box-shadow: inset 0 1px 0 rgba(255,255,255,.35), inset 0 -1px 0 rgba(0,0,0,.35);
}
.upload-panel, .side-body, .helper-body, .popup-body {
background-color: #fff;
background-image: linear-gradient(180deg, rgba(255,255,255,.9), rgba(238,238,238,.58)), repeating-linear-gradient(0deg, rgba(0,0,0,.025) 0 1px, transparent 1px 6px);
}
.upload-header {
background: #dfdfdf;
border-top: 1px solid #fff; border-left: 1px solid #fff; border-right: 1px solid #808080; border-bottom: 1px solid #808080;
padding: 8px;
box-shadow: inset 1px 1px 0 #f7f7f7, inset -1px -1px 0 #b0b0b0;
}
.upload-dropzone {
background: repeating-linear-gradient(45deg, #dfdfdf 0 4px, #e9e9e9 4px 8px), #dfdfdf;
border: 1px solid #808080;
box-shadow: inset 1px 1px 0 #fff, inset -1px -1px 0 #808080, inset 2px 2px 0 rgba(0,0,0,.18), 0 1px 0 rgba(255,255,255,.7);
}
.upload-dropzone.is-dragging, .upload-dropzone:hover {
background: repeating-linear-gradient(45deg, #c7d8f2 0 4px, #d8e5f8 4px 8px), #c7d8f2;
outline: 2px dashed #000078; outline-offset: -6px;
}
.upload-dropzone.is-current-step { animation: dropzone-attention 1500ms steps(5, end) infinite; }
@keyframes dropzone-attention {
0%, 100% { filter: brightness(1); transform: translateY(0); }
50% { filter: brightness(1.07); transform: translateY(-1px); box-shadow: inset 1px 1px 0 #fff, inset -1px -1px 0 #808080, inset 2px 2px 0 rgba(0,0,0,.2), 0 0 0 2px rgba(0,0,120,.5); }
}
.upload-details, .upload-result, .upload-quota, .upload-overall-track { box-shadow: inset 1px 1px 0 rgba(0,0,0,.16), inset -1px -1px 0 rgba(255,255,255,.75); }
.upload-file-list {
background: linear-gradient(#fff, #fff), repeating-linear-gradient(90deg, #f8f8ff 0 32px, #fff 32px 64px);
border-top: 2px solid #606060; border-left: 2px solid #606060; border-right: 2px solid #fff; border-bottom: 2px solid #fff;
}
.upload-file-row:nth-child(odd) { background: rgba(255,255,255,.92); }
.upload-file-row:nth-child(even) { background: rgba(240,244,255,.88); }
.upload-file-row:hover { background: #d8e5f8; }
.side-panel:nth-child(2) .side-body { display: flex; flex-direction: column; min-height: 0; }
.terminal-box {
flex: 1 1 auto; min-height: 104px; max-height: 134px; overflow: auto; padding: 10px;
color: #00ff66; background-color: #050505;
background-image: radial-gradient(circle at 14px 10px, rgba(0,255,102,.10), transparent 22px), repeating-linear-gradient(transparent 0 2px, rgba(0,255,102,.055) 2px 4px);
border: 0;
box-shadow: inset 2px 2px 0 #000, inset -1px -1px 0 rgba(255,255,255,.55), 0 0 10px rgba(0,255,102,.12);
text-shadow: 0 0 5px rgba(0,255,102,.75);
}
.terminal-actions { flex: 0 0 auto; margin-top: 8px; padding-top: 2px; }
.start-upload-cta.is-current-step {
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf, 0 0 0 1px #000078, 0 0 8px rgba(0,0,120,.28);
animation: start-ready-breathe 1100ms steps(5, end) infinite;
}
.start-upload-cta.is-current-step::after {
content: ""; position: absolute; inset: -3px; pointer-events: none;
background: repeating-linear-gradient(90deg, rgba(255,0,76,.32) 0 8px, rgba(255,204,0,.32) 8px 16px, rgba(0,210,106,.32) 16px 24px, rgba(0,162,255,.32) 24px 32px, rgba(140,72,255,.32) 32px 40px);
z-index: -1; animation: border-march 900ms steps(8, end) infinite;
}
@keyframes start-ready-breathe { 0%, 100% { transform: rotate(-.25deg) scale(1); } 50% { transform: rotate(.25deg) scale(1.012); } }
.upload-progress-bar, .upload-overall-bar { position: relative; transform-origin: left center; }
.upload-progress-bar.just-completed, .upload-overall-bar.just-completed { animation: progress-impact-bar 520ms steps(5, end) 1; }
.upload-progress-bar.just-completed::after, .upload-overall-bar.just-completed::after {
content: ""; position: absolute; right: -7px; top: 50%; width: 12px; height: 22px; transform: translateY(-50%);
background: repeating-linear-gradient(45deg, rgba(255,255,255,.95) 0 2px, rgba(0,255,102,.85) 2px 4px, transparent 4px 6px);
box-shadow: 0 0 0 1px #fff, 0 0 8px #00ff66; pointer-events: none; animation: progress-impact-spark 520ms steps(5, end) 1;
}
@keyframes progress-impact-bar { 0% { filter: brightness(1); } 35% { filter: brightness(1.75); } 100% { filter: brightness(1); } }
@keyframes progress-impact-spark { 0% { opacity: 0; transform: translateY(-50%) scale(.7); } 30% { opacity: 1; transform: translateY(-50%) scale(1.18); } 100% { opacity: 0; transform: translateY(-50%) scale(.7); } }
.popup-window.is-visible { animation: popup-open-v10 180ms steps(5, end); }
@keyframes popup-open-v10 { from { transform: translate(-50%, -48%) scale(.97); opacity: .35; } to { transform: translate(-50%, -50%) scale(1); opacity: 1; } }
.toast { background: repeating-linear-gradient(45deg, #ffffcc 0 5px, #fff8aa 5px 10px); }
@media (max-width: 760px) {
.popup-window.is-visible { animation: popup-open-mobile-v10-final 160ms steps(5, end); }
@keyframes popup-open-mobile-v10-final { from { transform: translateY(10px); opacity: .35; } to { transform: translateY(0); opacity: 1; } }
}
/* v11 polish and stricter upload-state UX */
.win98-titlebar { background-size: 160% 100%, 8px 100%; animation: titlebar-drift 8s linear infinite; }
@keyframes titlebar-drift { from { background-position: 0 0, 0 0; } to { background-position: 160% 0, 24px 0; } }
.start-upload-cta.is-current-step {
background: var(--w98-gray);
border-color: #ffffff #000000 #000000 #ffffff;
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf, 0 0 0 1px rgba(0,0,120,.75);
animation: start-ready-breathe-subtle 1200ms steps(5, end) infinite;
}
.start-upload-cta.is-current-step::after {
content: "";
position: absolute;
inset: -3px;
pointer-events: none;
z-index: -1;
background: none;
border: 2px solid transparent;
border-image: repeating-linear-gradient(90deg, rgba(255,0,76,.48) 0 8px, rgba(255,204,0,.48) 8px 16px, rgba(0,210,106,.48) 16px 24px, rgba(0,162,255,.48) 24px 32px, rgba(140,72,255,.48) 32px 40px) 1;
animation: border-march 1800ms steps(10, end) infinite;
}
@keyframes start-ready-breathe-subtle { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.006); } }
.terminal-box {
color: #b7ffc8;
background-image: repeating-linear-gradient(transparent 0 3px, rgba(0,255,102,.028) 3px 5px);
box-shadow: inset 2px 2px 0 #000, inset -1px -1px 0 rgba(255,255,255,.38);
text-shadow: none;
}
.terminal-muted { color: #82b68c; }
.helper-body {
justify-content: flex-start;
align-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
gap: 8px;
}
.folder-icon-button { flex: 0 0 86px; }
.folder-icon-button-disabled { color: #606060; }
.folder-icon-button-disabled .folder-icon { filter: grayscale(.9); opacity: .75; }
.folder-icon-exe { background: #dfdfdf; box-shadow: inset -2px -2px 0 #aaa; }
.folder-icon-exe:before { background: #dfdfdf; }
.upload-quota.is-quota-warning {
background: repeating-linear-gradient(45deg, #ffdede 0 5px, #fff2a8 5px 10px);
border-color: #800000 #800000 #800000 #800000;
animation: quota-warning-breathe 900ms steps(4, end) infinite;
}
.upload-quota-bar.is-over-quota {
background-image: repeating-linear-gradient(45deg, #800000 0 7px, #ffcc00 7px 14px);
}
.upload-file-row.is-too-large {
position: relative;
background: #fff0b8 !important;
animation: row-warning-breathe 900ms steps(4, end) infinite;
}
.upload-file-row.is-too-large::after {
content: "";
position: absolute;
inset: 1px;
pointer-events: none;
border: 2px solid transparent;
border-image: repeating-linear-gradient(90deg, #800000 0 8px, #ffcc00 8px 16px) 1;
}
@keyframes row-warning-breathe { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.12); } }
@keyframes quota-warning-breathe { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.08); } }
.upload-dropzone.is-locked {
opacity: .72;
cursor: not-allowed;
filter: grayscale(.3);
}
.upload-dropzone.is-locked .upload-primary::after { content: " - box created"; }
.win98-minimize { align-items: start; padding-top: 0; line-height: 8px; }
::-webkit-scrollbar { width: 17px; height: 17px; }
::-webkit-scrollbar-button:vertical:decrement { background: #c0c0c0; }
::-webkit-scrollbar-button:vertical:increment { background: #c0c0c0; }
::-webkit-scrollbar-button:horizontal:decrement { background: #c0c0c0; }
::-webkit-scrollbar-button:horizontal:increment { background: #c0c0c0; }
/* v12 visual polish, scalable proportions, accessibility, and quota dialogs */
:root { --ui-scale: 1; }
@media (min-width: 1800px) { :root { --base-font-size: 15px; --ui-scale: 1.20; } .desktop-wrap { zoom: var(--ui-scale); } }
@media (min-width: 2048px) { :root { --base-font-size: 16px; --ui-scale: 1.36; } .desktop-wrap { zoom: var(--ui-scale); } }
@media (min-width: 2560px) { :root { --base-font-size: 18px; --ui-scale: 1.58; } .desktop-wrap { zoom: var(--ui-scale); } }
@media (min-width: 3200px) { :root { --base-font-size: 20px; --ui-scale: 1.88; } .desktop-wrap { zoom: var(--ui-scale); } }
.win98-titlebar {
background: linear-gradient(90deg, #000078 0%, #000078 28%, #0f80cd 50%, #000078 72%, #000078 100%) !important;
background-size: 240% 100% !important;
animation: titlebar-center-drift 34s ease-in-out infinite alternate !important;
}
@keyframes titlebar-center-drift {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}
.start-upload-cta.is-current-step {
position: relative;
overflow: visible;
isolation: isolate;
animation: start-ready-rainbow-breathe 1150ms ease-in-out infinite !important;
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #dfdfdf, 0 0 0 1px #000 !important;
}
.start-upload-cta.is-current-step::after {
content: "" !important;
position: absolute;
inset: -4px;
pointer-events: none;
z-index: 1;
padding: 4px;
background: linear-gradient(90deg, #ff004c, #ffcc00, #00d26a, #00a2ff, #8c48ff, #ff004c, #ffcc00);
background-size: 280% 100%;
opacity: .9;
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: start-border-rainbow-slide 1850ms linear infinite;
}
@keyframes start-ready-rainbow-breathe {
0%, 100% { transform: rotate(-.35deg) scale(1); }
50% { transform: rotate(.35deg) scale(1.016); }
}
@keyframes start-border-rainbow-slide { from { background-position: 0% 50%; } to { background-position: 100% 50%; } }
.terminal-box {
color: #b4efbd !important;
background-color: #030403 !important;
background-image: repeating-linear-gradient(transparent 0 4px, rgba(0,255,102,.018) 4px 6px) !important;
box-shadow: inset 1px 1px 0 #000, inset -1px -1px 0 rgba(255,255,255,.22) !important;
text-shadow: none !important;
}
.terminal-muted { color: #79ad83 !important; }
.shortcut-section { margin-top: 12px; padding-top: 8px; border-top: 1px solid #808080; }
.shortcut-list { display: grid; gap: 6px; margin: 8px 0 0; padding: 0; list-style: none; }
.shortcut-list li { display: grid; grid-template-columns: 132px minmax(0,1fr); gap: 8px; align-items: center; padding: 4px; background: #dfdfdf; border: 1px solid #808080; box-shadow: inset 1px 1px 0 #fff; }
.kbd { display: inline-block; min-width: 18px; padding: 1px 5px; color: #000; background: #c0c0c0; border: 1px solid #000; box-shadow: inset 1px 1px 0 #fff, inset -1px -1px 0 #808080; font-family: 'PixelOperator', 'MS Sans Serif', Arial, sans-serif; text-align: center; }
.option-row.with-button { grid-template-columns: 88px minmax(0,1fr) auto; }
.option-row.with-two-buttons { grid-template-columns: 88px minmax(0,1fr) auto auto; }
.mini-win98-button { min-width: 68px; width: auto; height: 22px; padding: 0 7px; font-size: 12px; line-height: 12px; }
.quota-dialog-list { margin: 8px 0; padding: 6px 6px 6px 28px; background: #fff; border-top: 2px solid #808080; border-left: 2px solid #808080; border-right: 2px solid #fff; border-bottom: 2px solid #fff; max-height: 170px; overflow: auto; }
.quota-dialog-list li { padding: 3px 2px; border-bottom: 1px dotted #c0c0c0; }
.quota-dialog-list li:last-child { border-bottom: 0; }
.quota-dialog-summary { padding: 6px; background: #ffffcc; border: 1px solid #808080; }
.toast { border-width: 2px !important; }
.toast.toast-info { background: #ffffcc !important; border-color: #fff #808000 #808000 #fff !important; color: #000; }
.toast.toast-warning { background: repeating-linear-gradient(45deg, #111 0 8px, #ffcc00 8px 16px) !important; color: #fff; text-shadow: 1px 1px 0 #000; border-color: #ffec80 #000 #000 #ffec80 !important; }
.toast.toast-error { background: #b00000 !important; color: #fff; text-shadow: 1px 1px 0 #000; border-color: #ffb0b0 #300 #300 #ffb0b0 !important; }
.upload-quota-track, .upload-overall-track, .upload-progress {
background-color: #fff !important;
background-image: repeating-linear-gradient(to right, rgba(0,0,0,.05) 0 1px, transparent 1px 18px) !important;
}
.upload-quota-bar, .upload-overall-bar, .upload-progress-bar {
background-color: #000078 !important;
background-image: repeating-linear-gradient(to right, rgba(255,255,255,.12) 0 1px, transparent 1px 18px) !important;
}
.upload-text-input:disabled, .upload-select:disabled { cursor: not-allowed; }
.upload-quota { min-width: 250px; }
.popup-window.is-about-popup { width: min(360px, calc(100vw - 28px)); min-height: 220px; }
.popup-window.is-about-popup .popup-body { text-align: center; }
button[disabled], input[disabled], select[disabled], textarea[disabled] { cursor: not-allowed; }
/* v15 fixes: locked options, readable warnings, and large-screen dialog scaling */
.box-options-form.is-locked { opacity: .82; filter: grayscale(.12); }
.box-options-form.is-locked::after { content: "Box sealed after upload"; display: block; margin-top: 8px; padding: 5px 6px; color: #000; background: #dfdfdf; border-top: 1px solid #808080; border-left: 1px solid #808080; border-right: 1px solid #fff; border-bottom: 1px solid #fff; font-size: 12px; line-height: 13px; }
.box-options-form.is-locked input[readonly], .box-options-form.is-locked input:disabled, .box-options-form.is-locked select:disabled { color: #404040; background: repeating-linear-gradient(45deg, #d0d0d0 0 4px, #c7c7c7 4px 8px); }
.toast.toast-warning { color: #000 !important; text-shadow: none !important; background: #ffffcc !important; border: 4px solid transparent !important; border-image: repeating-linear-gradient(45deg, #111 0 8px, #ffcc00 8px 16px) 4 !important; box-shadow: 4px 4px 0 rgba(0,0,0,.45), inset 1px 1px 0 #fff7a8, inset -1px -1px 0 #a08000 !important; }
.toast.toast-error { border-width: 3px !important; }
@media (min-width: 2048px) { .popup-window { width: min(980px, calc(100vw - 120px)); max-height: min(900px, calc(100vh - 120px)); } .popup-window.is-about-popup { width: min(520px, calc(100vw - 120px)); min-height: 300px; } .popup-body { padding: 16px; font-size: 16px; line-height: 20px; max-height: calc(100vh - 170px); } .popup-body h3 { font-size: 21px; line-height: 24px; } .popup-body h4 { font-size: 18px; line-height: 21px; } .popup-body pre { font-size: 14px; line-height: 18px; } .toast { right: 24px; bottom: 84px; max-width: min(560px, calc(100vw - 48px)); padding: 12px 14px; font-size: 16px; line-height: 19px; } .shortcut-list li { grid-template-columns: 168px minmax(0, 1fr); } }
@media (min-width: 3200px) { .popup-window { width: min(1120px, calc(100vw - 180px)); max-height: min(1040px, calc(100vh - 180px)); } .popup-window.is-about-popup { width: min(620px, calc(100vw - 180px)); min-height: 360px; } .popup-body { padding: 20px; font-size: 18px; line-height: 23px; max-height: calc(100vh - 230px); } .popup-body h3 { font-size: 24px; line-height: 28px; } .popup-body h4 { font-size: 20px; line-height: 24px; } .popup-body pre { font-size: 16px; line-height: 21px; } .toast { right: 36px; bottom: 116px; max-width: min(680px, calc(100vw - 72px)); padding: 16px 18px; font-size: 19px; line-height: 23px; } }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; } }
/* v16 UX fixes: duplicate warnings and daily quota dialog */
.duplicate-list { margin: 8px 0; padding: 6px 6px 6px 28px; background: #fff; color: #000; border-top: 2px solid #808080; border-left: 2px solid #808080; border-right: 2px solid #fff; border-bottom: 2px solid #fff; max-height: 180px; overflow: auto; }
.duplicate-list li { padding: 3px 2px; border-bottom: 1px dotted #b0b0b0; }
.duplicate-list li:last-child { border-bottom: 0; }
.quota-meter-list { display: grid; gap: 10px; margin: 10px 0; }
.quota-meter { padding: 8px; background: #dfdfdf; border-top: 1px solid #fff; border-left: 1px solid #fff; border-right: 1px solid #808080; border-bottom: 1px solid #808080; }
.quota-meter-head { display: flex; justify-content: space-between; gap: 10px; margin-bottom: 5px; font-weight: bold; }
.quota-meter-track { height: 18px; background-color: #fff; background-image: repeating-linear-gradient(to right, transparent 0 19px, rgba(0,0,0,.1) 19px 20px); border-top: 2px solid #808080; border-left: 2px solid #808080; border-right: 2px solid #fff; border-bottom: 2px solid #fff; overflow: hidden; }
.quota-meter-bar { display: block; height: 100%; background: #000078; }
.quota-note { padding: 8px; background: #ffffcc; border: 1px solid #808080; }
</style>
</head>
<body>
<!-- TODO: When the user changes settings, make sure to save them and remember them in browser's LocalStorage. -->
<main>
<div class="desktop-wrap">
<section class="win98-window upload-window" aria-label="WarpBox upload window">
<div class="win98-titlebar upload-titlebar">
<div class="win98-titlebar-label">
<!-- TODO: Replace with an image from static folder when prompted to do so. -->
<span class="win98-titlebar-icon" aria-hidden="true">W</span>
<h1>WarpBox</h1>
</div>
<div class="win98-window-controls" aria-label="Window controls">
<button class="win98-control win98-minimize" type="button" data-action="minimize" title="Minimize" aria-label="Minimize">_</button>
<button class="win98-control" type="button" data-action="toggle-fit" title="Fit window" aria-label="Maximize"></button>
<button class="win98-control" type="button" data-action="fake-close" title="Close" aria-label="Close">×</button>
</div>
</div>
<nav class="menu-bar" aria-label="Upload menu">
<div class="menu-item">
<button class="menu-button" type="button" aria-expanded="false"><u>F</u>ile</button>
<div class="menu-popup" role="menu">
<button class="menu-action" type="button" data-action="browse"><span></span><span>Add files...</span><span class="shortcut">Ctrl+O</span></button>
<button class="menu-action" type="button" data-action="start-upload"><span></span><span>Start upload</span><span class="shortcut">Enter</span></button>
<button class="menu-action" type="button" data-action="copy-link"><span></span><span>Copy share URL</span><span></span></button>
<div class="menu-separator"></div>
<button class="menu-action" type="button" data-action="clear"><span>×</span><span>Clear queue</span><span></span></button>
</div>
</div>
<div class="menu-item">
<button class="menu-button" type="button" aria-expanded="false"><u>B</u>ox</button>
<div class="menu-popup" role="menu">
<button class="menu-action" type="button" data-expiry="1h"><span></span><span>Expire in 1 hour</span><span></span></button>
<button class="menu-action" type="button" data-expiry="24h"><span></span><span>Expire in 24 hours</span><span></span></button>
<button class="menu-action" type="button" data-expiry="7d"><span></span><span>Expire in 7 days</span><span></span></button>
<button class="menu-action" type="button" data-expiry="30d"><span></span><span>Expire in 30 days</span><span></span></button>
<div class="menu-separator"></div>
<button class="menu-action" type="button" data-action="toggle-delete-once"><span></span><span>Delete after first download</span><span></span></button>
<div class="menu-separator"></div>
<button class="menu-action" type="button" data-doc="dailyQuota"><span></span><span>Daily guest quota...</span><span></span></button>
</div>
</div>
<div class="menu-item">
<button class="menu-button" type="button" aria-expanded="false"><u>O</u>ptions</button>
<div class="menu-popup" role="menu">
<button class="menu-action" type="button" data-action="random-password"><span></span><span>Generate password</span><span></span></button>
<button class="menu-action" type="button" data-action="random-box-name"><span></span><span>Random box name</span><span></span></button>
<button class="menu-action" type="button" data-action="clear-password"><span></span><span>Clear password</span><span></span></button>
<button class="menu-action" type="button" data-action="toggle-page"><span></span><span>Download page</span><span></span></button>
</div>
</div>
<div class="menu-item">
<button class="menu-button" type="button" aria-expanded="false"><u>H</u>elp</button>
<div class="menu-popup" role="menu">
<button class="menu-action" type="button" data-action="help"><span>?</span><span>Show quick help</span><span>F1</span></button>
<button class="menu-action" type="button" data-action="terminal-help"><span>&gt;</span><span>Show curl command</span><span></span></button>
<button class="menu-action" type="button" data-doc="about"><span>i</span><span>About WarpBox</span><span></span></button>
</div>
</div>
</nav>
<form class="upload-form" id="upload-form">
<div class="win98-panel upload-panel" id="drop-surface">
<header class="upload-header">
<div>
<p class="upload-heading">Upload files</p>
<p class="upload-subtext">Drop files into this window or browse from your computer. WarpBox creates a temporary share link when the upload finishes.</p>
</div>
<aside class="upload-quota" aria-label="Box space">
<strong>Box space</strong>
<span id="box-space-text">0 B / 4 GB</span>
<span class="upload-quota-track" aria-hidden="true"><span class="upload-quota-bar" id="box-space-bar"></span></span>
</aside>
</header>
<label class="upload-dropzone" for="file-upload" tabindex="0" id="dropzone">
<span class="upload-icon" aria-hidden="true"></span>
<span class="upload-primary">Drop files here</span>
<span class="upload-secondary">or <span class="upload-linklike">click to browse</span> from your computer</span>
<span class="upload-secondary">Max box: 4 GB · max file: 2 GB · links expire automatically</span>
<input class="upload-input" id="file-upload" type="file" multiple data-disabled-reason="The current box is sealed after upload. Press Clear to start a new box." />
</label>
<div class="upload-details">
<span class="upload-detail-label">Queue:</span>
<span id="queue-label">No files selected</span>
<span class="upload-file-count" id="queue-size">0 B total</span>
</div>
<div class="upload-file-list" aria-label="Upload queue" id="file-list">
<p class="upload-empty-state">No files in the box yet. Drop files here, use File &gt; Add files, or click the dropzone.</p>
</div>
<div class="upload-result">
<span class="upload-result-label">Share URL:</span>
<a class="upload-result-link is-empty" id="share-link" href="#">Not created yet</a>
<button class="win98-button upload-share-button" type="button" id="copy-button" disabled data-disabled-reason="There is no share URL yet. Start an upload first.">Copy</button>
</div>
</div>
<div class="upload-overall">
<span class="upload-overall-track" aria-label="Overall upload progress"><span class="upload-overall-bar" id="overall-bar"></span></span>
<span class="upload-overall-percent" id="overall-percent">0%</span>
</div>
<div class="upload-actions">
<button class="win98-button" type="button" data-action="clear">Clear</button>
<button class="win98-button start-upload-cta" type="submit" id="start-button" data-disabled-reason="Start upload is unavailable right now.">Start upload</button>
</div>
</form>
<div class="win98-statusbar upload-statusbar">
<span id="status-text">Ready · drag files anywhere onto the window</span>
<span>WarpBox</span>
</div>
</section>
<aside class="side-stack" aria-label="Secondary upload panels">
<section class="win98-window side-panel">
<div class="win98-titlebar">
<div class="win98-titlebar-label">
<!-- TODO: Replace with an image from static folder when prompted to do so. -->
<span class="win98-titlebar-icon" aria-hidden="true">i</span>
<h2>Box Options</h2>
</div>
<div class="win98-window-controls"><button class="win98-control" type="button" data-action="side-close" title="Close-ish">×</button></div>
</div>
<div class="win98-panel side-body">
<div class="box-options-form" id="box-options-form">
<label class="option-row">
<span>Expires:</span>
<select class="upload-select" id="expiry-select">
<option value="1h">1 hour</option>
<option value="24h">24 hours</option>
<option value="7d" selected>7 days</option>
<option value="30d">30 days</option>
<option value="manual">Manual delete</option>
</select>
</label>
<label class="option-row">
<span>Password:</span>
<input class="upload-text-input" id="password-input" type="text" placeholder="optional" autocomplete="off" />
</label>
<label class="option-row">
<span>Max views:</span>
<input class="upload-text-input" id="max-views" type="number" min="1" max="9999" placeholder="unlimited" />
</label>
<label class="option-row">
<span>Box name:</span>
<input class="upload-text-input" id="box-name" type="text" maxlength="42" placeholder="optional, normal text" />
</label>
<label class="option-row">
<span>Custom slug:</span>
<input class="upload-text-input" id="custom-slug" type="text" maxlength="32" placeholder="optional" />
</label>
<label class="option-check">
<input type="checkbox" id="download-page" checked />
<span>Generate download page</span>
</label>
<label class="option-check">
<input type="checkbox" id="delete-once" />
<span>Delete after first download</span>
</label>
<label class="option-check">
<input type="checkbox" id="allow-preview" checked />
<span>Allow previews when possible</span>
</label>
<label class="option-check">
<input type="checkbox" id="keep-filenames" checked />
<span>Keep original filenames</span>
</label>
<label class="option-check">
<input type="checkbox" id="private-box" />
<span>Hide from public listings</span>
</label>
<label class="option-check">
<input type="checkbox" id="api-key-mode" />
<span>Use API key for larger quota</span>
</label>
<label class="option-row api-key-row" id="api-key-row">
<span>API key:</span>
<!-- TODO: When the API key is inserted, add a loading overlay over this field, disable the field, and clearly show validation progress while checking if the API key is valid. -->
<input class="upload-text-input" id="api-key-input" type="password" placeholder="paste key when enabled" autocomplete="off" disabled data-disabled-reason="Enable Use API key for larger quota before typing an API key." />
</label>
</div>
</div>
</section>
<section class="win98-window side-panel">
<div class="win98-titlebar">
<div class="win98-titlebar-label">
<!-- TODO: Replace with an image from static folder when prompted to do so. -->
<span class="win98-titlebar-icon" aria-hidden="true">&gt;</span>
<h2>Terminal Upload</h2>
</div>
<div class="win98-window-controls"><button class="win98-control" type="button" data-action="side-help" title="Help-ish">?</button></div>
</div>
<div class="win98-panel side-body">
<div class="terminal-box" id="terminal-box" tabindex="0" aria-label="Terminal upload command"><span class="terminal-muted">warpbox@cli</span>:~$ curl \
-F 'file=@build.zip' \
-F 'expires=7d' \
https://warpbox.dev/u</div>
<div class="terminal-actions">
<button class="win98-button terminal-copy-button" type="button" id="copy-curl-button">Copy cURL command</button>
</div>
</div>
</section>
<section class="win98-window helper-window" aria-label="WarpBox help folder">
<div class="win98-titlebar">
<div class="win98-titlebar-label"><!-- TODO: Replace with an image from static folder when prompted to do so. -->
<span class="win98-titlebar-icon" aria-hidden="true">?</span><h2>WarpBox Help Folder</h2></div>
<div class="win98-window-controls"><button class="win98-control" type="button" data-action="side-folder-close" title="Nope">×</button></div>
</div>
<div class="win98-panel helper-body">
<button class="folder-icon-button" type="button" data-doc="cli"><!-- TODO: Replace with an image from static folder when prompted to do so. -->
<span class="folder-icon" aria-hidden="true"></span><span>CLI Guide</span></button>
<button class="folder-icon-button" type="button" data-doc="faq"><!-- TODO: Replace with an image from static folder when prompted to do so. -->
<span class="folder-icon" aria-hidden="true"></span><span>Help &amp; FAQ</span></button>
<button class="folder-icon-button" type="button" data-doc="examples"><!-- TODO: Replace with an image from static folder when prompted to do so. -->
<span class="folder-icon" aria-hidden="true"></span><span>Examples</span></button>
<button class="folder-icon-button folder-icon-button-disabled" type="button" data-action="coming-soon"><!-- TODO: Replace with an image from static folder when prompted to do so. -->
<span class="folder-icon folder-icon-exe" aria-hidden="true"></span><span>WarpBox.exe</span></button>
</div>
</section>
</aside>
</div>
</main>
<div class="modal-backdrop" id="modal-backdrop"></div>
<section class="win98-window popup-window" id="doc-popup" aria-modal="true" role="dialog" aria-labelledby="doc-popup-title">
<div class="win98-titlebar">
<div class="win98-titlebar-label"><!-- TODO: Replace with an image from static folder when prompted to do so. -->
<span class="win98-titlebar-icon" aria-hidden="true">?</span><h2 id="doc-popup-title">WarpBox Help</h2></div>
<div class="win98-window-controls"><button class="win98-control popup-close" type="button" id="doc-popup-close" title="Close">×</button></div>
</div>
<div class="win98-panel popup-body" id="doc-popup-body"></div>
</section>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script>
const MAX_BOX_BYTES = 4 * 1024 * 1024 * 1024;
const MAX_FILE_BYTES = 2 * 1024 * 1024 * 1024;
const demoFiles = [
{ name: 'dashboard_mockup_final.png', size: 4.6 * 1024 * 1024, progress: 100, uploaded: true },
{ name: 'warpbox-release-linux-x86_64.zip', size: 842 * 1024 * 1024, progress: 0, uploaded: false },
{ name: 'terminal_demo_capture.mp4', size: 391 * 1024 * 1024, progress: 0, uploaded: false }
];
let files = [];
let uploadTimer = null;
let shareUrl = '';
let uploadLocked = false;
let impactedFiles = new Set();
let overallImpactDone = false;
let pendingDuplicateFiles = [];
const el = {
fileInput: document.getElementById('file-upload'),
dropSurface: document.getElementById('drop-surface'),
dropzone: document.getElementById('dropzone'),
fileList: document.getElementById('file-list'),
queueLabel: document.getElementById('queue-label'),
queueSize: document.getElementById('queue-size'),
boxSpaceText: document.getElementById('box-space-text'),
boxSpaceBar: document.getElementById('box-space-bar'),
overallBar: document.getElementById('overall-bar'),
overallPercent: document.getElementById('overall-percent'),
shareLink: document.getElementById('share-link'),
copyButton: document.getElementById('copy-button'),
startButton: document.getElementById('start-button'),
statusText: document.getElementById('status-text'),
form: document.getElementById('upload-form'),
toast: document.getElementById('toast'),
clock: document.getElementById('clock'),
terminal: document.getElementById('terminal-box'),
copyCurlButton: document.getElementById('copy-curl-button'),
docPopup: document.getElementById('doc-popup'),
modalBackdrop: document.getElementById('modal-backdrop'),
docPopupTitle: document.getElementById('doc-popup-title'),
docPopupBody: document.getElementById('doc-popup-body'),
docPopupClose: document.getElementById('doc-popup-close'),
expiry: document.getElementById('expiry-select'),
password: document.getElementById('password-input'),
optionsForm: document.getElementById('box-options-form'),
maxViews: document.getElementById('max-views'),
boxName: document.getElementById('box-name'),
customSlug: document.getElementById('custom-slug'),
allowPreview: document.getElementById('allow-preview'),
keepFilenames: document.getElementById('keep-filenames'),
privateBox: document.getElementById('private-box'),
apiKeyMode: document.getElementById('api-key-mode'),
apiKeyInput: document.getElementById('api-key-input'),
apiKeyRow: document.getElementById('api-key-row'),
downloadPage: document.getElementById('download-page'),
deleteOnce: document.getElementById('delete-once'),
startWrap: document.getElementById('start-wrap'),
startMenuButton: document.getElementById('start-menu-button')
};
function formatBytes(bytes) {
if (!bytes) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let value = bytes;
let unit = 0;
while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit++; }
return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`;
}
function extension(name) {
const parts = name.split('.');
return parts.length > 1 ? parts.pop().slice(0, 3).toUpperCase() : 'FILE';
}
function totalBytes() { return files.reduce((sum, file) => sum + file.size, 0); }
function uploadedBytes() { return files.reduce((sum, file) => sum + file.size * (file.progress / 100), 0); }
function overallProgress() {
const total = totalBytes();
return total ? Math.round((uploadedBytes() / total) * 100) : 0;
}
function oversizedFiles() { return files.filter(file => file.size > MAX_FILE_BYTES); }
function isOverBoxQuota() { return totalBytes() > MAX_BOX_BYTES; }
function hasQuotaError() { return isOverBoxQuota() || oversizedFiles().length > 0; }
function normalizedFileName(name) { return String(name || '').trim().toLowerCase(); }
function splitNameForIncrement(name) {
const value = String(name || 'file');
const dot = value.lastIndexOf('.');
if (dot > 0 && dot < value.length - 1) return [value.slice(0, dot), value.slice(dot)];
return [value, ''];
}
function nextIncrementedFileName(name, usedNames) {
const [base, ext] = splitNameForIncrement(name);
let index = 2;
let candidate = `${base} (${index})${ext}`;
while (usedNames.has(normalizedFileName(candidate))) {
index += 1;
candidate = `${base} (${index})${ext}`;
}
usedNames.add(normalizedFileName(candidate));
return candidate;
}
function duplicateFileReport(incoming = []) {
const used = new Set(files.map(file => normalizedFileName(file.name)));
const duplicates = [];
const unique = [];
incoming.forEach(file => {
const key = normalizedFileName(file.name);
if (used.has(key)) duplicates.push(file);
else { used.add(key); unique.push(file); }
});
return { unique, duplicates };
}
function showDuplicateDialog(duplicates) {
pendingDuplicateFiles = duplicates.map(file => ({ ...file }));
const list = duplicates.map(file => `<li><strong>${htmlEscape(file.name)}</strong> <span>${formatBytes(file.size)}</span></li>`).join('');
el.docPopupTitle.textContent = 'Duplicate file names';
el.docPopupBody.innerHTML = `
<h3>Duplicate file names detected</h3>
<p>These files are already selected for upload, or have the same exact name as an existing file in this box.</p>
<ol class="duplicate-list">${list}</ol>
<p>Skip them, or append numbers so they become names like <code>file (2).zip</code>.</p>
<div class="copy-fallback-actions">
<button class="win98-button" type="button" id="duplicate-append">Append numbers</button>
<button class="win98-button" type="button" id="duplicate-skip">Skip duplicates</button>
</div>`;
el.docPopup.classList.add('is-visible');
if (el.modalBackdrop) el.modalBackdrop.classList.add('is-visible');
showToast('Duplicate names found. Choose skip or append numbers.', 'warning');
setTimeout(() => document.getElementById('duplicate-append')?.focus(), 0);
}
function appendPendingDuplicates() {
if (!pendingDuplicateFiles.length) return;
const used = new Set(files.map(file => normalizedFileName(file.name)));
const renamed = pendingDuplicateFiles.map(file => ({ ...file, name: nextIncrementedFileName(file.name, used) }));
files.push(...renamed);
pendingDuplicateFiles = [];
shareUrl = '';
setShareUrl('');
renderFiles();
const warning = quotaWarningMessage(renamed);
if (warning) showWarningDialog('Quota warning', warning);
setStatus(`${renamed.length} duplicate file${renamed.length === 1 ? '' : 's'} added with numbered names`);
showToast('Duplicate files added with numbered names.', 'info');
}
function quotaWarningMessage(incoming = []) {
const tooBig = [...incoming, ...files].filter(file => file.size > MAX_FILE_BYTES);
const total = totalBytes();
if (tooBig.length) {
const list = tooBig.slice(0, 4).map(file => `${file.name} (${formatBytes(file.size)})`).join(', ');
const more = tooBig.length > 4 ? ` and ${tooBig.length - 4} more` : '';
return `These files are over the single-file demo limit of ${formatBytes(MAX_FILE_BYTES)}: ${list}${more}. Remove them or use an API key plan with higher quota.`;
}
if (total > MAX_BOX_BYTES) return `This box is ${formatBytes(total - MAX_BOX_BYTES)} over the 4 GB limit. Remove some files, clear the box, or use an API key with larger quota.`;
return '';
}
function setStatus(message) { el.statusText.textContent = message; }
function showToast(message, type = 'info') {
el.toast.textContent = message;
el.toast.classList.remove('toast-info', 'toast-warning', 'toast-error', 'is-visible');
el.toast.classList.add(`toast-${type}`, 'is-visible');
clearTimeout(showToast.timer);
showToast.timer = setTimeout(() => el.toast.classList.remove('is-visible'), 2600);
}
function updateCurrentStep() {
const hasFiles = files.length > 0;
const allDone = hasFiles && files.every(file => file.progress >= 100);
el.dropzone.classList.toggle('is-current-step', !hasFiles);
const startButton = document.getElementById('start-button');
if (startButton) startButton.classList.toggle('is-current-step', hasFiles && !allDone && !uploadLocked && !hasQuotaError());
const resultBox = document.querySelector('.upload-result');
if (resultBox) resultBox.classList.toggle('is-current-step', allDone && !!shareUrl);
}
function bumpCompletedBars() {
document.querySelectorAll('.upload-file-row.is-uploaded').forEach(row => {
const index = Number(row.dataset.index);
const file = files[index];
if (!file || file.progress < 100 || impactedFiles.has(index)) return;
impactedFiles.add(index);
const bar = row.querySelector('.upload-progress-bar');
if (!bar) return;
bar.classList.add('just-completed');
setTimeout(() => bar.classList.remove('just-completed'), 620);
});
if (overallProgress() === 100 && files.length && !overallImpactDone) {
overallImpactDone = true;
el.overallBar.classList.add('just-completed');
setTimeout(() => el.overallBar.classList.remove('just-completed'), 620);
}
}
function setDisabledReasons() {
if (el.startButton) {
let reason = '';
if (uploadLocked) reason = 'This upload already started. Press Clear to create another box.';
else if (hasQuotaError()) reason = 'Over maximum upload size. Remove highlighted files or clear some files.';
else if (!files.length) reason = 'There are no files selected. Please select files to upload.';
else reason = 'Start upload is unavailable right now.';
el.startButton.dataset.disabledReason = reason;
}
if (el.copyButton) {
el.copyButton.dataset.disabledReason = shareUrl ? '' : 'There is no share URL yet. Start an upload first.';
}
if (el.fileInput) {
el.fileInput.dataset.disabledReason = uploadLocked ? 'The current box is sealed after upload. Press Clear to start a new box.' : '';
}
document.querySelectorAll('.upload-file-remove:disabled').forEach(button => {
button.dataset.disabledReason = 'Files cannot be removed after the box is created. Press Clear to start a separate box.';
});
if (el.apiKeyInput && el.apiKeyInput.disabled) {
el.apiKeyInput.dataset.disabledReason = uploadLocked ? 'Box Options are locked because this box was already created. Press Clear to start another upload.' : 'Enable "Use API key for larger quota" before typing an API key.';
}
if (uploadLocked) {
[el.expiry, el.password, el.maxViews, el.boxName, el.customSlug, el.downloadPage, el.deleteOnce, el.allowPreview, el.keepFilenames, el.privateBox, el.apiKeyMode, el.apiKeyInput].filter(Boolean).forEach(control => {
control.dataset.disabledReason = 'Box Options are locked because this box was already created. Press Clear to start another upload.';
});
}
}
function setBoxOptionsLocked(locked) {
const controls = [el.expiry, el.password, el.maxViews, el.boxName, el.customSlug, el.downloadPage, el.deleteOnce, el.allowPreview, el.keepFilenames, el.privateBox, el.apiKeyMode, el.apiKeyInput].filter(Boolean);
if (el.optionsForm) el.optionsForm.classList.toggle('is-locked', locked);
controls.forEach(control => {
const reason = 'Box Options are locked because this box was already created. Press Clear to start another upload.';
control.dataset.disabledReason = locked ? reason : (control.dataset.disabledReason || '');
if (control.tagName === 'INPUT' && !['checkbox', 'radio', 'file'].includes(control.type)) {
control.readOnly = locked;
} else if (control !== el.apiKeyInput) {
control.disabled = locked;
}
});
if (el.password) el.password.type = locked ? 'password' : 'text';
if (!locked) syncApiKeyField();
setDisabledReasons();
}
function renderFiles() {
if (!files.length) {
el.fileList.innerHTML = '<p class="upload-empty-state">No files in the box yet. Drop files here, use File &gt; Add files, or click the dropzone.</p>';
} else {
el.fileList.innerHTML = files.map((file, index) => {
const state = file.progress >= 100 ? 'is-uploaded' : (file.progress > 0 ? 'is-working' : '');
const quotaState = file.size > MAX_FILE_BYTES ? 'is-too-large' : '';
return `
<div class="upload-file-row ${state} ${quotaState}" data-index="${index}">
<!-- TODO: Replace with an image from static folder when prompted to do so. -->
<span class="upload-file-icon">${extension(file.name)}</span>
<span class="upload-file-name" title="${file.name.replaceAll('"', '&quot;')}">${file.name}</span>
<span class="upload-file-size">${formatBytes(file.size)}</span>
<button class="win98-button upload-file-remove" type="button" title="${uploadLocked ? 'This file cannot be removed because this upload box was already created.' : 'Remove file'}" data-disabled-reason="${uploadLocked ? 'This file cannot be removed because this upload box was already created. Press Clear to create another upload.' : ''}" data-remove="${index}" ${uploadLocked ? 'disabled' : ''}>×</button>
<span class="upload-progress" aria-label="Upload progress ${Math.round(file.progress)} percent"><span class="upload-progress-bar" style="width: ${Math.min(100, file.progress)}%"></span></span>
</div>`;
}).join('');
}
const count = files.length;
el.queueLabel.textContent = count ? `${count} file${count === 1 ? '' : 's'} selected` : 'No files selected';
el.queueSize.textContent = `${formatBytes(totalBytes())} total`;
const usedPercentRaw = Math.round((totalBytes() / MAX_BOX_BYTES) * 100);
const usedPercent = Math.min(100, usedPercentRaw);
const overQuota = isOverBoxQuota();
const overFile = oversizedFiles().length > 0;
const quotaBox = document.querySelector('.upload-quota');
if (quotaBox) quotaBox.classList.toggle('is-quota-warning', overQuota || overFile);
el.boxSpaceText.textContent = overQuota ? `${formatBytes(totalBytes())} / 4 GB - over quota` : `${formatBytes(totalBytes())} / 4 GB`;
el.boxSpaceBar.style.width = `${usedPercent}%`;
el.boxSpaceBar.classList.toggle('is-over-quota', overQuota || overFile);
if (el.startButton) {
el.startButton.disabled = uploadLocked || hasQuotaError();
el.startButton.title = hasQuotaError() ? 'Over maximum upload size!' : '';
}
const progress = overallProgress();
el.overallBar.style.width = `${progress}%`;
el.overallPercent.textContent = `${progress}%`;
if (files.length && files.every(file => file.progress >= 100)) createShareLink();
updateTerminal();
updateCurrentStep();
requestAnimationFrame(bumpCompletedBars);
}
function htmlEscape(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function shellQuote(value) {
return `'${String(value).replaceAll("'", "'\\''")}'`;
}
function getCurlCommand({ full = true } = {}) {
const args = [];
const selectedFiles = files.length ? files : [{ name: 'build.zip' }];
const previewLimit = full ? selectedFiles.length : 4;
selectedFiles.slice(0, previewLimit).forEach(file => args.push(` -F ${shellQuote(`file=@${file.name}`)}`));
const hiddenFileCount = !full && selectedFiles.length > previewLimit ? selectedFiles.length - previewLimit : 0;
args.push(` -F ${shellQuote(`expires=${el.expiry.value}`)}`);
if (el.password.value) args.push(` -F ${shellQuote('password=YOUR_PASSWORD')}`);
if (el.maxViews.value) args.push(` -F ${shellQuote(`max_views=${el.maxViews.value}`)}`);
if (el.boxName && el.boxName.value) args.push(` -F ${shellQuote(`box_name=${el.boxName.value}`)}`);
if (el.customSlug && el.customSlug.value) args.push(` -F ${shellQuote(`slug=${el.customSlug.value}`)}`);
if (el.deleteOnce.checked) args.push(` -F ${shellQuote('delete_after_first_download=true')}`);
if (el.allowPreview && !el.allowPreview.checked) args.push(` -F ${shellQuote('preview=false')}`);
if (el.keepFilenames && !el.keepFilenames.checked) args.push(` -F ${shellQuote('keep_filenames=false')}`);
if (el.privateBox && el.privateBox.checked) args.push(` -F ${shellQuote('visibility=private')}`);
const commandLines = ['curl'];
if (el.apiKeyMode && el.apiKeyMode.checked) commandLines.push(` -H ${shellQuote('Authorization: Bearer YOUR_API_KEY')}`);
commandLines.push(...args, ' https://warpbox.dev/u');
const command = commandLines.join(' \\' + '\n');
return hiddenFileCount ? `${command}\n# and ${hiddenFileCount} other files included when copying` : command;
}
function updateTerminal() {
const command = getCurlCommand({ full: false });
el.terminal.innerHTML = `<span class="terminal-muted">warpbox@cli</span>:~$ ${htmlEscape(command)}`;
}
function showCopyFallback(kind, value, openUrl) {
el.docPopupTitle.textContent = `${kind} copy failed`;
el.docPopupBody.innerHTML = `
<h3>Clipboard access failed</h3>
<p>The browser refused clipboard access. Copy it manually from the field below, or open the URL in a new tab if available.</p>
<textarea class="copy-fallback-text" readonly>${htmlEscape(value)}</textarea>
<div class="copy-fallback-actions">
${openUrl ? `<a class="win98-button" href="${htmlEscape(openUrl)}" target="_blank" rel="noreferrer">Open in new tab</a>` : ''}
<button class="win98-button" type="button" id="fallback-close">Close</button>
</div>`;
el.docPopup.classList.toggle('is-about-popup', name === 'about');
el.docPopup.classList.add('is-visible');
if (el.modalBackdrop) el.modalBackdrop.classList.add('is-visible');
setTimeout(() => { const close = document.getElementById('fallback-close'); if (close) close.addEventListener('click', closeDoc); }, 0);
}
function quotaWarningHtml(message) {
const tooLarge = oversizedFiles();
const overTotal = isOverBoxQuota();
const parts = [];
if (tooLarge.length) {
parts.push('<p class="quota-dialog-summary"><strong>Single-file limit exceeded.</strong> Remove these files or use a plan with a larger quota.</p>');
parts.push('<ol class="quota-dialog-list">' + tooLarge.map(file => `<li><strong>${htmlEscape(file.name)}</strong> <span>${formatBytes(file.size)} / max ${formatBytes(MAX_FILE_BYTES)}</span></li>`).join('') + '</ol>');
}
if (overTotal) {
parts.push(`<p class="quota-dialog-summary"><strong>Box quota exceeded.</strong> Current total is ${formatBytes(totalBytes())}. The demo limit is ${formatBytes(MAX_BOX_BYTES)}. Remove ${formatBytes(totalBytes() - MAX_BOX_BYTES)} or more.</p>`);
}
if (!parts.length) parts.push(`<p>${htmlEscape(message)}</p>`);
parts.push('<p>Fix the highlighted rows, clear some files, or enable API key mode if your account allows a bigger quota.</p>');
return parts.join('');
}
function showWarningDialog(title, message) {
el.docPopupTitle.textContent = title;
el.docPopupBody.innerHTML = `
<h3>${htmlEscape(title)}</h3>
${quotaWarningHtml(message)}
<div class="copy-fallback-actions"><button class="win98-button" type="button" id="fallback-close">OK</button></div>`;
el.docPopup.classList.add('is-visible');
if (el.modalBackdrop) el.modalBackdrop.classList.add('is-visible');
setTimeout(() => { const close = document.getElementById('fallback-close'); if (close) close.addEventListener('click', closeDoc); }, 0);
}
async function copyCurlCommand() {
const command = getCurlCommand({ full: true });
try {
await navigator.clipboard.writeText(command);
showToast('cURL command copied. The shell beast is pleased.');
setStatus('Copied cURL command');
} catch (_) {
showCopyFallback('cURL command', command, '');
}
}
const docs = {
cli: {
title: 'CLI Guide',
html: `
<h3>Upload with cURL</h3>
<p>WarpBox accepts normal multipart form uploads. The basic shape is:</p>
<pre>curl \\
-F 'file=@./my-file.zip' \\
-F 'expires=7d' \\
https://warpbox.dev/u</pre>
<h4>Common fields</h4>
<ul>
<li><strong>file</strong>: one or more files. Repeat <code>-F 'file=@...'</code> for multiple files.</li>
<li><strong>expires</strong>: example values are <code>1h</code>, <code>24h</code>, <code>7d</code>, <code>30d</code>, or <code>manual</code>.</li>
<li><strong>password</strong>: optional download password.</li>
<li><strong>max_views</strong>: optional download/view limit.</li>
<li><strong>delete_after_first_download</strong>: set to <code>true</code> for one-time downloads.</li>
</ul>
<h4>API key for bigger quota</h4>
<p>Users with API keys can add an authorization header:</p>
<pre>curl \\
-H 'Authorization: Bearer YOUR_API_KEY' \\
-F 'file=@./big-archive.zip' \\
-F 'expires=30d' \\
https://warpbox.dev/u</pre>
<h4>Alias option</h4>
<pre>alias warpbox='curl -H "Authorization: Bearer YOUR_API_KEY" -F expires=7d https://warpbox.dev/u -F file=@'</pre>
<p>Then upload with:</p>
<pre>warpbox ./build.zip</pre>
<h4>Small bash helper</h4>
<p>Create a local executable helper:</p>
<pre>vim warpbox
chmod +x warpbox
mkdir -p ~/.local/bin
mv warpbox ~/.local/bin/</pre>
<p>Example script content:</p>
<pre>#!/usr/bin/env bash
set -euo pipefail
API_KEY="\${WARPBOX_API_KEY:-}"
EXPIRES="\${WARPBOX_EXPIRES:-7d}"
if [ "$#" -lt 1 ]; then
echo "usage: warpbox FILE [FILE...]"
exit 1
fi
args=()
for file in "$@"; do
args+=( -F "file=@\${file}" )
done
if [ -n "$API_KEY" ]; then
curl -H "Authorization: Bearer \${API_KEY}" \\
"\${args[@]}" \\
-F "expires=\${EXPIRES}" \\
https://warpbox.dev/u
else
curl "\${args[@]}" \\
-F "expires=\${EXPIRES}" \\
https://warpbox.dev/u
fi</pre>
<p>Put <code>export WARPBOX_API_KEY='your-key'</code> in <code>~/.bashrc</code>, <code>~/.zshrc</code>, or another shell profile. For system-wide install, move the helper to <code>/usr/local/bin/warpbox</code>.</p>
`
},
faq: {
title: 'Help & FAQ',
html: `
<h3>Help & FAQ</h3>
<section class="shortcut-section">
<h4>Keyboard shortcuts</h4>
<p>These are meant to make the upload page usable without a mouse.</p>
<ul class="shortcut-list">
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">O</span></span><span>Browse for files.</span></li>
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">U</span></span><span>Start the current upload.</span></li>
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">K</span></span><span>Copy the full cURL command.</span></li>
<li><span><span class="kbd">Ctrl</span> + <span class="kbd">L</span></span><span>Copy the share URL after upload.</span></li>
<li><span><span class="kbd">Shift</span> + <span class="kbd">1</span></span><span>Focus the upload queue.</span></li>
<li><span><span class="kbd">Shift</span> + <span class="kbd">2</span></span><span>Focus Box Options.</span></li>
<li><span><span class="kbd">Shift</span> + <span class="kbd">3</span></span><span>Focus Terminal Upload.</span></li>
<li><span><span class="kbd">Shift</span> + <span class="kbd">4</span></span><span>Focus WarpBox Help Folder.</span></li>
<li><span><span class="kbd">F1</span></span><span>Open this Help & FAQ window.</span></li>
<li><span><span class="kbd">Esc</span></span><span>Close menus and popups.</span></li>
</ul>
</section>
<h4>Questions and answers</h4>
<div class="faq-list">
<div class="faq-item"><p class="faq-q"><span class="faq-icon">?</span><span>What is WarpBox?</span></p><p class="faq-a"><span class="faq-icon">i</span><span>A temporary file hosting service for quick browser uploads, terminal uploads, and API based uploads.</span></p></div>
<div class="faq-item"><p class="faq-q"><span class="faq-icon">?</span><span>Can I password protect uploads?</span></p><p class="faq-a"><span class="faq-icon">i</span><span>Yes. Set a password in Box Options or pass <code>-F 'password=...'</code> via the API.</span></p></div>
<div class="faq-item"><p class="faq-q"><span class="faq-icon">?</span><span>How do I upload from a terminal?</span></p><p class="faq-a"><span class="faq-icon">i</span><span>Use the cURL command shown in Terminal Upload. Select files first, then press <strong>Copy cURL command</strong>. The visible preview may collapse long file lists, but the copied command includes every file.</span></p></div>
<div class="faq-item"><p class="faq-q"><span class="faq-icon">?</span><span>What do API keys do?</span></p><p class="faq-a"><span class="faq-icon">i</span><span>They are intended for larger quota, automation, and scripted uploads. Enable API key mode in Box Options to reveal the API key field.</span></p></div>
<div class="faq-item"><p class="faq-q"><span class="faq-icon">?</span><span>Do the toolbar menus actually do anything?</span></p><p class="faq-a"><span class="faq-icon">i</span><span>Yes. File, Box, Options, and Help include extra actions such as adding files, starting uploads, changing expiry, generating passwords, copying commands, opening docs, and clearing the queue.</span></p></div>
<div class="faq-item"><p class="faq-q"><span class="faq-icon">?</span><span>Can I use the page without a mouse?</span></p><p class="faq-a"><span class="faq-icon">i</span><span>Yes. Use <span class="kbd">Tab</span> and <span class="kbd">Shift</span> + <span class="kbd">Tab</span> to move around. The full shortcut list is at the top of this window.</span></p></div>
</div>
`
},
dailyQuota: {
title: 'Daily guest quota',
html: `
<h3>Daily guest quota</h3>
<p>Guest limits are tracked per IP address. This demo uses placeholder numbers so the UI shape is ready for the real API response.</p>
<div class="quota-meter-list">
<div class="quota-meter">
<div class="quota-meter-head"><span>Uploads today</span><span>1.2 GB / 4 GB</span></div>
<div class="quota-meter-track"><span class="quota-meter-bar" style="width:30%"></span></div>
</div>
<div class="quota-meter">
<div class="quota-meter-head"><span>Downloads today</span><span>3.8 GB / 10 GB</span></div>
<div class="quota-meter-track"><span class="quota-meter-bar" style="width:38%"></span></div>
</div>
<div class="quota-meter">
<div class="quota-meter-head"><span>Boxes created</span><span>2 / 10</span></div>
<div class="quota-meter-track"><span class="quota-meter-bar" style="width:20%"></span></div>
</div>
</div>
<p class="quota-note"><strong>Tip:</strong> API keys can unlock larger quota for storage, upload size, automation, and scripted usage. Wire this window to your quota endpoint later.</p>
`
},
about: {
title: 'About WarpBox',
html: `
<h3>WarpBox v1.0.1</h3>
<p><strong>WarpBox</strong> was made by <strong>Daniel Legt</strong>.</p>
<p>This demo page shows the browser upload flow, terminal upload hints, API examples, and old-web inspired UI behavior.</p>
<div class="quota-dialog-summary">Temporary file boxes, terminal-first uploads, and tiny pixel drama.</div>
`
},
examples: {
title: 'Examples',
html: `
<h3>Upload examples</h3>
<h4>Basic CLI upload</h4>
<pre>curl \\
-F 'file=@./photo.png' \\
-F 'expires=24h' \\
https://warpbox.dev/u</pre>
<h4>Multiple files with password</h4>
<pre>curl \\
-F 'file=@./one.png' \\
-F 'file=@./two.zip' \\
-F 'expires=7d' \\
-F 'password=secret-pass' \\
https://warpbox.dev/u</pre>
<h4>CLI upload with API key</h4>
<pre>curl \\
-H 'Authorization: Bearer YOUR_API_KEY' \\
-F 'file=@./archive.tar.gz' \\
-F 'expires=30d' \\
https://warpbox.dev/u</pre>
<h4>Go</h4>
<pre>package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
file, err := os.Open("photo.png")
if err != nil { panic(err) }
defer file.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "photo.png")
if err != nil { panic(err) }
if _, err := io.Copy(part, file); err != nil { panic(err) }
_ = writer.WriteField("expires", "7d")
writer.Close()
req, err := http.NewRequest("POST", "https://warpbox.dev/u", &body)
if err != nil { panic(err) }
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
}</pre>
<h4>Java 11+ HttpClient</h4>
<pre>import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
public class UploadWarpBox {
public static void main(String[] args) throws Exception {
String boundary = "----WarpBoxBoundary" + System.currentTimeMillis();
Path file = Path.of("photo.png");
byte[] prefix = ("--" + boundary + "\\r\\n" +
"Content-Disposition: form-data; name=\\"expires\\"\\r\\n\\r\\n" +
"7d\\r\\n" +
"--" + boundary + "\\r\\n" +
"Content-Disposition: form-data; name=\\"file\\"; filename=\\"photo.png\\"\\r\\n" +
"Content-Type: application/octet-stream\\r\\n\\r\\n").getBytes();
byte[] suffix = ("\\r\\n--" + boundary + "--\\r\\n").getBytes();
byte[] fileBytes = Files.readAllBytes(file);
byte[] body = new byte[prefix.length + fileBytes.length + suffix.length];
System.arraycopy(prefix, 0, body, 0, prefix.length);
System.arraycopy(fileBytes, 0, body, prefix.length, fileBytes.length);
System.arraycopy(suffix, 0, body, prefix.length + fileBytes.length, suffix.length);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://warpbox.dev/u"))
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.header("Authorization", "Bearer YOUR_API_KEY")
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
.build();
HttpResponse&lt;String&gt; response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}</pre>
<h4>JavaScript Node.js</h4>
<pre>import { openAsBlob } from 'node:fs';
const file = await openAsBlob('./photo.png');
const form = new FormData();
form.append('file', file, 'photo.png');
form.append('expires', '7d');
const res = await fetch('https://warpbox.dev/u', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY'
},
body: form
});
console.log(await res.text());</pre>
`
}
};
function openDoc(name) {
const doc = docs[name];
if (!doc) return;
el.docPopupTitle.textContent = doc.title;
el.docPopupBody.innerHTML = doc.html;
el.docPopup.classList.add('is-visible');
if (el.modalBackdrop) el.modalBackdrop.classList.add('is-visible');
setStatus(`${doc.title} opened`);
}
function closeDoc() {
el.docPopup.classList.remove('is-visible');
el.docPopup.classList.remove('is-about-popup');
if (el.modalBackdrop) el.modalBackdrop.classList.remove('is-visible');
}
function addFiles(fileList) {
if (uploadLocked) { showToast('This box is sealed. Clear it to create a fresh upload.', 'warning'); return; }
const incoming = Array.from(fileList).map(file => ({ name: file.name, size: file.size || 1, progress: 0, uploaded: false }));
if (!incoming.length) return;
const { unique, duplicates } = duplicateFileReport(incoming);
if (unique.length) {
files.push(...unique);
shareUrl = '';
setShareUrl('');
renderFiles();
const warning = quotaWarningMessage(unique);
if (warning) showWarningDialog('Quota warning', warning);
}
if (duplicates.length) showDuplicateDialog(duplicates);
const added = unique.length;
const skipped = duplicates.length;
if (added) setStatus(`${added} file${added === 1 ? '' : 's'} added to queue${skipped ? `, ${skipped} duplicate${skipped === 1 ? '' : 's'} pending` : ''}`);
else setStatus(`${skipped} duplicate file${skipped === 1 ? '' : 's'} need your choice`);
}
function loadDemoFiles() {
if (files.length) return;
files = demoFiles.map(file => ({ ...file }));
renderFiles();
setStatus('Demo files loaded. Press Start upload to simulate progress.');
}
function removeFile(index) {
if (uploadLocked) { showToast('Box already created. Clear it before editing the queue.', 'warning'); return; }
files.splice(index, 1);
shareUrl = '';
setShareUrl('');
renderFiles();
setStatus('File removed from queue');
}
function clearQueue() {
files = [];
shareUrl = '';
uploadLocked = false;
setBoxOptionsLocked(false);
impactedFiles = new Set();
overallImpactDone = false;
setShareUrl('');
clearInterval(uploadTimer);
uploadTimer = null;
if (el.startButton) el.startButton.disabled = false;
if (el.fileInput) el.fileInput.disabled = false;
setDisabledReasons();
if (el.dropzone) el.dropzone.classList.remove('is-locked');
renderFiles();
setStatus('Queue cleared');
showToast('Queue cleared. The void is tidy again.');
}
function confirmClearQueue() {
if (!files.length && !shareUrl) {
showToast('Nothing to clear. Impressive restraint.');
return;
}
el.docPopupTitle.textContent = 'Clear WarpBox?';
el.docPopupBody.innerHTML = `
<h3>Confirm clear</h3>
<p>This removes the current queue, resets progress, and unlocks the Start upload button.</p>
<p>Uploaded files in this demo are not real, but the button still wants consent.</p>
<div class="copy-fallback-actions">
<button class="win98-button" type="button" id="confirm-clear-yes">Clear</button>
<button class="win98-button" type="button" id="confirm-clear-no">Cancel</button>
</div>`;
el.modalBackdrop.classList.add('is-visible');
el.docPopup.classList.add('is-visible');
setTimeout(() => document.getElementById('confirm-clear-no')?.focus(), 0);
}
function startUpload() {
if (hasQuotaError()) {
showWarningDialog('Over maximum upload size!', quotaWarningMessage() || 'Over maximum upload size!');
showToast('Over maximum upload size!', 'error');
return;
}
if (uploadLocked) {
showToast('Upload already started. Press Clear to create another box.', 'warning');
return;
}
if (!files.length) {
showWarningDialog('No files selected', 'There are no files selected. Please select files to upload.');
showToast('No files selected. Please select files to upload.', 'warning');
setStatus('No files selected');
return;
}
if (hasQuotaError()) {
showWarningDialog('Over maximum upload size!', quotaWarningMessage() || 'Over maximum upload size!');
showToast('Over maximum upload size!', 'error');
return;
}
uploadLocked = true;
setBoxOptionsLocked(true);
if (el.startButton) el.startButton.disabled = true;
if (el.fileInput) el.fileInput.disabled = true;
setDisabledReasons();
if (el.dropzone) el.dropzone.classList.add('is-locked');
clearInterval(uploadTimer);
setStatus('Uploading...');
uploadTimer = setInterval(() => {
let changed = false;
files = files.map(file => {
if (file.progress >= 100) return file;
changed = true;
const step = Math.max(3, Math.round(90000000 / Math.max(file.size, 1) * 100));
return { ...file, progress: Math.min(100, file.progress + step + Math.random() * 9) };
});
renderFiles();
if (!changed || files.every(file => file.progress >= 100)) {
clearInterval(uploadTimer);
uploadTimer = null;
files = files.map(file => ({ ...file, progress: 100, uploaded: true }));
renderFiles();
setStatus('Upload complete. Share URL created. Press Clear to start another upload.');
showToast('Upload complete. Share URL created.');
}
}, 450);
}
function createShareLink() {
if (shareUrl || !files.length) return;
const token = Math.random().toString(36).slice(2, 7) + Math.random().toString(36).slice(2, 4).toUpperCase();
setShareUrl(`https://warpbox.dev/b/${token}`);
}
function setShareUrl(url) {
shareUrl = url;
if (!url) {
el.shareLink.textContent = 'Not created yet';
el.shareLink.href = '#';
el.shareLink.classList.add('is-empty');
el.copyButton.disabled = true;
el.copyButton.dataset.disabledReason = 'There is no share URL yet. Start an upload first.';
} else {
el.shareLink.textContent = url;
el.shareLink.href = url;
el.shareLink.classList.remove('is-empty');
el.copyButton.disabled = false;
el.copyButton.dataset.disabledReason = '';
}
updateTerminal();
}
async function copyLink() {
if (!shareUrl) {
showToast('No share URL yet. Start an upload first. The box contains vibes, not files.', 'warning');
return;
}
try {
await navigator.clipboard.writeText(shareUrl);
showToast('Share URL copied to clipboard.');
setStatus('Copied share URL');
} catch (_) {
showCopyFallback('Share URL', shareUrl, shareUrl);
}
}
function randomPassword() {
el.password.value = Math.random().toString(36).slice(2, 8) + '-' + Math.random().toString(36).slice(2, 6);
setStatus('Generated a demo password');
updateTerminal();
}
const funnyAdjectives = ['haunted', 'turbo', 'sleepy', 'chunky', 'wizard', 'neon', 'forbidden', 'crunchy', 'cosmic', 'feral'];
const funnyNouns = ['floppy', 'box', 'hamster', 'archive', 'goblin', 'packet', 'zip', 'portal', 'folder', 'upload'];
function slugify(value) {
return String(value || '')
.toLowerCase()
.replace(/[^a-z0-9-]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.slice(0, 32);
}
function syncSlugFromName(force = false) {
if (!el.customSlug || !el.boxName) return;
if (force || !el.customSlug.value || el.customSlug.dataset.auto === 'true') {
el.customSlug.value = slugify(el.boxName.value);
el.customSlug.dataset.auto = 'true';
}
updateTerminal();
}
function generateFunnyBoxName() {
const adj = funnyAdjectives[Math.floor(Math.random() * funnyAdjectives.length)];
const noun = funnyNouns[Math.floor(Math.random() * funnyNouns.length)];
const number = Math.floor(100 + Math.random() * 900);
const titleAdj = adj.charAt(0).toUpperCase() + adj.slice(1);
const titleNoun = noun.charAt(0).toUpperCase() + noun.slice(1);
el.boxName.value = `${titleAdj} ${titleNoun} ${number}`;
syncSlugFromName(true);
setStatus('Hamsters writing random names for you');
showToast('Hamsters writing random names for you', 'info');
}
function openMenu(button) {
document.querySelectorAll('.menu-item').forEach(item => {
const isTarget = item.contains(button);
item.classList.toggle('is-open', isTarget && !item.classList.contains('is-open'));
const btn = item.querySelector('.menu-button');
if (btn) btn.setAttribute('aria-expanded', item.classList.contains('is-open') ? 'true' : 'false');
});
}
function closeMenus() {
document.querySelectorAll('.menu-item').forEach(item => item.classList.remove('is-open'));
document.querySelectorAll('.menu-button').forEach(button => button.setAttribute('aria-expanded', 'false'));
if (el.startWrap) el.startWrap.classList.remove('is-open');
}
function runAction(action) {
switch (action) {
case 'browse': uploadLocked ? showToast('Box already created. Clear it to add files to a new box.') : el.fileInput.click(); break;
case 'start-upload': startUpload(); break;
case 'copy-link': copyLink(); break;
case 'copy-curl': copyCurlCommand(); break;
case 'clear': confirmClearQueue(); break;
case 'toggle-delete-once': el.deleteOnce.checked = !el.deleteOnce.checked; setStatus(`Delete after first download ${el.deleteOnce.checked ? 'enabled' : 'disabled'}`); break;
case 'toggle-page': el.downloadPage.checked = !el.downloadPage.checked; setStatus(`Download page ${el.downloadPage.checked ? 'enabled' : 'disabled'}`); break;
case 'random-password': randomPassword(); break;
case 'random-box-name': generateFunnyBoxName(); break;
case 'clear-password': el.password.value = ''; setStatus('Password cleared'); updateTerminal(); break;
case 'help': showToast('Drop files, configure Box Options, then press Start upload. Menus are clickable.'); setStatus('Help opened'); break;
case 'terminal-help': openDoc('cli'); break;
case 'minimize': showToast('Minimize? Nice try. This box has abandonment issues.'); break;
case 'toggle-fit': document.body.classList.toggle('fit-window'); showToast('Maximize requested. The pixel rectangle feels important now.'); break;
case 'fake-close': showToast('Close button denied. The box is emotionally attached to your files.'); break;
case 'side-close': showToast('Box Options refuses to leave. It pays rent in checkboxes.'); break;
case 'side-help': showToast('Terminal help? Copy the command. Feed it files. Try not to anger bash.'); break;
case 'side-folder-close': showToast('The folder window saw that click and chose denial.'); break;
case 'coming-soon': showToast('Coming Soon, not implemented just yet.'); break;
}
}
el.fileInput.addEventListener('change', event => addFiles(event.target.files));
el.form.addEventListener('submit', event => { event.preventDefault(); startUpload(); });
el.copyButton.addEventListener('click', copyLink);
if (el.copyCurlButton) el.copyCurlButton.addEventListener('click', copyCurlCommand);
if (el.docPopupClose) el.docPopupClose.addEventListener('click', closeDoc);
if (el.modalBackdrop) el.modalBackdrop.addEventListener('click', closeDoc);
if (el.docPopupBody) el.docPopupBody.addEventListener('click', event => {
if (event.target.closest('#confirm-clear-yes')) { clearQueue(); closeDoc(); }
if (event.target.closest('#confirm-clear-no')) closeDoc();
if (event.target.closest('#duplicate-append')) { appendPendingDuplicates(); closeDoc(); }
if (event.target.closest('#duplicate-skip')) { pendingDuplicateFiles = []; showToast('Duplicate files skipped.', 'info'); closeDoc(); }
});
el.fileList.addEventListener('click', event => {
const remove = event.target.closest('[data-remove]');
if (remove) removeFile(Number(remove.dataset.remove));
});
['dragenter', 'dragover'].forEach(name => {
el.dropSurface.addEventListener(name, event => {
event.preventDefault();
el.dropzone.classList.add('is-dragging');
setStatus('Drop files to add them to the queue');
});
});
['dragleave', 'drop'].forEach(name => {
el.dropSurface.addEventListener(name, event => {
event.preventDefault();
el.dropzone.classList.remove('is-dragging');
});
});
el.dropSurface.addEventListener('drop', event => addFiles(event.dataTransfer.files));
document.querySelectorAll('.menu-item').forEach(item => {
item.addEventListener('mouseenter', () => {
const anyOpen = document.querySelector('.menu-item.is-open');
if (!anyOpen || anyOpen === item) return;
document.querySelectorAll('.menu-item').forEach(other => {
const open = other === item;
other.classList.toggle('is-open', open);
const btn = other.querySelector('.menu-button');
if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
});
});
});
document.addEventListener('pointerdown', event => {
const disabledTarget = event.target.closest('button:disabled, input:disabled, select:disabled, textarea:disabled, input[readonly], textarea[readonly]');
if (!disabledTarget) return;
let reason = disabledTarget.dataset.disabledReason || disabledTarget.title || 'This control is disabled right now.';
if (disabledTarget.classList && disabledTarget.classList.contains('upload-file-remove')) reason = 'This file cannot be removed because the upload box is already created. Press Clear to start a separate box.';
showToast(reason, 'warning');
if (disabledTarget.id === 'start-button' && hasQuotaError()) {
showWarningDialog('Over maximum upload size!', quotaWarningMessage() || reason);
}
}, true);
document.addEventListener('click', event => {
const menuButton = event.target.closest('.menu-button');
const actionTarget = event.target.closest('[data-action]');
const expiryTarget = event.target.closest('[data-expiry]');
const docTarget = event.target.closest('[data-doc]');
if (docTarget) {
openDoc(docTarget.dataset.doc);
closeMenus();
return;
}
if (menuButton) {
event.stopPropagation();
openMenu(menuButton);
return;
}
if (expiryTarget) {
el.expiry.value = expiryTarget.dataset.expiry;
setStatus(`Expiry changed to ${el.expiry.options[el.expiry.selectedIndex].text}`);
updateTerminal();
closeMenus();
return;
}
if (actionTarget) {
event.preventDefault();
runAction(actionTarget.dataset.action);
closeMenus();
return;
}
if (!event.target.closest('.menu-popup')) closeMenus();
});
if (el.startMenuButton && el.startWrap) {
el.startMenuButton.addEventListener('click', event => {
event.stopPropagation();
document.querySelectorAll('.menu-item').forEach(item => item.classList.remove('is-open'));
el.startWrap.classList.toggle('is-open');
});
}
[el.expiry, el.password, el.maxViews, el.boxName, el.customSlug, el.downloadPage, el.deleteOnce, el.allowPreview, el.keepFilenames, el.privateBox, el.apiKeyMode, el.apiKeyInput].filter(Boolean).forEach(input => {
input.addEventListener('input', () => { setStatus('Box options updated'); updateTerminal(); });
input.addEventListener('change', () => { setStatus('Box options updated'); updateTerminal(); });
});
if (el.randomBoxName) el.randomBoxName.addEventListener('click', generateFunnyBoxName);
if (el.boxName) {
el.boxName.addEventListener('input', () => syncSlugFromName(false));
}
if (el.customSlug) {
el.customSlug.addEventListener('input', () => {
const clean = slugify(el.customSlug.value);
if (el.customSlug.value !== clean) el.customSlug.value = clean;
el.customSlug.dataset.auto = 'false';
updateTerminal();
});
}
if (el.apiKeyRow) {
el.apiKeyRow.addEventListener('click', event => {
if (el.apiKeyInput && el.apiKeyInput.disabled) {
showToast(uploadLocked ? 'Box Options are locked because this box was already created. Press Clear to start another upload.' : 'Enable "Use API key for larger quota" before typing an API key.', 'warning');
}
});
}
function syncApiKeyField() {
if (uploadLocked) return;
const enabled = !!(el.apiKeyMode && el.apiKeyMode.checked);
if (el.apiKeyRow) el.apiKeyRow.classList.toggle('is-visible', enabled);
if (el.apiKeyInput) {
el.apiKeyInput.disabled = !enabled;
if (!enabled) el.apiKeyInput.value = '';
}
updateTerminal();
}
if (el.apiKeyMode) el.apiKeyMode.addEventListener('change', syncApiKeyField);
if (el.maxViews) {
el.maxViews.addEventListener('wheel', event => {
if (document.activeElement !== el.maxViews && !el.maxViews.matches(':hover')) return;
event.preventDefault();
const current = Number(el.maxViews.value || 0);
let step = 1;
if (event.shiftKey) step *= 10;
if (event.ctrlKey || event.metaKey) step *= 100;
const direction = event.deltaY < 0 ? 1 : -1;
el.maxViews.value = Math.max(1, Math.min(9999, current + direction * step));
setStatus(`Max views changed to ${el.maxViews.value}`);
updateTerminal();
}, { passive: false });
}
document.addEventListener('keydown', event => {
if (event.key === 'Escape') { closeMenus(); closeDoc(); }
if (event.key === 'F1') { event.preventDefault(); openDoc('faq'); }
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'u') { event.preventDefault(); startUpload(); }
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') { event.preventDefault(); copyCurlCommand(); }
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'l') { event.preventDefault(); copyLink(); }
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'o') { event.preventDefault(); el.fileInput.click(); }
if (event.shiftKey && !event.ctrlKey && !event.metaKey && ['1','2','3','4'].includes(event.key)) {
event.preventDefault();
const focusTargets = {
'1': el.fileList,
'2': document.querySelector('.side-panel .side-body'),
'3': el.terminal,
'4': document.querySelector('.helper-body')
};
const target = focusTargets[event.key];
if (target) { target.setAttribute('tabindex', target.getAttribute('tabindex') || '0'); target.focus(); }
}
if (event.key === 'Enter' && document.activeElement === document.body) startUpload();
});
function updateClock() {
if (!el.clock) return;
const now = new Date();
el.clock.textContent = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
syncApiKeyField();
setBoxOptionsLocked(false);
updateClock();
if (el.clock) setInterval(updateClock, 1000);
renderFiles();
</script>
</body>
</html>