@jseeio/jsee 0.4.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +21 -0
  3. package/README.md +583 -55
  4. package/dist/2b3e1faf89f94a483539.png +0 -0
  5. package/dist/416d91365b44e4b4f477.png +0 -0
  6. package/dist/8f2c4d11474275fbc161.png +0 -0
  7. package/dist/jsee.core.js +1 -0
  8. package/dist/jsee.full.js +1 -0
  9. package/dist/jsee.runtime.js +2 -1
  10. package/package.json +84 -18
  11. package/src/app.js +127 -32
  12. package/src/browser-bundle-node.js +9 -0
  13. package/src/cli.js +479 -67
  14. package/src/extended-imports.js +11 -0
  15. package/src/main.js +232 -44
  16. package/src/overlay.js +26 -1
  17. package/src/utils.js +386 -16
  18. package/templates/common-inputs.js +88 -0
  19. package/templates/common-outputs.js +340 -4
  20. package/templates/minimal-app.vue +367 -0
  21. package/templates/minimal-input.vue +573 -0
  22. package/templates/minimal-output.vue +426 -0
  23. package/templates/virtual-table.vue +194 -0
  24. package/.claude/settings.local.json +0 -15
  25. package/.eslintrc.js +0 -38
  26. package/AGENTS.md +0 -65
  27. package/CLAUDE.md +0 -5
  28. package/CNAME +0 -1
  29. package/_config.yml +0 -26
  30. package/dist/jsee.js +0 -1
  31. package/dump.sh +0 -23
  32. package/jest-puppeteer.config.js +0 -14
  33. package/jest.config.js +0 -8
  34. package/jest.unit.config.js +0 -8
  35. package/jsee.dump.txt +0 -5459
  36. package/load/index.html +0 -52
  37. package/templates/bulma-app.vue +0 -242
  38. package/templates/bulma-input.vue +0 -125
  39. package/templates/bulma-output.vue +0 -101
  40. package/test/arrow-main.html +0 -18
  41. package/test/arrow-worker.html +0 -18
  42. package/test/class.html +0 -22
  43. package/test/code.html +0 -25
  44. package/test/codew.html +0 -25
  45. package/test/example-class.js +0 -8
  46. package/test/example-sum.js +0 -3
  47. package/test/fixtures/lodash-like.js +0 -15
  48. package/test/fixtures/upload-sample.csv +0 -3
  49. package/test/importw.html +0 -28
  50. package/test/minimal.html +0 -14
  51. package/test/minimal1.html +0 -13
  52. package/test/minimal2.html +0 -15
  53. package/test/minimal3.html +0 -10
  54. package/test/minimal4.html +0 -22
  55. package/test/pipeline.html +0 -52
  56. package/test/python.html +0 -41
  57. package/test/runtime-arrow.html +0 -18
  58. package/test/string.html +0 -26
  59. package/test/stringw.html +0 -29
  60. package/test/sum.schema.json +0 -17
  61. package/test/sumw.schema.json +0 -15
  62. package/test/test-basic.test.js +0 -630
  63. package/test/test-python.test.js +0 -23
  64. package/test/unit/cli-fetch.test.js +0 -229
  65. package/test/unit/utils.test.js +0 -908
  66. package/webpack.config.js +0 -101
@@ -0,0 +1,11 @@
1
+ // Extended bundle imports — only loaded when EXTENDED flag is true.
2
+ // These libraries are heavy and not included in the core bundle.
3
+ // Users on the core bundle can load them manually via schema `imports`
4
+ // and they'll be detected via window globals.
5
+
6
+ window.Plot = require('@observablehq/plot')
7
+ window.THREE = require('three')
8
+ window.L = require('leaflet')
9
+
10
+ // Leaflet CSS
11
+ require('leaflet/dist/leaflet.css')
package/src/main.js CHANGED
@@ -25,6 +25,10 @@ const Overlay = require('./overlay')
25
25
 
26
26
  require('notyf/notyf.min.css')
27
27
 
28
+ if (typeof EXTENDED !== 'undefined' && EXTENDED) {
29
+ require('./extended-imports')
30
+ }
31
+
28
32
  const fetch = window['fetch']
29
33
  const Blob = window['Blob']
30
34
 
@@ -124,10 +128,6 @@ function collectStreamInputConfig (inputs, config={}) {
124
128
  return config
125
129
  }
126
130
 
