arcane-os 0.2.2 → 0.3.0

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 (117) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
  4. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
  5. package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
  6. package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
  8. package/browser-runtime/ai/browser-speech.mjs +2 -0
  9. package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
  10. package/browser-runtime/ai/model-controller.mjs +285 -95
  11. package/browser-runtime/ai/speech-worker-client.mjs +247 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
  13. package/browser-runtime/event-manager.mjs +1097 -1
  14. package/docs/architecture.md +2 -2
  15. package/docs/event-manager.md +155 -27
  16. package/docs/reference/README.md +27 -27
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1162 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +6 -3
  21. package/docs/reference/behavioral-testing.md +29 -6
  22. package/docs/reference/cli.md +117 -9
  23. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  24. package/docs/reference/event-manager.md +577 -32
  25. package/docs/reference/inventory/package-api.json +478 -2
  26. package/docs/reference/inventory/runtime-components.json +108 -44
  27. package/docs/reference/inventory/runtime-modules.json +131 -53
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +157 -43
  30. package/docs/reference/runtime-components.md +258 -83
  31. package/docs/reference/runtime-modules.md +613 -77
  32. package/docs/reference/sdk-api.md +1014 -25
  33. package/package.json +5 -4
  34. package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
  35. package/runtime/arcane/components/app-bar.html +34 -13
  36. package/runtime/arcane/components/assistant-panel.html +110 -57
  37. package/runtime/arcane/components/calculator.html +7 -4
  38. package/runtime/arcane/components/chart.html +58 -17
  39. package/runtime/arcane/components/chat.html +606 -136
  40. package/runtime/arcane/components/conversation-view.html +13 -6
  41. package/runtime/arcane/components/dashboard-config.html +96 -59
  42. package/runtime/arcane/components/data-maintenance.html +69 -14
  43. package/runtime/arcane/components/data-view.html +53 -7
  44. package/runtime/arcane/components/directory-picker.html +118 -32
  45. package/runtime/arcane/components/document-inspector.html +47 -10
  46. package/runtime/arcane/components/file-drop.html +81 -35
  47. package/runtime/arcane/components/file-inspector.html +72 -22
  48. package/runtime/arcane/components/file-manager.html +374 -79
  49. package/runtime/arcane/components/integration-settings.html +12 -5
  50. package/runtime/arcane/components/local-ai-status.html +48 -19
  51. package/runtime/arcane/components/markdown-document.html +161 -68
  52. package/runtime/arcane/components/markdown-editor.html +110 -33
  53. package/runtime/arcane/components/media-embed.html +8 -5
  54. package/runtime/arcane/components/modal.html +15 -5
  55. package/runtime/arcane/components/output-panel.html +28 -23
  56. package/runtime/arcane/components/preferences-form.html +22 -4
  57. package/runtime/arcane/components/record-timeline.html +18 -2
  58. package/runtime/arcane/components/relationship-board.html +23 -3
  59. package/runtime/arcane/components/screen-capture.html +10 -4
  60. package/runtime/arcane/components/source-code-viewer.html +76 -9
  61. package/runtime/arcane/components/source-explanation.html +23 -3
  62. package/runtime/arcane/components/speech.html +462 -384
  63. package/runtime/arcane/components/summary-strip.html +22 -11
  64. package/runtime/arcane/components/table.html +39 -21
  65. package/runtime/arcane/components/task-progress.html +79 -21
  66. package/runtime/arcane/components/terminal-workspace.html +7 -4
  67. package/runtime/arcane/components/theme-editor.html +7 -3
  68. package/runtime/arcane/components/unified-inbox.html +9 -4
  69. package/runtime/arcane/components/voice-transcription.html +639 -98
  70. package/runtime/arcane/components/weather-widget.html +5 -3
  71. package/runtime/arcane/components/web-navigator.html +48 -8
  72. package/runtime/arcane/entities/Chat.js +1 -1
  73. package/runtime/arcane/entities/User.js +110 -23
  74. package/runtime/arcane/modules/AI.js +2109 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
  76. package/runtime/arcane/modules/AIRuntimeState.js +109 -52
  77. package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
  78. package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
  79. package/runtime/arcane/modules/CalculatorEngine.js +63 -3
  80. package/runtime/arcane/modules/CommunicationAppController.js +588 -28
  81. package/runtime/arcane/modules/CommunicationHub.js +590 -10
  82. package/runtime/arcane/modules/ComponentContracts.js +470 -0
  83. package/runtime/arcane/modules/ConversationTimebox.js +152 -33
  84. package/runtime/arcane/modules/DBLS.js +40 -7
  85. package/runtime/arcane/modules/DBOPFS.js +35 -11
  86. package/runtime/arcane/modules/DataMaintenance.js +12 -2
  87. package/runtime/arcane/modules/Errors.js +65 -7
  88. package/runtime/arcane/modules/HTMLImport.js +198 -14
  89. package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
  90. package/runtime/arcane/modules/Mail.js +738 -115
  91. package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
  92. package/runtime/arcane/modules/MailTransport.mjs +197 -39
  93. package/runtime/arcane/modules/Ollama.js +36 -1
  94. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
  95. package/runtime/arcane/modules/PreferenceStore.js +367 -33
  96. package/runtime/arcane/modules/RecordReviewStore.js +322 -23
  97. package/runtime/arcane/modules/ScreenCapture.js +1397 -15
  98. package/runtime/arcane/modules/SpeechPlayback.js +438 -41
  99. package/runtime/arcane/modules/TerminalClient.js +277 -12
  100. package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
  101. package/runtime/arcane/modules/ThemeManager.js +39 -7
  102. package/runtime/arcane/modules/TimeGuard.js +131 -20
  103. package/runtime/arcane/modules/WaitForComponent.js +386 -33
  104. package/schemas/arcane-lock.schema.json +2 -2
  105. package/src/cli/main.mjs +435 -9
  106. package/src/event-manager.mjs +1097 -1
  107. package/src/import-map.mjs +21 -3
  108. package/src/index.mjs +13 -0
  109. package/src/installed-sdk-runtime.mjs +112 -0
  110. package/src/mail-api.mjs +22 -0
  111. package/src/mail-credentials.mjs +667 -0
  112. package/src/mail-server.mjs +1769 -0
  113. package/src/mail.mjs +261 -0
  114. package/src/sdk-browser-runtime.mjs +85 -41
  115. package/src/testing-loader.mjs +7 -0
  116. package/src/toolchain.mjs +3 -0
  117. package/src/workspace.mjs +1 -1
