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
@@ -89,6 +89,9 @@
89
89
  applyMarkdownFormat,
90
90
  normalizeMarkdownOptions
91
91
  }=await import(moduleURL('ComponentContracts.js'));
92
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
93
+ const events=createArcaneEventSource(host,{source:'arcane.component.markdown-editor',eventTypes:['markdown-editor-change','markdown-editor-saved','markdown-editor-ready']});
94
+ const lifecycleController=new AbortController();
92
95
  const titleInput=host.shadowRoot.querySelector('#entryTitle');
93
96
  const preview=host.shadowRoot.querySelector('#preview');
94
97
  const toolbar=host.shadowRoot.querySelector('#toolbar');
@@ -97,29 +100,43 @@
97
100
  const status=host.shadowRoot.querySelector('#status');
98
101
 
99
102
  let previewFrame=0;
103
+ let operationSequence=0;
104
+ let saveGeneration=0;
105
+ let saveController=null;
106
+ let destroyed=false;
100
107
  let options=normalizeMarkdownOptions(optionsFromDataset());
101
108
 
109
+ function emit(type,detail={},publicDetail={},operationId=null){
110
+ if(destroyed){
111
+ return false;
112
+ }
113
+ const publication=events.dispatch(type,detail,{publicDetail,operationId});
114
+ return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
115
+ }
116
+
102
117
  host.clear=clear;
103
118
  host.configure=configure;
104
- host.focus=()=>textarea.focus();
119
+ host.destroy=destroy;
120
+ host.focus=()=>{if(destroyed)return false;textarea.focus();return true;};
105
121
  host.saveEntry=saveEntry;
106
122
 
107
123
  Object.defineProperties(
108
124
  host,
109
125
  {
110
126
  entryTitle:{
127
+ configurable:true,
111
128
  get:()=>titleInput.value,
112
129
  set:value=>setTitle(value)
113
130
  },
114
- options:{get:getOptions,set:configure},
115
- value:{get:()=>textarea.value,set:setValue}
131
+ options:{configurable:true,get:getOptions,set:configure},
132
+ value:{configurable:true,get:()=>textarea.value,set:setValue}
116
133
  }
117
134
  );
118
135
 
119
- textarea.addEventListener('input',contentChanged);
120
- titleInput.addEventListener('input',contentChanged);
121
- toolbar.addEventListener('click',formatMarkdown);
122
- saveButton.addEventListener('click',saveEntry);
136
+ textarea.addEventListener('input',contentChanged,{signal:lifecycleController.signal});
137
+ titleInput.addEventListener('input',contentChanged,{signal:lifecycleController.signal});
138
+ toolbar.addEventListener('click',formatMarkdown,{signal:lifecycleController.signal});
139
+ saveButton.addEventListener('click',saveEntry,{signal:lifecycleController.signal});
123
140
 
124
141
  function optionsFromDataset(){
125
142
  const dataset=host.dataset||{};
@@ -152,6 +169,9 @@
152
169
  }
153
170
 