127
- function getFunctionContainer (target) {
128
- // Check if the number of parameters is > 1, then 'args'
129
- }
130
-
131
131
  export default class JSEE {
132
132
  constructor (params, alt1, alt2) {
133
133
  // Check if JSEE was initialized with args rather than with a params object
@@ -152,7 +152,12 @@ export default class JSEE {
152
152
  this.__version__ = VERSION
153
153
  this.cancelled = false
154
154
  this._cancelWorkerRun = null
155
+ this._rejectRun = null
156
+ this._rejectWorkerRun = null
155
157
  this._workers = []
158
+ this._pendingRun = null
159
+ this._runToken = null
160
+ this._needsModelReinit = false
156
161
 
157
162
  // Check if schema is provided
158
163
  if (typeof this.schema === 'undefined') {
@@ -185,10 +190,31 @@ export default class JSEE {
185
190
 
186
191
  cancelCurrentRun () {
187
192
  log('Stopping current run')
193
+ const cancelError = new Error('Cancelled')
188
194
  this.cancelled = true
195
+ this._pendingRun = null
196
+ this._runToken = {}
197
+ if (typeof this._rejectRun === 'function') {
198
+ this._rejectRun(cancelError)
199
+ this._rejectRun = null
200
+ }
189
201
  if (typeof this._cancelWorkerRun === 'function') {
190
202
  this._cancelWorkerRun()
191
203
  }
204
+ // Reject the in-flight worker promise so run() stops awaiting
205
+ if (typeof this._rejectWorkerRun === 'function') {
206
+ this._rejectWorkerRun(cancelError)
207
+ this._rejectWorkerRun = null
208
+ }
209
+ // Terminate all workers to force-stop blocking WASM computations
210
+ const hadWorkers = this._workers.length > 0
211
+ this._workers.forEach(w => {
212
+ try { w.terminate() } catch (e) { /* ignore */ }
213
+ })
214
+ this._workers = []
215
+ this._cancelWorkerRun = null
216
+ // Force model re-init on next run only when worker-backed state was torn down.
217
+ if (hadWorkers) this._needsModelReinit = true
192
218
  }
193
219
 
194
220
  isCancelled () {
@@ -264,6 +290,11 @@ export default class JSEE {
264
290
  progress.style.transform = 'none'
265
291
  progress.style.width = `${progressState.value}%`
266
292
  }
293
+
294
+ // Overlay progress bar
295
+ if (this.overlay) {
296
+ this.overlay.setProgress(progressState)
297
+ }
267
298
  }
268
299
 
269
300
  async init () {
@@ -274,6 +305,12 @@ export default class JSEE {
274
305
  this.streamInputConfig = collectStreamInputConfig(this.schema.inputs)
275
306
  await this.initVue() // Inits: this.app, this.data
276
307
  await this.initPipeline() // Inits: this.pipeline (function)
308
+
309
+ // Request notification permission if schema opts in
310
+ if (this.schema.notify && typeof Notification !== 'undefined' && Notification.permission === 'default') {
311
+ Notification.requestPermission()
312
+ }
313
+
277
314
  if (this.schema.autorun || this.schema.inputs.some(input => input.disabled && input.reactive)) {
278
315
  // 1. If autorun is enabled in the schema, run the model immediately
279
316
  // 2. Server-side inputs: If there are inputs with disabled and reactive flags
@@ -365,10 +402,10 @@ export default class JSEE {
365
402
  }
366
403
  })
367
404
 
368
- // Check if model is empty
405
+ // Check if model is empty — allow identity pipeline (no model)
369
406
  if (this.model.length === 0) {
370
- notyf.error('Model is in a wrong format')
371
- throw new Error(`Model is in a wrong format: ${this.schema.model}`)
407
+ log('No model defined, using identity pipeline')
408
+ return
372
409
  }
373
410
 
374
411
  // Put worker and imports inside model blocks
@@ -470,8 +507,12 @@ export default class JSEE {
470
507
  // Relies on model.code
471
508
  // So run after possible fetching
472
509
  if (typeof this.schema.inputs === 'undefined') {
473
- this.model[0].container = 'args'
474
- this.schema.inputs = getInputs(this.model[0])
510
+ if (this.model.length > 0) {
511
+ this.model[0].container = 'args'
512
+ this.schema.inputs = getInputs(this.model[0])
513
+ } else {
514
+ this.schema.inputs = []
515
+ }
475
516
  }
476
517
 
477
518
  // Read URL params, e.g. ?input1=1&input2=2
@@ -512,14 +553,6 @@ export default class JSEE {
512
553
  this.modelContainer = container.querySelector('#model')
513
554
  // Init overlay
514
555
  this.overlay = new Overlay(this.inputsContainer ? this.inputsContainer : this.outputsContainer)
515
- // Stop button is shown only while a run is active
516
- this.stopElement = document.createElement('button')
517
- this.stopElement.innerHTML = 'Stop'
518
- this.stopElement.style = 'display: none; margin-left: 12px; background: white; color: #333; border: 1px solid #DDD; padding: 6px 10px; border-radius: 5px; cursor: pointer;'
519
- this.stopElement.addEventListener('click', () => {
520
- this.cancelCurrentRun()
521
- })
522
- this.overlay.element.appendChild(this.stopElement)
523
556
  resolve()
524
557
  }, log)
525
558
  this.data = this.app.$data
@@ -606,6 +639,11 @@ export default class JSEE {
606
639
  notyf.success('Pipeline initialized')
607
640
  this.overlay.hide()
608
641
  }
642
+
643
+ if (this.model.length === 0) {
644
+ log('Identity pipeline ready (no model)')
645
+ this.overlay.hide()
646
+ }
609
647
  }
610
648
 
611
649
  async initWorker (model) {
@@ -636,6 +674,7 @@ export default class JSEE {
636
674
  : utils.toWorkerSerializable(inputs)
637
675
 
638
676
  const workerPromise = new Promise((resolve, reject) => {
677
+ this._rejectWorkerRun = reject
639
678
  worker.onmessage = (e) => {
640
679
  const res = e.data
641
680
  if ((typeof res === 'object') && (res._status)) {
@@ -659,6 +698,12 @@ export default class JSEE {
659
698
  reject(res._error)
660
699
  break
661
700
  }
701
+ } else if ((typeof res === 'object') && res._partial) {
702
+ // Partial/streaming result: update outputs without resolving the promise
703
+ log('Partial result from worker:', Object.keys(res))
704
+ const partial = { ...res }
705
+ delete partial._partial
706
+ this.output(partial)
662
707
  } else {
663
708
  log('Response from worker:', res)
664
709
  this.progress(0)
@@ -672,7 +717,13 @@ export default class JSEE {
672
717
  reject(e)
673
718
  }
674
719
  try {
675
- worker.postMessage(payload)
720
+ // Transfer ArrayBuffers for zero-copy WASM passing
721
+ const transferables = utils.collectTransferables(payload)
722
+ if (transferables.length) {
723
+ worker.postMessage(payload, transferables)
724
+ } else {
725
+ worker.postMessage(payload)
726
+ }
676
727
  } catch (error) {
677
728
  const hasBinaryPayload = utils.containsBinaryPayload(payload)
678
729
  if (hasBinaryPayload) {
@@ -719,7 +770,7 @@ export default class JSEE {
719
770
  await utils.importScripts(['https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js'])
720
771
  const pyodide = await loadPyodide()
721
772
  if (model.imports && Array.isArray(model.imports) && model.imports.length) {
722
- await pyodide.loadPackage(model.imports.url)
773
+ await pyodide.loadPackage(model.imports.map(i => i.url))
723
774
  } else {
724
775
  await pyodide.loadPackagesFromImports(model.code)
725
776
  }
@@ -778,8 +829,11 @@ export default class JSEE {
778
829
  // Worker:
779
830
  this.worker.postMessage(model)
780
831
  } else {
781
- // Main:
782
- return utils.getModelFuncAPI(model, log)
832
+ // Main: pass onChunk callback for SSE streaming
833
+ const onChunk = model.stream ? (chunk) => {
834
+ this.output(chunk)
835
+ } : undefined
836
+ return utils.getModelFuncAPI(model, log, onChunk)
783
837
  }
784
838
  }
785
839
 
@@ -811,10 +865,18 @@ export default class JSEE {
811
865
  return
812
866
  }
813
867
 
868
+ // Re-init model if workers were terminated by cancelCurrentRun
869
+ if (this._needsModelReinit) {
870
+ this._needsModelReinit = false
871
+ await this.initModel()
872
+ }
873
+
814
874
  const schema = this.schema
815
875
  const data = this.data
816
876
  this.running = true
817
877
  this.cancelled = false
878
+ let runSucceeded = false
879
+ let rejectCurrentRun = null
818
880
  // Run token to detect stale results when worker.onmessage gets rebound
819
881
  const runToken = this._runToken = {}
820
882
 
@@ -826,19 +888,54 @@ export default class JSEE {
826
888
  // Skip buttons
827
889
  if (input.name && !(input.type == 'action' || input.type == 'button')) {
828
890
  inputValues[input.name] = getValue(input)
891
+ } else if (input.type === 'group' && !input.name && input.elements) {
892
+ // Unnamed groups (e.g. tabs layout): flatten children into top-level
893
+ input.elements.forEach(el => {
894
+ if (el.name) {
895
+ inputValues[el.name] = getValue(el)
896
+ }
897
+ })
829
898
  }
830
899
  })
900
+ // For folder inputs with select mode, filter to selected files
901
+ data.inputs.forEach(input => {
902
+ if (input.type === 'folder' && input.select && Array.isArray(inputValues[input.name])) {
903
+ const selected = inputValues[input.name].filter(f => f.selected)
904
+ inputValues[input.name] = selected
905
+ }
906
+ })
907
+
831
908
  // Add caller to input values so we can change model behavior based on it
832
909
  inputValues.caller = caller
833
910
 
911
+ // Chat mode: inject history and capture user message
912
+ const chatOutput = this.data.outputs
913
+ ? this.data.outputs.find(o => o.type === 'chat')
914
+ : null
915
+ if (chatOutput) {
916
+ inputValues.history = chatOutput._messages || []
917
+ this._lastChatMessage = inputValues.message || ''
918
+ }
919
+
920
+ // Convert declared arrayBuffer inputs to typed arrays for WASM efficiency
921
+ inputValues = utils.wrapTypedArrayInputs(inputValues, schema.inputs)
922
+
834
923
  log('Input values:', inputValues)
835
924
  this.overlay.show()
836
- if (this.stopElement) {
837
- this.stopElement.style.display = 'inline-block'
838
- }
925
+ if (this.data) this.data.running = true
839
926
 
840
927
  // Run pipeline
841
- const results = await this.pipeline(inputValues)
928
+ const cancelPromise = new Promise((_, reject) => {
929
+ rejectCurrentRun = reject
930
+ this._rejectRun = reject
931
+ })
932
+ const pipelinePromise = Promise.resolve().then(() => this.pipeline(inputValues))
933
+ pipelinePromise.catch(err => {
934
+ if (this.cancelled || this._runToken !== runToken) {
935
+ log('Cancelled pipeline settled:', err)
936
+ }
937
+ })
938
+ const results = await Promise.race([pipelinePromise, cancelPromise])
842
939
 
843
940
  // Drop stale results if a newer run started (e.g. worker.onmessage rebound race)
844
941
  if (this._runToken !== runToken) return
@@ -846,6 +943,18 @@ export default class JSEE {
846
943
  // Output results
847
944
  this.output(results)
848
945
 
946
+ // Chat mode: clear the text input for next message
947
+ if (chatOutput && this._lastChatMessage) {
948
+ data.inputs.forEach(input => {
949
+ if (input.name === 'message' || input.type === 'text') {
950
+ input.value = ''
951
+ }
952
+ })
953
+ this._lastChatMessage = null
954
+ }
955
+
956
+ runSucceeded = true
957
+
849
958
  // Check if interval is defined
850
959
  if (utils.shouldContinueInterval(schema.interval, this.running, this.isCancelled(), caller)) {
851
960
  log('Interval is defined:', schema.interval)
@@ -853,16 +962,29 @@ export default class JSEE {
853
962
  await this.run(caller)
854
963
  }
855
964
  } catch (err) {
856
- // Surface pipeline/worker errors so they don't silently swallow failures
857
- log('Pipeline error:', err)
858
- notyf.error(typeof err === 'string' ? err : (err.message || 'Pipeline error'))
965
+ // Silently ignore cancellation errors from cancelCurrentRun()
966
+ if (this.cancelled && err && err.message === 'Cancelled') {
967
+ log('Run cancelled by user')
968
+ } else {
969
+ // Surface pipeline/worker errors so they don't silently swallow failures
970
+ log('Pipeline error:', err)
971
+ notyf.error(typeof err === 'string' ? err : (err.message || 'Pipeline error'))
972
+ }
859
973
  } finally {
974
+ if (this._rejectRun === rejectCurrentRun) {
975
+ this._rejectRun = null
976
+ }
860
977
  // Always clean up UI state so overlay and running flag don't get stuck
861
978
  this.overlay.hide()
862
- if (this.stopElement) {
863
- this.stopElement.style.display = 'none'
864
- }
865
979
  this.running = false
980
+ if (this.data) this.data.running = false
981
+
982
+ // Notify user when tab is hidden and run succeeded
983
+ if (runSucceeded && schema.notify && document.hidden
984
+ && typeof Notification !== 'undefined' && Notification.permission === 'granted') {
985
+ const title = this.model[0] && (this.model[0].title || this.model[0].name) || 'JSEE'
986
+ new Notification('Run complete', { body: title })
987
+ }
866
988
 
867
989
  // Drain queued run if a manual click arrived while we were running
868
990
  if (this._pendingRun) {
@@ -878,6 +1000,43 @@ export default class JSEE {
878
1000
  await utils.delay(1)
879
1001
  }
880
1002
 
1003
+ _mapResultsToOutputs (outputs, res) {
1004
+ outputs.forEach(output => {
1005
+ if (output.type === 'group' && output.elements) {
1006
+ this._mapResultsToOutputs(output.elements, res)
1007
+ } else {
1008
+ const r = res[output.name]
1009
+ || res[utils.sanitizeName(output.name)]
1010
+ || (output.alias && res[output.alias])
1011
+ if (typeof r !== 'undefined') {
1012
+ log(`Updating output: ${output.name} with data: ${typeof r}`)
1013
+ // Convert large base64 image data URLs to blob URLs for efficiency
1014
+ if (output.type === 'image' && typeof r === 'string'
1015
+ && r.startsWith('data:') && r.length > 50000) {
1016
+ // Revoke previous blob URL
1017
+ if (output._objectUrl) {
1018
+ URL.revokeObjectURL(output._objectUrl)
1019
+ }
1020
+ try {
1021
+ const [header, b64] = r.split(',')
1022
+ const mime = header.match(/data:([^;]+)/)[1]
1023
+ const binary = atob(b64)
1024
+ const bytes = new Uint8Array(binary.length)
1025
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
1026
+ const blob = new Blob([bytes], { type: mime })
1027
+ output._objectUrl = URL.createObjectURL(blob)
1028
+ output.value = output._objectUrl
1029
+ } catch (e) {
1030
+ output.value = r
1031
+ }
1032
+ } else {
1033
+ output.value = r
1034
+ }
1035
+ }
1036
+ }
1037
+ })
1038
+ }
1039
+
881
1040
  output (res) {
882
1041
  // Edge case: no output field with reactivity is handled — undefined results exit early
883
1042
 
@@ -887,6 +1046,14 @@ export default class JSEE {
887
1046
 
888
1047
  log('[Output] Got output results of type:', typeof res)
889
1048
 
1049
+ // Normalize primitive results to {result: value} for consistent rendering
1050
+ // regardless of execution mode (browser worker vs server POST)
1051
+ if ((typeof res !== 'object' || res === null) && !Array.isArray(res)) {
1052
+ if (!(this.schema.outputs && this.schema.outputs.length)) {
1053
+ res = { result: res }
1054
+ }
1055
+ }
1056
+
890
1057
  const inputNames = this.schema.inputs ? this.schema.inputs.map(i => i.name) : []
891
1058
  log('Input names:', inputNames)
892
1059
 
@@ -899,7 +1066,35 @@ export default class JSEE {
899
1066
  delete res._progress
900
1067
  log('Processing results as an object:', res)
901
1068
 
902
- if (Object.keys(res).every(key => inputNames.includes(key))) {
1069
+ // Chat mode: accumulate messages instead of replacing output
1070
+ if (this.data.outputs) {
1071
+ this.data.outputs.forEach(output => {
1072
+ if (output.type !== 'chat' || typeof res[output.name] === 'undefined') return
1073
+ if (!output._messages) output._messages = []
1074
+ if (this._lastChatMessage) {
1075
+ output._messages.push({ role: 'user', content: this._lastChatMessage })
1076
+ }
1077
+ const response = res[output.name]
1078
+ if (typeof response === 'string') {
1079
+ output._messages.push({ role: 'assistant', content: response })
1080
+ } else if (isObject(response) && response.content) {
1081
+ output._messages.push({ role: response.role || 'assistant', content: response.content })
1082
+ }
1083
+ delete res[output.name]
1084
+ })
1085
+ }
1086
+
1087
+ if (this.model.length === 0) {
1088
+ // Identity mode: all keys become outputs, auto-detect types
1089
+ log('Identity mode: creating outputs from all result keys')
1090
+ this.data.outputs = Object.keys(res)
1091
+ .filter(key => key !== 'caller')
1092
+ .map(key => ({
1093
+ name: key,
1094
+ type: utils.inferOutputType(key, res[key]),
1095
+ value: res[key]
1096
+ }))
1097
+ } else if (Object.keys(res).every(key => inputNames.includes(key))) {
903
1098
  // Update input fields from results
904
1099
  // e.g. loading a csv file and updating list of target variables
905
1100
  // This will be dynamically updated in the UI
@@ -920,16 +1115,7 @@ export default class JSEE {
920
1115
  } else if (this.data.outputs && this.data.outputs.length) {
921
1116
  // Update outputs from results
922
1117
  log('Updating outputs from results with keys:', Object.keys(res))
923
- this.data.outputs.forEach((output, i) => {
924
- // try output.name, sanitized output.name, output.alias
925
- const r = res[output.name]
926
- || res[utils.sanitizeName(output.name)]
927
- || (output.alias && res[output.alias])
928
- if (typeof r !== 'undefined') {
929
- log(`Updating output: ${output.name} with data: ${typeof r}`)
930
- output.value = r
931
- }
932
- })
1118
+ this._mapResultsToOutputs(this.data.outputs, res)
933
1119
  } else if (!this.schema.render && !this.schema.view) {
934
1120
  // There's no render or view defined in the schema, also:
935
1121
  // No outputs defined, create outputs from results
@@ -940,8 +1126,7 @@ export default class JSEE {
940
1126
  .map(key => {
941
1127
  return {
942
1128
  'name': key,
943
- // typeof returns 'object' for arrays; distinguish them for proper rendering
944
- 'type': Array.isArray(res[key]) ? 'array' : typeof res[key],
1129
+ 'type': utils.inferOutputType(key, res[key]),
945
1130
  'value': res[key]
946
1131
  }
947
1132
  })
@@ -1013,6 +1198,9 @@ export default class JSEE {
1013
1198
  console.error('Error removing GA script tags:', error.message)
1014
1199
  }
1015
1200
 
1201
+ // Remove serve bar (bundled HTML is standalone, no server)
1202
+ try { clone.getElementById('jsee-serve-bar')?.remove() } catch (e) {}
1203
+
1016
1204
  console.log('Caching schema:', this.schema)
1017
1205
  storeInHiddenElement(this.schemaUrl, this.schema)
1018
1206
 
package/src/overlay.js CHANGED
@@ -2,8 +2,15 @@ class Overlay {
2
2
  constructor (parent) {
3
3
  this.element = document.createElement('div')
4
4
  this.element.id = 'overlay'
5
- this.element.innerHTML = `...`
5
+ this.element.innerHTML = ''
6
6
  parent.appendChild(this.element)
7
+
8
+ this.progressBar = document.createElement('div')
9
+ this.progressBar.style.cssText = 'display:none; width:200px; height:6px; background:#e0e0e0; border-radius:3px; overflow:hidden;'
10
+ this.progressFill = document.createElement('div')
11
+ this.progressFill.style.cssText = 'width:0%; height:100%; background:#00d1b2; border-radius:3px; transition:width 0.2s;'
12
+ this.progressBar.appendChild(this.progressFill)
13
+ this.element.appendChild(this.progressBar)
7
14
  }
8
15
 
9
16
  show () {
@@ -12,6 +19,24 @@ class Overlay {
12
19
 
13
20
  hide () {
14
21
  this.element.style.display = 'none'
22
+ this.setProgress(null)
23
+ }
24
+
25
+ setProgress (state) {
26
+ if (!state) {
27
+ this.progressBar.style.display = 'none'
28
+ this.progressFill.style.width = '0%'
29
+ this.progressFill.style.animation = 'none'
30
+ return
31
+ }
32
+ this.progressBar.style.display = 'block'
33
+ if (state.mode === 'indeterminate') {
34
+ this.progressFill.style.width = '30%'
35
+ this.progressFill.style.animation = 'jsee-progress-indeterminate 1.2s ease-in-out infinite'
36
+ } else {
37
+ this.progressFill.style.animation = 'none'
38
+ this.progressFill.style.width = state.value + '%'
39
+ }
15
40
  }
16
41
  }
17
42