@@ -1,8 +1,14 @@
1
1
  <link rel="stylesheet" href="./arcane/css/theme.css?v=1"><link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
2
2
  <style>:host{display:block}.capture{display:grid;gap:var(--arcane-space-4)}.actions{display:grid;gap:var(--arcane-space-3);grid-template-columns:repeat(3,minmax(0,1fr))}.action{display:grid;gap:var(--arcane-space-2);padding:var(--arcane-space-4);text-align:left}.action strong{font-size:1.05rem}.action span{color:var(--arcane-muted);font-size:.85rem}.recording{align-items:center;display:flex;gap:var(--arcane-space-3);justify-content:space-between;padding:var(--arcane-space-3);border:1px solid var(--arcane-border);border-radius:var(--arcane-radius-medium)}.preview{display:grid;place-items:center;min-height:16rem;overflow:hidden;background:var(--arcane-surface-muted);border:1px solid var(--arcane-border);border-radius:var(--arcane-radius-large)}.preview :where(img,video){display:block;max-width:100%;max-height:34rem}.empty{color:var(--arcane-muted);text-align:center}.status{margin:0;color:var(--arcane-muted)}@media(max-width:44rem){.actions{grid-template-columns:1fr}}</style>
3
3
  <section class="capture"><div class="actions"><button class="action arcane-card" type="button" data-capture="image"><strong>Screenshot</strong><span>Capture the selected display as PNG.</span></button><button class="action arcane-card" type="button" data-capture="video"><strong>Record video</strong><span>Record the selected display in a browser-supported video format.</span></button><button class="action arcane-card" type="button" data-capture="gif"><strong>Record GIF</strong><span>Record a compact, lower-frame-rate animated GIF.</span></button></div><div id="recording" class="recording" hidden><span id="recordingLabel">Recording…</span><button id="stop" class="arcane-button" type="button">Stop recording</button></div><div id="preview" class="preview"><div class="empty"><strong>No capture yet</strong><p>Your capture remains on this device unless you choose to save it.</p></div></div><p id="status" class="status" role="status" aria-live="polite">Choose a capture type. Your browser will ask which display to share.</p></section>