154
171
  function configure(input={}){
172
+ if(destroyed){
173
+ return false;
174
+ }
155
175
  const setInitialValue=Object.prototype.hasOwnProperty.call(
156
176
  input||{},
157
177
  'initialValue'
@@ -163,9 +183,11 @@
163
183
  options=normalizeMarkdownOptions(input,options);
164
184
  renderOptions();
165
185
  if(setInitialTitle){
186
+ supersedeSave('markdown-title-replaced');
166
187
  titleInput.value=options.initialTitle;
167
188
  }
168
189
  if(setInitialValue){
190
+ supersedeSave('markdown-content-replaced');
169
191
  textarea.value=options.initialValue;
170
192
  }
171
193
  updatePreview();
@@ -204,17 +226,16 @@
204
226
  }
205
227
 
206
228
  function contentChanged(){
229
+ if(destroyed){
230
+ return;
231
+ }
232
+ supersedeSave('markdown-content-edited');
207
233
  schedulePreview();
208
234
  const detail={
209
235
  markdown:textarea.value,
210
236
  title:titleInput.value
211
237
  };
212
- host.dispatchEvent(
213
- new CustomEvent(
214
- 'markdown-editor-change',
215
- {bubbles:true,composed:true,detail}
216
- )
217
- );
238
+ emit('markdown-editor-change',detail,{markdownLength:detail.markdown.length,titleLength:detail.title.length});
218
239
  if(options.onChange){
219
240
  Promise.resolve(options.onChange(detail)).catch(
220
241
  error=>console.error('Markdown change callback failed:',error)
@@ -223,6 +244,9 @@
223
244
  }
224
245
 
225
246
  function schedulePreview(){
247
+ if(destroyed){
248
+ return;
249
+ }
226
250
  if(previewFrame){
227
251
  cancelAnimationFrame(previewFrame);
228
252
  }
@@ -230,6 +254,9 @@
230
254
  }
231
255
 
232
256
  function updatePreview(){
257
+ if(destroyed){
258
+ return;
259
+ }
233
260
  if(previewFrame){
234
261
  cancelAnimationFrame(previewFrame);
235
262
  }
@@ -268,7 +295,7 @@
268
295
  }
269
296
 
270
297
  async function saveEntry(){
271
- if(options.readOnly||!options.showSave){
298
+ if(destroyed||options.readOnly||!options.showSave){
272
299
  return false;
273
300
  }
274
301
  const markdown=textarea.value;
@@ -287,35 +314,52 @@
287
314
  return false;
288
315
  }
289
316
 
317
+ saveController?.abort('markdown-save-replaced');
318
+ const controller=new AbortController();
319
+ saveController=controller;
320
+ const generation=++saveGeneration;
321
+ const operationId=`${events.descriptor.instanceId}:save:${++operationSequence}`;
290
322
  saveButton.disabled=true;
291
323
  status.innerText=options.labels.saving;
292
324
  try{
293
- await save({title:entryTitle,markdown});
325
+ await save({title:entryTitle,markdown},{signal:controller.signal,operationId});
326
+ if(destroyed||generation!==saveGeneration){
327
+ return false;
328
+ }
294
329
  if(options.clearOnSave){
295
- clear();
330
+ resetEditor(false);
296
331
  }
297
332
  status.innerText=options.labels.saved;
298
- host.dispatchEvent(
299
- new CustomEvent(
300
- 'markdown-editor-saved',
301
- {
302
- bubbles:true,
303
- composed:true,
304
- detail:{title:entryTitle,markdown}
305
- }
306
- )
307
- );
333
+ emit('markdown-editor-saved',{title:entryTitle,markdown},{markdownLength:markdown.length,titleLength:entryTitle.length},operationId);
308
334
  return true;
309
335
  }catch(error){
336
+ if(destroyed||generation!==saveGeneration||controller.signal.aborted){
337
+ return false;
338
+ }
310
339
  status.innerText=options.labels.saveError;
311
340
  console.error('Unable to save Markdown content:',error);
312
341
  return false;
313
342
  }finally{
314
- syncSaveButton();
343
+ if(saveController===controller){
344
+ saveController=null;
345
+ }
346
+ if(!destroyed&&generation===saveGeneration){
347
+ syncSaveButton();
348
+ }
315
349
  }
316
350
  }
317
351
 
318
352
  function clear(){
353
+ return resetEditor(true);
354
+ }
355
+
356
+ function resetEditor(supersede){
357
+ if(destroyed){
358
+ return false;
359
+ }
360
+ if(supersede){
361
+ supersedeSave('markdown-content-cleared');
362
+ }
319
363
  titleInput.value='';
320
364
  textarea.value='';
321
365
  updatePreview();
@@ -323,12 +367,20 @@
323
367
  }
324
368
 
325
369
  function setValue(value=''){
370
+ if(destroyed){
371
+ return false;
372
+ }
373
+ supersedeSave('markdown-content-replaced');
326
374
  textarea.value=`${value??''}`;
327
375
  updatePreview();
328
376
  return textarea.value;
329
377
  }
330
378
 
331
379
  function setTitle(value=''){
380
+ if(destroyed){
381
+ return false;
382
+ }
383
+ supersedeSave('markdown-title-replaced');
332
384
  titleInput.value=`${value??''}`;
333
385
  return titleInput.value;
334
386
  }
@@ -345,16 +397,41 @@
345
397
  };
346
398
  }
347
399
 
400
+ function supersedeSave(reason){
401
+ const active=saveController!==null;
402
+ saveGeneration+=1;
403
+ saveController?.abort(reason);
404
+ saveController=null;
405
+ if(!destroyed){
406
+ if(active){
407
+ status.innerText='';
408
+ }
409
+ syncSaveButton();
410
+ }
411
+ }
412
+
413
+ function destroy(){
414
+ if(destroyed){
415
+ return false;
416
+ }
417
+ destroyed=true;
418
+ supersedeSave('markdown-editor-destroyed');
419
+ lifecycleController.abort('markdown-editor-destroyed');
420
+ if(previewFrame){
421
+ cancelAnimationFrame(previewFrame);
422
+ previewFrame=0;
423
+ }
424
+ preview.replaceChildren();
425
+ events.dispose();
426
+ host.ready=false;
427
+ return true;
428
+ }
429
+
348
430
  renderOptions();
349
431
  titleInput.value=options.initialTitle;
350
432
  textarea.value=options.initialValue;
351
433
  updatePreview();
352
434
  status.innerText='';
353
435
  host.ready=true;
354
- host.dispatchEvent(
355
- new CustomEvent(
356
- 'markdown-editor-ready',
357
- {bubbles:true,composed:true}
358
- )
359
- );
436
+ emit('markdown-editor-ready',{}, {ready:true});
360
437
  </script>
@@ -1,8 +1,11 @@
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}.player-shell{display:grid;gap:var(--arcane-space-4)}.loader{display:flex;gap:var(--arcane-space-2)}.loader input{min-width:0;width:100%}.player{aspect-ratio:16/9;display:grid;place-items:center;overflow:hidden;background:var(--arcane-surface-muted);border:1px solid var(--arcane-border);border-radius:var(--arcane-radius-large)}iframe{width:100%;height:100%;border:0}.empty{text-align:center;color:var(--arcane-muted);padding:var(--arcane-space-5)}.footer{align-items:center;display:flex;gap:var(--arcane-space-3);justify-content:space-between}.status{margin:0;color:var(--arcane-muted)}@media(max-width:36rem){.loader,.footer{align-items:stretch;flex-direction:column}}</style>
3
3
  <section class="player-shell"><form id="loader" class="loader"><label class="visually-hidden" for="mediaUrl">Video or playlist address</label><input id="mediaUrl" type="text" inputmode="url" autocomplete="off" spellcheck="false" placeholder="Paste a YouTube video or playlist URL"><button class="arcane-button" type="submit">Play</button></form><div class="player"><div id="empty" class="empty"><strong>Choose something to play</strong><p>Paste a YouTube or YouTube Music video or playlist address.</p></div><iframe id="frame" title="Embedded media player" hidden allow="autoplay; encrypted-media; picture-in-picture; fullscreen" allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe></div><div class="footer"><p id="status" class="status" role="status" aria-live="polite">Nothing loaded.</p><button id="platform" class="arcane-button arcane-button--secondary" type="button">Open platform</button></div></section>
