gitoriaLog in with ident

components

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Branchmain3bc905c5components #15 (mission 003) 2/2: docs (README Look, STATUS, LOG), reportmremain/components/file-upload/file-upload.hl

5.1 KB

  1. // 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
  2. // to the app through `emit server saveFile(file)` (the framework carries it over HTTP, in parts, with progress: hybriel#94); the app's
  3. // `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
  4. // answers null = the upload failed). Name, size and progress show while it goes; a finished file is listed under "Uploaded".
  5. // 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
  6. // component's members are the page's state). The page is written by DOM calls in the handlers: the emit blocks its handler, and
  7. // `on client uploadProgress` paints the bar meanwhile.
  8. String dropText = 'Drop a file here or'
  9. String chooseText = 'Choose a file'
  10. String uploadedTitle = 'Uploaded'
  11. sizeText(n) {
  12. if (n < 1024) { return n + ' B' }
  13. if (n < 1048576) { return (n / 1024).toFixed(1) + ' KB' }
  14. return (n / 1048576).toFixed(1) + ' MB'
  15. }
  16. View {
  17. fileUpload {
  18. dropZone { on dragover(e) { emit uploadOver(e) } on dragleave(e) { emit uploadLeave(e) } on drop(e) { emit uploadDrop(e) }
  19. p { dropText }
  20. label { class = "choose" chooseText input { type = "file" on change(e) { emit uploadChosen(e) } } }
  21. uploadStatus { 'aria-live' = "polite"
  22. strong { class = "name" }
  23. span { class = "size" }
  24. progress { max = "100" value = "0" }
  25. output { class = "state" }
  26. }
  27. }
  28. uploadedList {
  29. h2 { uploadedTitle }
  30. ul { class = "uploaded" }
  31. }
  32. }
  33. }
  34. rootOf(node) { return node.closest('file-upload') }
  35. on uploadOver(e) {
  36. e.preventDefault()
  37. rootOf(e.target).classList.add('over')
  38. }
  39. on uploadLeave(e) { rootOf(e.target).classList.remove('over') }
  40. on uploadDrop(e) {
  41. e.preventDefault()
  42. root = rootOf(e.target)
  43. root.classList.remove('over')
  44. if (e.dataTransfer.files.length > 0) { send(root, e.dataTransfer.files[0]) }
  45. }
  46. on uploadChosen(e) {
  47. root = rootOf(e.target)
  48. input = e.target
  49. if (input.files.length > 0) { send(root, input.files[0]) }
  50. input.value = ''
  51. }
  52. on client uploadProgress(p) {
  53. for (root of document.querySelectorAll('file-upload.busy')) {
  54. let bar = root.querySelector('progress')
  55. bar.max = p.size > 0 ? p.size : 1
  56. bar.value = p.sent
  57. }
  58. }
  59. send(root, file) {
  60. if (root.classList.contains('busy')) { return null }
  61. root.classList.add('busy')
  62. root.classList.remove('done')
  63. root.classList.remove('failed')
  64. root.classList.add('shown')
  65. root.querySelector('.name').textContent = file.name
  66. root.querySelector('.size').textContent = sizeText(file.size)
  67. bar = root.querySelector('progress')
  68. bar.max = 100
  69. bar.value = 0
  70. state = root.querySelector('.state')
  71. state.textContent = 'Uploading…'
  72. saved = emit server saveFile(file)
  73. root.classList.remove('busy')
  74. if (saved == null) {
  75. root.classList.add('failed')
  76. state.textContent = 'Failed'
  77. return null
  78. }
  79. bar.max = 100
  80. bar.value = 100
  81. root.classList.add('done')
  82. state.textContent = 'Uploaded'
  83. li = document.createElement('li')
  84. li.textContent = file.name + ' — ' + sizeText(file.size)
  85. root.querySelector('ul.uploaded').appendChild(li)
  86. }
  87. // ---- look: the component's own Style; the base look is black, white and the greys #555 #999 #ddd #f2f2f2 (shared/base.hl names them;
  88. // 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
  89. // theme) restyles it. No frames: the one border is the drop zone's dashed one (it means "drop here")
  90. Style {
  91. 'file-upload' { display = 'block' color = 'var(--color-text, #000)' }
  92. 'file-upload drop-zone' { display = 'block' padding = '1rem' textAlign = 'center' border = '2px dashed var(--color-border, #999)' borderRadius = '.5rem' }
  93. 'file-upload.over drop-zone' { borderColor = 'var(--color-accent, #000)' background = 'var(--color-surface, #f2f2f2)' }
  94. 'file-upload drop-zone p' { margin = '0 0 .5rem' color = 'var(--color-text-muted, #555)' }
  95. 'file-upload label.choose' { display = 'inline-block' padding = '.35rem .8rem' cursor = 'pointer' border = '0' borderRadius = '.35rem' background = 'var(--color-surface, #f2f2f2)' }
  96. 'file-upload label.choose:hover' { filter = 'brightness(.93)' }
  97. 'file-upload label.choose input' { display = 'none' }
  98. 'file-upload upload-status' { display = 'none' marginTop = '.8rem' textAlign = 'left' }
  99. 'file-upload.shown upload-status' { display = 'block' }
  100. 'file-upload upload-status .name' { overflowWrap = 'anywhere' }
  101. 'file-upload upload-status .size' { marginLeft = '.6rem' color = 'var(--color-text-muted, #555)' }
  102. 'file-upload progress' { display = 'block' width = '100%' margin = '.4rem 0' accentColor = 'var(--color-accent, #000)' }
  103. 'file-upload.done .state' { color = 'var(--color-success, #000)' }
  104. 'file-upload.failed .state' { color = 'var(--color-danger, #000)' }
  105. 'file-upload upload-status .state' { fontSize = '.9rem' }
  106. 'file-upload uploaded-list h2' { margin = '1rem 0 .3rem' fontSize = '1rem' }
  107. 'file-upload ul.uploaded' { margin = '0' paddingLeft = '1.2rem' }
  108. 'file-upload ul.uploaded:empty::after' { content = '"Nothing yet."' marginLeft = '-1.2rem' color = 'var(--color-text-muted, #555)' }
  109. }

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