4
- <script type="module">const host=this,root=host.shadowRoot,preview=root.querySelector('#preview'),recording=root.querySelector('#recording'),status=root.querySelector('#status');const componentURL=new URL(host.getAttribute('href')||'./arcane/components/screen-capture.html',document.baseURI);const {default:ScreenCapture}=await import(new URL('../modules/ScreenCapture.js',componentURL));const capture=new ScreenCapture();let previewUrl='';
5
- root.querySelector('.actions').addEventListener('click',async event=>{const mode=event.target.closest('[data-capture]')?.dataset.capture;if(!mode)return;setButtons(true);try{if(mode==='image'){status.textContent='Choose a display to capture…';showResult(await capture.captureImage())}else{status.textContent='Choose a display to record…';if(mode==='video')await capture.startVideo();else await capture.startGif();recording.hidden=false;root.querySelector('#recordingLabel').textContent=mode==='gif'?'Recording GIF frames…':'Recording video…';status.textContent='Recording. Return here and press Stop when finished.'}}catch(error){status.textContent=error.message;setButtons(false)}});root.querySelector('#stop').addEventListener('click',async()=>{root.querySelector('#stop').disabled=true;status.textContent='Finishing capture…';try{showResult(await capture.stop())}catch(error){status.textContent=error.message}finally{recording.hidden=true;root.querySelector('#stop').disabled=false;setButtons(false)}});
4
+ <script type="module">const host=this,root=host.shadowRoot,preview=root.querySelector('#preview'),recording=root.querySelector('#recording'),status=root.querySelector('#status');const componentURL=new URL(host.getAttribute('href')||'./arcane/components/screen-capture.html',document.baseURI);const {default:ScreenCapture}=await import(new URL('../modules/ScreenCapture.js',componentURL));const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.screen-capture',eventTypes:['screen-capture-result','screen-capture-ready']});const capture=new ScreenCapture(),listeners=new AbortController();let destroyed=false,operationGeneration=0,previewUrl='';
5
+ function emit(type,detail={},publicDetail={},operationId=null){if(destroyed)return false;const publication=events.dispatch(type,detail,{operationId,publicDetail});return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true})}
6
+ function current(generation){return !destroyed&&generation===operationGeneration}
7
+ function cleanupCaptureStart(originalError=null){const cleanupErrors=[];clearInterval(capture.timer);if(capture.recorder&&capture.recorder.state!=='inactive'){try{capture.recorder.stop()}catch(error){cleanupErrors.push(error)}}try{capture.stopTracks(capture.stream)}catch(error){cleanupErrors.push(error)}try{capture.reset()}catch(error){cleanupErrors.push(error)}for(const error of cleanupErrors)console.error('Error cleaning up rejected screen capture start:',error);return originalError}
8
+ async function onCapture(event){const mode=event.target.closest('[data-capture]')?.dataset.capture;if(!mode||destroyed)return;const generation=++operationGeneration;setButtons(true);try{if(mode==='image'){status.textContent='Choose a display to capture…';const result=await capture.captureImage();if(!current(generation))return;showResult(result,generation)}else{status.textContent='Choose a display to record…';if(mode==='video')await capture.startVideo();else await capture.startGif();if(!current(generation)){cleanupCaptureStart();return}recording.hidden=false;root.querySelector('#recordingLabel').textContent=mode==='gif'?'Recording GIF frames…':'Recording video…';status.textContent='Recording. Return here and press Stop when finished.'}}catch(error){const originalError=mode==='image'?error:cleanupCaptureStart(error);if(!current(generation))return;status.textContent=originalError.message;setButtons(false)}}
9
+ async function onStop(){if(destroyed)return;const generation=operationGeneration;root.querySelector('#stop').disabled=true;status.textContent='Finishing capture…';try{const result=await capture.stop();if(current(generation)&&result)showResult(result,generation)}catch(error){if(current(generation))status.textContent=error.message}finally{if(current(generation)){recording.hidden=true;root.querySelector('#stop').disabled=false;setButtons(false)}}}
10
+ root.querySelector('.actions').addEventListener('click',onCapture,{signal:listeners.signal});root.querySelector('#stop').addEventListener('click',onStop,{signal:listeners.signal});
6
11
  function setButtons(value){for(const button of root.querySelectorAll('[data-capture]'))button.disabled=value}