4
- <script type="module">const host=this,root=host.shadowRoot,form=root.querySelector('#loader'),input=root.querySelector('#mediaUrl'),frame=root.querySelector('#frame'),empty=root.querySelector('#empty'),status=root.querySelector('#status');const componentURL=new URL(host.getAttribute('href')||'./arcane/components/media-embed.html',document.baseURI);const {parseYouTubeMedia,youtubeEmbedUrl}=await import(new URL('../modules/YouTubeMedia.js',componentURL));let platformUrl='https://www.youtube.com/',privacyEnhanced=true;
5
- host.configure=options=>{platformUrl=String(options?.platformUrl||platformUrl);privacyEnhanced=options?.privacyEnhanced!==false;if(options?.placeholder)input.placeholder=String(options.placeholder);if(options?.openLabel)root.querySelector('#platform').textContent=String(options.openLabel);if(options?.initialValue){input.value=String(options.initialValue);load(input.value)}return host};host.load=load;
6
- form.addEventListener('submit',event=>{event.preventDefault();load(input.value)});root.querySelector('#platform').addEventListener('click',()=>host.dispatchEvent(new CustomEvent('media-open-platform',{bubbles:true,composed:true,detail:{url:platformUrl}})));
7
- function load(value){try{const locator=parseYouTubeMedia(value),url=youtubeEmbedUrl(locator,{privacyEnhanced});frame.src=url;frame.hidden=false;empty.hidden=true;status.textContent=locator.type==='playlist'?'Playlist loaded.':'Video loaded.';host.dispatchEvent(new CustomEvent('media-load',{bubbles:true,composed:true,detail:{locator,url}}));return locator}catch(error){status.textContent=error.message;frame.hidden=true;empty.hidden=false;host.dispatchEvent(new CustomEvent('media-error',{bubbles:true,composed:true,detail:{error}}));return null}}
8
- host.ready=true;host.dispatchEvent(new CustomEvent('media-embed-ready',{bubbles:true,composed:true}));</script>
4
+ <script type="module">const host=this,root=host.shadowRoot,form=root.querySelector('#loader'),input=root.querySelector('#mediaUrl'),frame=root.querySelector('#frame'),empty=root.querySelector('#empty'),status=root.querySelector('#status');const componentURL=new URL(host.getAttribute('href')||'./arcane/components/media-embed.html',document.baseURI);const {parseYouTubeMedia,youtubeEmbedUrl}=await import(new URL('../modules/YouTubeMedia.js',componentURL));const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');const events=createArcaneEventSource(host,{source:'arcane.component.media-embed',eventTypes:['media-open-platform','media-load','media-error','media-embed-ready']}),lifecycleController=new AbortController();let platformUrl='https://www.youtube.com/',privacyEnhanced=true,destroyed=false,operationSequence=0;
5
+ function nextOperationId(kind){operationSequence+=1;return `${events.descriptor.instanceId}:${kind}:${operationSequence}`}
6
+ function emit(type,detail={},publicDetail={},operationId=null){if(destroyed)return false;const publication=events.dispatch(type,detail,{publicDetail,operationId});return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true})}
7
+ host.configure=options=>{if(destroyed)return false;platformUrl=String(options?.platformUrl||platformUrl);privacyEnhanced=options?.privacyEnhanced!==false;if(options?.placeholder)input.placeholder=String(options.placeholder);if(options?.openLabel)root.querySelector('#platform').textContent=String(options.openLabel);if(options?.initialValue){input.value=String(options.initialValue);load(input.value)}return host};host.load=load;host.destroy=destroy;
8
+ form.addEventListener('submit',event=>{event.preventDefault();load(input.value)},{signal:lifecycleController.signal});root.querySelector('#platform').addEventListener('click',()=>emit('media-open-platform',{url:platformUrl},{destination:'platform'},nextOperationId('open-platform')),{signal:lifecycleController.signal});
9
+ function load(value){if(destroyed)return null;const operationId=nextOperationId('load');try{const locator=parseYouTubeMedia(value),url=youtubeEmbedUrl(locator,{privacyEnhanced});frame.src=url;frame.hidden=false;empty.hidden=true;status.textContent=locator.type==='playlist'?'Playlist loaded.':'Video loaded.';emit('media-load',{locator,url},{mediaType:String(locator.type||'video')},operationId);return locator}catch(error){const code=String(error?.code||'ARCANE_MEDIA_EMBED_SOURCE_INVALID'),reason='media-source-invalid';status.textContent=error.message;frame.hidden=true;empty.hidden=false;emit('media-error',{error,code,reason},{code,reason},operationId);return null}}
10
+ function destroy(){if(destroyed)return false;destroyed=true;lifecycleController.abort('media-embed-destroyed');frame.src='about:blank';frame.removeAttribute('src');frame.hidden=true;events.dispose();host.ready=false;return true}
11
+ host.ready=true;emit('media-embed-ready',{}, {ready:true});</script>
@@ -158,6 +158,8 @@
158
158
  <script type="module">
