components
All repositories: gitoria
5.1 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()root = rootOf(e.target)root.classList.remove('over')if (e.dataTransfer.files.length > 0) { send(root, e.dataTransfer.files[0]) }}on uploadChosen(e) {root = rootOf(e.target)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)bar = root.querySelector('progress')bar.max = 100bar.value = 0state = root.querySelector('.state')state.textContent = 'Uploading…'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'li = document.createElement('li')li.textContent = file.name + ' — ' + sizeText(file.size)root.querySelector('ul.uploaded').appendChild(li)}// ---- look: the component's own Style; the base look is black, white and the greys #555 #999 #ddd #f2f2f2 (shared/base.hl names them;// README "Look"); every colour is a semantic token with its base value as fallback, so an app that declares the tokens (layouts.worldapi.org's// theme) restyles it. No frames: the one border is the drop zone's dashed one (it means "drop here")Style {'file-upload' { display = 'block' color = 'var(--color-text, #000)' }'file-upload drop-zone' { display = 'block' padding = '1rem' textAlign = 'center' border = '2px dashed var(--color-border, #999)' borderRadius = '.5rem' }'file-upload.over drop-zone' { borderColor = 'var(--color-accent, #000)' background = 'var(--color-surface, #f2f2f2)' }'file-upload drop-zone p' { margin = '0 0 .5rem' color = 'var(--color-text-muted, #555)' }'file-upload label.choose' { display = 'inline-block' padding = '.35rem .8rem' cursor = 'pointer' border = '0' borderRadius = '.35rem' background = 'var(--color-surface, #f2f2f2)' }'file-upload label.choose:hover' { filter = 'brightness(.93)' }'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, #555)' }'file-upload progress' { display = 'block' width = '100%' margin = '.4rem 0' accentColor = 'var(--color-accent, #000)' }'file-upload.done .state' { color = 'var(--color-success, #000)' }'file-upload.failed .state' { color = 'var(--color-danger, #000)' }'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, #555)' }}
Branches
- mainmain branch
Latest commits
- 3bc905c5components #15 (mission 003) 2/2: docs (README Look, STATUS, LOG), reportmre
- 5a98d77ecomponents #15 (mission 003) 1/2: black/white base look — greys for nuances, no WorldAPI fallbacks, no decorative frames; site + icon black/white; gate 230/0mre
- 3f93d716components code order (mission 002) 4/4: docs, tests/letcount.py, tests/same-output*, reportmre
- e747cfa0components code order (mission 002) 3/4: let only where reassigned (257 dropped, 0 never-reassigned left); gate 220/0, layouts gate 73/0, same outputmre
- 172914a3components code order (mission 002) 2/4: lib/uploads.hl (demo upload storage), thin demo handlers, project.hl map; gate 220/0, same outputmre
- d29f35b1components code order (mission 002) 1/4: styles.hl -> components/styles.hl; gate 220/0, same outputmre
- 09945750components: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, http1 773de63e); gate 220/0mre
- ede4d19acomponents: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gate 220/0mre
- 2bb9a935components: Hybriel master 8efba065 (re-vendor round 069: #126 memory, #48 lambda copy)mre
- e4f01768antcolony#40: mission references point to the moved missionsmre
- 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