- 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.
2450 lines
117 KiB
HTML
2450 lines
117 KiB
HTML
<!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>></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 > 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">></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 & 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 > 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('"', '"')}">${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('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''');
|
||
}
|
||
|
||
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<String> 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>
|