159
159
  const host=this;
160
160
  const shadowRoot=host.shadowRoot;
161
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
162
+ const events=createArcaneEventSource(host,{source:'arcane.component.modal',eventTypes:['modal-action','modal-opened','modal-closed','modal-ready']});
161
163
  const dialog=shadowRoot.querySelector('#modal');
162
164
  const modalContent=shadowRoot.querySelector('#modal-content');
163
165
  const modalSearch=shadowRoot.querySelector('#modal-search');
@@ -175,8 +177,14 @@
175
177
  host.populate=populate;
176
178
  host.open=open;
177
179
  host.close=close;
180
+ host.destroy=dispose;
178
181
  host.runTasks=runTasks;
179
182
 
183
+ function emit(type,detail={},publicDetail={}){
184
+ const publication=events.dispatch(type,detail,{publicDetail});
185
+ return projectArcaneDOMEvent(host,publication.occurrence);
186
+ }
187
+
180
188
  Object.defineProperties(host,{
181
189
  running:{get:function getRunningState(){return running;}},
182
190
  opened:{get:function getOpenedState(){return dialog.open;}}
@@ -220,7 +228,7 @@
220
228
  const detail={...action.dataset};
221
229
  detail.action=detail.modalAction;
222
230
  delete detail.modalAction;
223
- host.dispatchEvent(new CustomEvent('modal-action',{detail}));
231
+ emit('modal-action',detail,{action:String(detail.action||'')});
224
232
  });