7
- function showResult(result){if(previewUrl)URL.revokeObjectURL(previewUrl);previewUrl=URL.createObjectURL(result.blob);const media=document.createElement(result.mimeType.startsWith('video/')?'video':'img');media.src=previewUrl;if(media.tagName==='VIDEO')media.controls=true;media.alt='Captured display preview';preview.replaceChildren(media);status.textContent=`Capture ready · ${(result.blob.size/1024/1024).toFixed(2)} MB`;setButtons(false);host.dispatchEvent(new CustomEvent('screen-capture-result',{bubbles:true,composed:true,detail:{result}}))}
8
- host.capture=capture;host.ready=true;host.dispatchEvent(new CustomEvent('screen-capture-ready',{bubbles:true,composed:true}));</script>
12
+ function showResult(result,generation){if(!current(generation))return false;if(previewUrl)URL.revokeObjectURL(previewUrl);previewUrl=URL.createObjectURL(result.blob);const media=document.createElement(result.mimeType.startsWith('video/')?'video':'img');media.src=previewUrl;if(media.tagName==='VIDEO')media.controls=true;media.alt='Captured display preview';preview.replaceChildren(media);status.textContent=`Capture ready · ${(result.blob.size/1024/1024).toFixed(2)} MB`;setButtons(false);return emit('screen-capture-result',{result},{mimeType:String(result.mimeType||''),size:Number(result.blob?.size||0)},`screen-capture-${events.instanceId}-${generation}`)}
13
+ function destroy(){if(destroyed)return false;destroyed=true;++operationGeneration;listeners.abort('screen-capture-component-destroyed');host.ready=false;recording.hidden=true;setButtons(true);root.querySelector('#stop').disabled=true;if(previewUrl){URL.revokeObjectURL(previewUrl);previewUrl=''}let error=null;try{capture.destroy()}catch(value){error=value}finally{events.dispose()}if(error)throw error;return true}
14
+ host.capture=capture;host.destroy=destroy;host.ready=true;emit('screen-capture-ready',{}, {ready:true},`screen-capture-ready-${events.instanceId}`);</script>
@@ -179,6 +179,18 @@
179
179
  <script type="module">
180
180
  const host=this;
181
181
  host.ready=false;
182
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
183
+ const events=createArcaneEventSource(host,{
184
+ source:'arcane.component.source-code-viewer',
185
+ eventTypes:[
186
+ 'source-code-viewer-state',
187
+ 'source-code-viewer-state-empty',
188
+ 'source-code-viewer-state-error',
189
+ 'source-code-viewer-state-loading',
190
+ 'source-code-viewer-state-ready',
191
+ 'source-code-viewer-ready'
192
+ ]
193
+ });
182
194
 
183
195
  const viewer=host.shadowRoot.querySelector('#viewer');
184
196
  const status=host.shadowRoot.querySelector('#status');
@@ -210,12 +222,15 @@
210
222
  text:'',
211
223
  title:''
212
224
  });
225
+ let destroyed=false;
226
+ let eventOperationSequence=0;
213
227
  let loadSequence=0;
214
228
  let options=normalizeOptions({},defaultOptions);
215
229
  let state='empty';
216
230
 
217
231
  host.clear=clear;
218
232
  host.configure=configure;
233
+ host.destroy=destroy;
219
234
  host.fail=fail;
220
235
  host.focus=focusSource;
221
236
  host.focusLine=focusLine;
@@ -289,12 +304,13 @@
289
304
  }
290
305
 
