components
All repositories: gitoria
5.0 KB
// file-upload.hl — THE FILE UPLOAD (one file), pure Hybriel: a drop zone with a "Choose a file" button. The chosen or dropped file goes// to the app through `emit server saveFile(file)` (the framework carries it over HTTP, in parts, with progress: hybriel#94); the app's// `on server saveFile(file)` decides where it is stored and answers anything but null when it took it (a face that raises an error or// answers null = the upload failed). Name, size and progress show while it goes; a finished file is listed under "Uploaded".// One file at a time: a drop of several takes the first, a file dropped during an upload is ignored. USE ONE PER PAGE (a composed// component's members are the page's state). The page is written by DOM calls in the handlers: the emit blocks its handler, and// `on client uploadProgress` paints the bar meanwhile.String dropText = 'Drop a file here or'String chooseText = 'Choose a file'String uploadedTitle = 'Uploaded'sizeText(n) {if (n < 1024) { return n + ' B' }if (n < 1048576) { return (n / 1024).toFixed(1) + ' KB' }return (n / 1048576).toFixed(1) + ' MB'}View {fileUpload {dropZone { on dragover(e) { emit uploadOver(e) } on dragleave(e) { emit uploadLeave(e) } on drop(e) { emit uploadDrop(e) }p { dropText }label { class = "choose" chooseText input { type = "file" on change(e) { emit uploadChosen(e) } } }uploadStatus { 'aria-live' = "polite"strong { class = "name" }span { class = "size" }progress { max = "100" value = "0" }output { class = "state" }}}uploadedList {h2 { uploadedTitle }ul { class = "uploaded" }}}}rootOf(node) { return node.closest('file-upload') }on uploadOver(e) {e.preventDefault()rootOf(e.target).classList.add('over')}on uploadLeave(e) { rootOf(e.target).classList.remove('over') }on uploadDrop(e) {e.preventDefault()let root = rootOf(e.target)root.classList.remove('over')if (e.dataTransfer.files.length > 0) { send(root, e.dataTransfer.files[0]) }}on uploadChosen(e) {let root = rootOf(e.target)let input = e.targetif (input.files.length > 0) { send(root, input.files[0]) }input.value = ''}on client uploadProgress(p) {for (root of document.querySelectorAll('file-upload.busy')) {let bar = root.querySelector('progress')bar.max = p.size > 0 ? p.size : 1bar.value = p.sent}}send(root, file) {if (root.classList.contains('busy')) { return null }root.classList.add('busy')root.classList.remove('done')root.classList.remove('failed')root.classList.add('shown')root.querySelector('.name').textContent = file.nameroot.querySelector('.size').textContent = sizeText(file.size)let bar = root.querySelector('progress')bar.max = 100bar.value = 0let state = root.querySelector('.state')state.textContent = 'Uploading…'let saved = emit server saveFile(file)root.classList.remove('busy')if (saved == null) {root.classList.add('failed')state.textContent = 'Failed'return null}bar.max = 100bar.value = 100root.classList.add('done')state.textContent = 'Uploaded'let li = document.createElement('li')li.textContent = file.name + ' — ' + sizeText(file.size)root.querySelector('ul.uploaded').appendChild(li)}// ---- look: the component's own Style; colours are the semantic tokens with the palette value as fallbackStyle {'file-upload' { display = 'block' color = 'var(--color-text, rgb(195, 200, 205))' }'file-upload drop-zone' { display = 'block' padding = '1rem' textAlign = 'center' border = '2px dashed var(--color-border, rgb(70, 75, 80))' borderRadius = '.5rem' }'file-upload.over drop-zone' { borderColor = 'var(--color-accent, #569bd4)' background = 'var(--color-surface, rgb(35, 40, 45))' }'file-upload drop-zone p' { margin = '0 0 .5rem' color = 'var(--color-text-muted, rgb(145, 150, 155))' }'file-upload label.choose' { display = 'inline-block' padding = '.35rem .8rem' cursor = 'pointer' border = '1px solid var(--color-border, rgb(70, 75, 80))' borderRadius = '.35rem' background = 'var(--color-surface, rgb(35, 40, 45))' }'file-upload label.choose:hover' { borderColor = 'var(--color-accent, #569bd4)' }'file-upload label.choose input' { display = 'none' }'file-upload upload-status' { display = 'none' marginTop = '.8rem' textAlign = 'left' }'file-upload.shown upload-status' { display = 'block' }'file-upload upload-status .name' { overflowWrap = 'anywhere' }'file-upload upload-status .size' { marginLeft = '.6rem' color = 'var(--color-text-muted, rgb(145, 150, 155))' }'file-upload progress' { display = 'block' width = '100%' margin = '.4rem 0' accentColor = 'var(--color-accent, #569bd4)' }'file-upload.done .state' { color = 'var(--color-success, #6a9955)' }'file-upload.failed .state' { color = 'var(--color-danger, #f44747)' }'file-upload upload-status .state' { fontSize = '.9rem' }'file-upload uploaded-list h2' { margin = '1rem 0 .3rem' fontSize = '1rem' }'file-upload ul.uploaded' { margin = '0' paddingLeft = '1.2rem' }'file-upload ul.uploaded:empty::after' { content = '"Nothing yet."' marginLeft = '-1.2rem' color = 'var(--color-text-muted, rgb(145, 150, 155))' }}
Branches
- mainmain branch
Latest commits
- d32b44e2antcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 0fb80b57components: Hybriel master ff51cf46 (re-vendor round)mre
- 57408b6ecomponents#14: installable app (manifest, service worker, offline index), own iconmre
- 317bd09fdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- becbd59bcomponents#13: modal dialog (<dialog> based, focus wrap, backdrop/Esc/x close with reason, scroll lock, footer buttons)mre
- 367dd19adeploy.sh: never send .git or .gitignore to Byrodinmre
- 4b4dabe5State of 2026-09-27, before the move to gitoriamre