225
233
 
226
234
  async function open(){
@@ -244,7 +252,7 @@
244
252
  const focusTarget=modalContent.querySelector('[autofocus],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),a[href]')||closeButton;
245
253
  focusTarget?.focus({preventScroll:true});
246
254
  });
247
- host.dispatchEvent(new CustomEvent('modal-opened'));
255
+ emit('modal-opened',{}, {opened:true});
248
256
  }
249
257
  return true;
250
258
  }
@@ -269,7 +277,7 @@
269
277
  }
270
278
 
271
279
  if(wasOpen){
272
- host.dispatchEvent(new CustomEvent('modal-closed'));
280
+ emit('modal-closed',{}, {opened:false});
273
281
  const target=returnFocus;
274
282
  returnFocus=null;
275
283
  queueMicrotask(function restoreModalReturnFocus(){
@@ -287,7 +295,7 @@
287
295
 
288
296
  function dispose(){
289
297
  if(disposed){
290
- return;
298
+ return false;
291
299
  }
292
300
  disposed=true;
293
301
  host.ready=false;
@@ -298,7 +306,9 @@
298
306
  if(dialog.open){
299
307
  dialog.close();
300
308
  }
309
+ events.dispose();
301
310
  host.remove();
311
+ return true;
302
312
  }
303
313
 
304
314
  async function populate(content='<p>Content goes here</p>',search=false){
@@ -398,5 +408,5 @@
398
408
  }
399
409
 
400
410
  host.ready=true;
401
- host.dispatchEvent(new CustomEvent('modal-ready'));
411
+ emit('modal-ready',{}, {ready:true});
402
412
  </script>
@@ -43,6 +43,8 @@
43
43
  <script type="module">
44
44
  const host=this;
45
45
  const root=host.shadowRoot;
46
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
47
+ const events=createArcaneEventSource(host,{source:'arcane.component.output-panel',eventTypes:['output-panel-action','output-panel-state','output-panel-error','output-panel-change','output-panel-cleared','output-panel-ready']});
46
48
  const region=root.querySelector('#region');
47
49
  const title=root.querySelector('#outputTitle');
48
50
  const status=root.querySelector('#status');
@@ -55,11 +57,13 @@
55
57
  const footerActions=root.querySelector('#footerActions');
56
58
 
57
59
  let actions=[];
60
+ let destroyed=false;
58
61
  let output=null;
59
62
  let options={};
60
63
 
61
64
  host.clear=clear;
62
65
  host.configure=configure;
66
+ host.destroy=destroy;
63
67
  host.setActions=setActions;
64
68
  host.setBody=setBody;
65
69
  host.setCoverage=setCoverage;
@@ -77,6 +81,13 @@
77
81
  footerActions.addEventListener('click',handleAction);
78
82
  bodySlot.addEventListener('slotchange',syncBodySlot);
79
83
 
84
+ function emit(type,detail={},publicDetail={},cancelable=false){
85
+ if(destroyed)return false;
86
+ const publication=events.dispatch(type,detail,{publicDetail,cancelable});
87
+ if(cancelable&&!publication.accepted) return false;
88
+ return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true,cancelable});
89
+ }
90
+
80
91
  function datasetOptions(){
81
92
  return {
82
93
  ariaLabel:host.dataset.ariaLabel||'Output',
@@ -183,12 +194,7 @@
183
194
  if(!button||!action||action.disabled){
184
195
  return;
185
196
  }
186
- host.dispatchEvent(new CustomEvent('output-panel-action',{
187
- bubbles:true,
188
- cancelable:true,
189
- composed:true,
190
- detail:{action:{...action},id:action.id,output:output?{...output}:null}
191
- }));
197
+ emit('output-panel-action',{action:{...action},id:action.id,output:output?{...output}:null},{actionId:action.id,hasOutput:Boolean(output)},true);
192
198
  }
193
199
 
194
200
  function setPending(value=true,message='Working…'){
@@ -197,11 +203,7 @@
197
203
  if(pending){
198
204
  renderState(message,'loading');
199
205
  }
200
- host.dispatchEvent(new CustomEvent('output-panel-state',{
201
- bubbles:true,
202
- composed:true,
203
- detail:{pending,state:pending?'loading':'ready'}
204
- }));
206
+ emit('output-panel-state',{pending,state:pending?'loading':'ready'},{pending,state:pending?'loading':'ready'});
205
207
  return pending;
206
208
  }
207
209
 
@@ -210,11 +212,7 @@
210
212
  body.setAttribute('aria-busy','false');
211
213
  if(text){
212
214
  renderState(text,'error');
213
- host.dispatchEvent(new CustomEvent('output-panel-error',{
214
- bubbles:true,
215
- composed:true,
216
- detail:{message:text,output:output?{...output}:null}
217
- }));
215
+ emit('output-panel-error',{message:text,output:output?{...output}:null},{hasOutput:Boolean(output),hasError:true});
218
216
  }
219
217
  return text;
220
218
  }
@@ -231,11 +229,7 @@
231
229
  }