291
306
  async function load(source='',renderOptions={}){
307
+ if(destroyed)return false;
292
308
  const sequence=++loadSequence;
293
309
  transition('loading');
294
310
  try{
295
311
  const pending=typeof source==='function'?source():source;
296
312
  const result=await pending;
297
- if(sequence!==loadSequence)return false;
313
+ if(destroyed||sequence!==loadSequence)return false;
298
314
  if(result&&typeof result==='object'&&!Array.isArray(result)){
299
315
  return renderSource(result.text??result.value??'',{
300
316
  ...renderOptions,
@@ -303,12 +319,13 @@
303
319
  }
304
320
  return renderSource(result??'',renderOptions);
305
321
  }catch(error){
306
- if(sequence===loadSequence)showError(error);
322
+ if(!destroyed&&sequence===loadSequence)showError(error);
307
323
  return false;
308
324
  }
309
325
  }
310
326
 
311
327
  function render(text='',renderOptions={}){
328
+ if(destroyed)return false;
312
329
  ++loadSequence;
313
330
  try{
314
331
  return renderSource(text,renderOptions);
@@ -394,8 +411,11 @@
394
411
  }
395
412
 
396
413
  function transition(next,error=null){
414
+ if(destroyed)return false;
397
415
  state=next;
398
416
  updateStateDOM();
417
+ const operationId=
418
+ `${events.instanceId}:source-state:${(++eventOperationSequence).toString(36)}`;
399
419
  const detail={
400
420
  characters:current.text.length,
401
421
  language:current.language,
@@ -404,8 +424,46 @@
404
424
  state
405
425
  };
406
426
  if(error)detail.message=String(error?.message||'Source rendering failed.').slice(0,512);
407
- host.dispatchEvent(new CustomEvent('source-code-viewer-state',{bubbles:true,composed:true,detail}));
408
- host.dispatchEvent(new CustomEvent(`source-code-viewer-${state}`,{bubbles:true,composed:true,detail}));
427
+ const reason={
428
+ empty:'source-code-viewer-cleared',
429
+ error:'source-code-viewer-render-rejected',
430
+ loading:'source-code-viewer-load-started',
431
+ ready:'source-code-viewer-rendered'
432
+ }[state];
433
+ const publicDetail={
434
+ characters:detail.characters,
435
+ language:detail.language,
436
+ lines:detail.lines,
437
+ state:`source-code-viewer-${state}`,
438
+ reason,
439
+ ...(state==='error'?{
440
+ code:typeof error?.code==='string'
441
+ ?error.code
442
+ :'ARCANE_SOURCE_CODE_VIEWER_RENDER_REJECTED'
443
+ }:{})
444
+ };
445
+ const statePublication=events.dispatch(
446
+ 'source-code-viewer-state',
447
+ detail,
448
+ {operationId,publicDetail}
449
+ );
450
+ projectArcaneDOMEvent(host,statePublication.occurrence,{bubbles:true,composed:true});
451
+ const exactPublication=events.dispatch(
452
+ `source-code-viewer-state-${state}`,
453
+ detail,
454
+ {operationId,publicDetail}
455
+ );
456
+ projectArcaneDOMEvent(host,exactPublication.occurrence,{type:`source-code-viewer-${state}`,bubbles:true,composed:true});
457
+ return true;
458
+ }
459
+
460
+ function destroy(){
461
+ if(destroyed)return false;
462
+ destroyed=true;
463
+ loadSequence+=1;
464
+ host.ready=false;
465
+ events.dispose();
466
+ return true;
409
467
  }
410
468
 
411
469
  function updateStateDOM(){
@@ -433,9 +491,18 @@
433
491
  repositoryLink.textContent=options.labels.repository;
434
492
  transition('empty');
435
493
  host.ready=true;
436
- host.dispatchEvent(new CustomEvent('source-code-viewer-ready',{
437
- bubbles:true,
438
- composed:true,
439
- detail:{component:host,state}
440
- }));
494
+ const readyPublication=events.dispatch(
495
+ 'source-code-viewer-ready',
496
+ {component:host,state},
497
+ {
498
+ operationId:
499
+ `${events.instanceId}:ready:${(++eventOperationSequence).toString(36)}`,
500
+ publicDetail:{
501
+ ready:true,
502
+ state:'source-code-viewer-empty',
503
+ reason:'source-code-viewer-ready'
504
+ }
505
+ }
506
+ );
507
+ projectArcaneDOMEvent(host,readyPublication.occurrence,{bubbles:true,composed:true});
441
508
  </script>
@@ -47,17 +47,24 @@
47
47
  const elements=root.querySelector('[data-elements]'); const application=root.querySelector('[data-application]'); const limitations=root.querySelector('[data-limitations]'); const nextSteps=root.querySelector('[data-next-steps]');
48
48
  const evidenceList=root.querySelector('[data-evidence]'); const original=root.querySelector('[data-original]'); const originalLabel=root.querySelector('[data-original-label]'); const originalPicker=root.querySelector('[data-original-picker]');
49
49
  const reviewForm=root.querySelector('[data-review]'); const actor=root.querySelector('[data-actor]'); const disposition=root.querySelector('[data-disposition]'); const confidence=root.querySelector('[data-confidence]'); const notes=root.querySelector('[data-notes]'); const saveState=root.querySelector('[data-save-state]');
50
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
51
+ const events=createArcaneEventSource(host,{source:'arcane.component.source-explanation',eventTypes:['source-explanation-save','source-explanation-source-selected','source-explanation-ready']});
52
+ let destroyed=false;
50
53
  let finding=null; let sources=[];
51
54
 
52
55
  host.showFinding=showFinding;
53
56
  host.populate=(value={},options={})=>showFinding(value,options);
54
57
  host.selectSource=selectSource;
55
58
  host.markSaved=message=>{saveState.textContent=String(message||'Saved.');};
59
+ host.destroy=destroy;
56
60
 
57
61
  reviewForm.addEventListener('submit',event=>{
62
+ if(destroyed) return;
58
63
  event.preventDefault(); if(!finding) return;
59
64
  saveState.textContent='Saving…';
60
- host.dispatchEvent(new CustomEvent('source-explanation-save',{bubbles:true,composed:true,detail:{recordId:finding.id,classification:actor.value,status:disposition.value,attributes:{confidence:confidence.value},notes:notes.value}}));
65
+ const detail={recordId:finding.id,classification:actor.value,status:disposition.value,attributes:{confidence:confidence.value},notes:notes.value};
66
+ const publication=events.dispatch('source-explanation-save',detail,{publicDetail:{hasFinding:true,hasNotes:Boolean(notes.value)}});
67
+ projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
61
68
  });
62
69
  originalPicker.addEventListener('change',()=>showSource(Number(originalPicker.value)));
63
70
 
@@ -110,15 +117,28 @@
110
117
  }
111
118
 
112
119
  function showSource(index=0){
120
+ if(destroyed) return null;
113
121
  const source=sources[index]; if(!source?.pdfUrl) return null;
114
122
  try{
115
123
  const url=sameOrigin(source.pdfUrl); if(source.page){url.searchParams.set('arcane-pdf-page',String(Number(source.page)));url.hash=`page=${Number(source.page)}`;}
116
124
  originalPicker.value=String(index); original.src=url.href; original.title=`Original cited source: ${source.filename||source.recordId||'document'}${source.page?`, page ${source.page}`:''}`; original.hidden=false;
117
125
  originalLabel.textContent=`Original source${source.page?` at PDF page ${source.page}`:''}. Compare the full page before relying on the extracted text.`;
118
- host.dispatchEvent(new CustomEvent('source-explanation-source-selected',{bubbles:true,composed:true,detail:{index,source:{...source}}}));
126
+ const publication=events.dispatch('source-explanation-source-selected',{index,source:{...source}},{publicDetail:{index,hasSource:true}});
127
+ projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
119
128
  return {...source};
120
129
  }catch(error){original.hidden=true; originalLabel.textContent=error.message; return null;}
121
130
  }
122
131
 
123
- host.dispatchEvent(new CustomEvent('source-explanation-ready',{bubbles:true,composed:true}));
132
+ function destroy(){
133
+ if(destroyed) return false;
134
+ destroyed=true;
135
+ host.ready=false;
136
+ original.removeAttribute('src');
137
+ events.dispose();
138
+ return true;
139
+ }
140
+
141
+ host.ready=true;
142
+ const readyPublication=events.dispatch('source-explanation-ready',{}, {publicDetail:{ready:true}});
143
+ projectArcaneDOMEvent(host,readyPublication.occurrence,{bubbles:true,composed:true});
124
144
  </script>