232
230
 
233
231
  function emitChange(){
234
- host.dispatchEvent(new CustomEvent('output-panel-change',{
235
- bubbles:true,
236
- composed:true,
237
- detail:{output:output?{...output}:null}
238
- }));
232
+ emit('output-panel-change',{output:output?{...output}:null},{hasOutput:Boolean(output)});
239
233
  }
240
234
 
241
235
  function clear(){
@@ -247,7 +241,18 @@
247
241
  setActions([]);
248
242
  renderState(options.emptyLabel||'No output yet.','empty');
249
243
  body.setAttribute('aria-busy','false');
250
- host.dispatchEvent(new CustomEvent('output-panel-cleared',{bubbles:true,composed:true}));
244
+ emit('output-panel-cleared');
245
+ return true;
246
+ }
247
+
248
+ function destroy(){
249
+ if(destroyed)return false;
250
+ destroyed=true;
251
+ host.ready=false;
252
+ headerActions.removeEventListener('click',handleAction);
253
+ footerActions.removeEventListener('click',handleAction);
254
+ bodySlot.removeEventListener('slotchange',syncBodySlot);
255
+ events.dispose();
251
256
  return true;
252
257
  }
253
258
 
@@ -255,5 +260,5 @@
255
260
  clear();
256
261
  syncBodySlot();
257
262
  host.ready=true;
258
- host.dispatchEvent(new CustomEvent('output-panel-ready',{bubbles:true,composed:true}));
263
+ emit('output-panel-ready',{}, {ready:true});
259
264
  </script>
@@ -38,9 +38,19 @@
38
38
  const status=root.querySelector('#status');
39
39
  const reset=root.querySelector('#reset');
40
40
  const save=root.querySelector('#save');
41
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
42
+ const events=createArcaneEventSource(host,{source:'arcane.component.preferences-form',eventTypes:['preferences-submit','preferences-change','preferences-reset','preferences-form-ready']});
43
+ let destroyed=false;
41
44
  let schema=[];
42
45
 
46
+ function emit(type,detail={},publicDetail={}){
47
+ if(destroyed)return false;
48
+ const publication=events.dispatch(type,detail,{publicDetail});
49
+ return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
50
+ }
51
+
43
52
  host.configure=configure;
53
+ host.destroy=destroy;
44
54
  host.getValues=getValues;
45
55
  host.setValues=setValues;
46
56
  host.setBusy=setBusy;
@@ -48,14 +58,14 @@
48
58
 
49
59
  form.addEventListener('submit',event=>{
50
60
  event.preventDefault();
51
- host.dispatchEvent(new CustomEvent('preferences-submit',{bubbles:true,composed:true,detail:{values:getValues()}}));
61
+ emit('preferences-submit',{values:getValues()},{fieldCount:schema.length});
52
62
  });
53
63
  form.addEventListener('input',event=>{
54
64
  const field=schema.find(item=>item.key===event.target.name);
55
65
  if(!field) return;
56
- host.dispatchEvent(new CustomEvent('preferences-change',{bubbles:true,composed:true,detail:{key:field.key,value:readControl(event.target,field),values:getValues()}}));
66
+ emit('preferences-change',{key:field.key,value:readControl(event.target,field),values:getValues()},{key:String(field.key),fieldType:String(field.type||'text')});
57
67
  });
58
- reset.addEventListener('click',()=>host.dispatchEvent(new CustomEvent('preferences-reset',{bubbles:true,composed:true})));
68
+ reset.addEventListener('click',()=>emit('preferences-reset'));
59
69
 
60
70
  function configure(options={}){
61
71
  schema=Array.from(options.schema||[]).map(item=>typeof item.toJSON==='function'?item.toJSON():{...item});
@@ -130,6 +140,14 @@
130
140
  }
131
141
  function setStatus(message=''){status.innerText=String(message||'');return status.innerText;}
132
142
 
143
+ function destroy(){
144
+ if(destroyed)return false;
145
+ destroyed=true;
146
+ host.ready=false;
147
+ events.dispose();
148
+ return true;
149
+ }
150
+
133
151
  host.ready=true;
134
- host.dispatchEvent(new CustomEvent('preferences-form-ready',{bubbles:true,composed:true}));
152
+ emit('preferences-form-ready',{}, {ready:true});
135
153
  </script>
@@ -28,18 +28,24 @@
28
28
  const host=this;
29
29
  const list=host.shadowRoot.querySelector('[data-list]');
30
30
  const empty=host.shadowRoot.querySelector('[data-empty]');
31
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
32
+ const events=createArcaneEventSource(host,{source:'arcane.component.record-timeline',eventTypes:['record-timeline-open','record-timeline-ready']});
33
+ let destroyed=false;
31
34
  let items=[];
32
35
  let options={emptyLabel:'No dated events match this view.'};
33
36
 
34
37
  host.setItems=setItems;
35
38
  host.populate=(value,input={})=>setItems(value,input);
39
+ host.destroy=destroy;
36
40
 
37
41
  list.addEventListener('click',event=>{
42
+ if(destroyed) return;
38
43
  const trigger=event.target.closest('[data-event-id]');
39
44
  if(!trigger) return;
40
45
  const item=items.find(candidate=>candidate.id===trigger.dataset.eventId);
41
46
  if(!item) return;
42
- host.dispatchEvent(new CustomEvent('record-timeline-open',{bubbles:true,composed:true,detail:{id:item.id,item:{...item}}}));
47
+ const publication=events.dispatch('record-timeline-open',{id:item.id,item:{...item}},{publicDetail:{category:String(item.category||'')}});
48
+ projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
43
49
  });
44
50
 
45
51
  function normalizeItem(item,index){
@@ -101,5 +107,15 @@
101
107
  }
102
108
  }
103
109
 
104
- host.dispatchEvent(new CustomEvent('record-timeline-ready',{bubbles:true,composed:true}));
110
+ function destroy(){
111
+ if(destroyed) return false;
112
+ destroyed=true;
113
+ host.ready=false;
114
+ events.dispose();
115
+ return true;
116
+ }
117
+
118
+ host.ready=true;
119
+ const readyPublication=events.dispatch('record-timeline-ready',{}, {publicDetail:{ready:true}});
120
+ projectArcaneDOMEvent(host,readyPublication.occurrence,{bubbles:true,composed:true});
105
121
  </script>
@@ -34,23 +34,33 @@
34
34
  const selectedTitle=host.shadowRoot.querySelector('[data-selected-title]');
35
35
  const selectedSummary=host.shadowRoot.querySelector('[data-selected-summary]');
36
36
  const edgeList=host.shadowRoot.querySelector('[data-edges]');
37
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
38
+ const events=createArcaneEventSource(host,{source:'arcane.component.relationship-board',eventTypes:['relationship-node-open','relationship-edge-open','relationship-board-ready']});
39
+ let destroyed=false;
37
40
  let nodes=[]; let edges=[]; let lanes=[]; let selectedId='';
38
41
 
39
42
  host.setGraph=setGraph;
40
43
  host.populate=(graph={},options={})=>setGraph(graph,options);
44
+ host.destroy=destroy;
41
45
 
42
46
  board.addEventListener('click',event=>{
47
+ if(destroyed) return;
43
48
  const trigger=event.target.closest('[data-node-id]');
44
49
  if(!trigger) return;
45
50
  selectedId=trigger.dataset.nodeId; renderSelection();
46
51
  const node=nodes.find(item=>item.id===selectedId);
47
- host.dispatchEvent(new CustomEvent('relationship-node-open',{bubbles:true,composed:true,detail:{id:selectedId,node:{...node}}}));
52
+ const publication=events.dispatch('relationship-node-open',{id:selectedId,node:{...node}},{publicDetail:{lane:String(node?.lane||'')}});
53
+ projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
48
54
  });
49
55
  edgeList.addEventListener('click',event=>{
56
+ if(destroyed) return;
50
57
  const trigger=event.target.closest('[data-edge-id]');
51
58
  if(!trigger) return;
52
59
  const edge=edges.find(item=>item.id===trigger.dataset.edgeId);
53
- if(edge) host.dispatchEvent(new CustomEvent('relationship-edge-open',{bubbles:true,composed:true,detail:{id:edge.id,edge:{...edge}}}));
60
+ if(edge){
61
+ const publication=events.dispatch('relationship-edge-open',{id:edge.id,edge:{...edge}},{publicDetail:{hasEdge:true}});
62
+ projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
63
+ }
54
64
  });
55
65
 
56
66
  function normalizeNode(value,index){
@@ -112,5 +122,15 @@
112
122
  if(!related.length){const row=document.createElement('li'); row.className='empty'; row.textContent='No displayed connections.'; edgeList.append(row);}
113
123
  }
114
124
 
115
- host.dispatchEvent(new CustomEvent('relationship-board-ready',{bubbles:true,composed:true}));
125
+ function destroy(){
126
+ if(destroyed) return false;
127
+ destroyed=true;
128
+ host.ready=false;
129
+ events.dispose();
130
+ return true;
131
+ }
132
+
133
+ host.ready=true;
134
+ const readyPublication=events.dispatch('relationship-board-ready',{}, {publicDetail:{ready:true}});
135
+ projectArcaneDOMEvent(host,readyPublication.occurrence,{bubbles:true,composed:true});
116
136
  </script>