arcane-os 0.2.3 → 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 +17 -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 +20 -20
  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 +2 -2
  21. package/docs/reference/behavioral-testing.md +28 -5
  22. package/docs/reference/cli.md +115 -7
  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 +86 -37
  27. package/docs/reference/inventory/runtime-modules.json +126 -49
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +143 -33
  30. package/docs/reference/runtime-components.md +192 -80
  31. package/docs/reference/runtime-modules.md +584 -70
  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 +453 -131
  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 +244 -72
  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 +235 -37
  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
@@ -40,13 +40,17 @@
40
40
  <script type="module">
41
41
  const host=this;
42
42
  const root=host.shadowRoot;
43
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
44
+ const events=createArcaneEventSource(host,{source:'arcane.component.summary-strip',eventTypes:['summary-strip-change','summary-strip-select','summary-strip-ready']});
43
45
  const region=root.querySelector('#region');
44
46
  const list=root.querySelector('#items');
47
+ let destroyed=false;
45
48
  let items=[];
46
49
  let options={};
47
50
 
48
51
  host.clear=clear;
49
52
  host.configure=configure;
53
+ host.destroy=destroy;
50
54
  host.setItems=setItems;
51
55
  host.updateItem=updateItem;
52
56
 
@@ -57,6 +61,12 @@
57
61
 
58
62
  list.addEventListener('click',selectItem);
59
63
 
64
+ function emit(type,detail={},publicDetail={}){
65
+ if(destroyed)return false;
66
+ const publication=events.dispatch(type,detail,{publicDetail});
67
+ return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
68
+ }
69
+
60
70
  function datasetOptions(){
61
71
  return {
62
72
  ariaLabel:host.dataset.ariaLabel||'Summary',
@@ -74,11 +84,7 @@
74
84
  function setItems(values=[]){
75
85
  items=Array.from(values||[]).map((value,index)=>normalizeItem(value,index));
76
86
  render();
77
- host.dispatchEvent(new CustomEvent('summary-strip-change',{
78
- bubbles:true,
79
- composed:true,
80
- detail:{items:getItems()}
81
- }));
87
+ emit('summary-strip-change',{items:getItems()},{itemCount:items.length});
82
88
  return getItems();
83
89
  }
84
90
 
@@ -156,11 +162,7 @@
156
162
  if(!target||!item||item.disabled){
157
163
  return;
158
164
  }
159
- host.dispatchEvent(new CustomEvent('summary-strip-select',{
160
- bubbles:true,
161
- composed:true,
162
- detail:{id:item.id,item:{...item}}
163
- }));
165
+ emit('summary-strip-select',{id:item.id,item:{...item}},{id:item.id,status:item.status});
164
166
  }
165
167
 
166
168
  function getItems(){
@@ -171,7 +173,16 @@
171
173
  return setItems([]);
172
174
  }
173
175
 
176
+ function destroy(){
177
+ if(destroyed)return false;
178
+ destroyed=true;
179
+ host.ready=false;
180
+ list.removeEventListener('click',selectItem);
181
+ events.dispose();
182
+ return true;
183
+ }
184
+
174
185
  configure(datasetOptions());
175
186
  host.ready=true;
176
- host.dispatchEvent(new CustomEvent('summary-strip-ready',{bubbles:true,composed:true}));
187
+ emit('summary-strip-ready',{}, {ready:true});
177
188
  </script>
@@ -16,32 +16,39 @@
16
16
  </tbody>
17
17
  </table>
18
18
 
19
- <script typr="module">
19
+ <script type="module">
20
20
  const host = this;
21
21
  const shadowRoot=this.shadowRoot;
22
+ const {createArcaneEventSource,projectArcaneDOMEvent}=
23
+ await import('arcane-os/event-manager');
24
+ const events=createArcaneEventSource(host,{
25
+ source:'arcane.component.table',
26
+ eventTypes:['table-ready','header-update','body-update']
27
+ });
28
+ let destroyed=false;
29
+ let eventOperationSequence=0;
22
30
  host.buildHeader=buildHeader;
23
31
  host.buildTable=buildTable;
32
+ host.destroy=destroy;
24
33
  host.ready=false;
25
34
 
26
- const tableReady=new CustomEvent(
27
- 'table-ready', {
28
- detail: { }
29
- }
30
- );
31
-
32
- const headerUpdate=new CustomEvent(
33
- 'header-update', {
34
- detail: { }
35
- }
36
- );
37
-
38
- const bodyUpdate=new CustomEvent(
39
- 'body-update', {
40
- detail: { }
41
- }
42
- );
35
+ function emit(type,detail={},publicDetail={}){
36
+ if(destroyed)return false;
37
+ const operationId=
38
+ `${events.instanceId}:${type}:${(++eventOperationSequence).toString(36)}`;
39
+ const publication=events.dispatch(
40
+ type,
41
+ detail,
42
+ {
43
+ operationId,
44
+ publicDetail:{...publicDetail,reason:type}
45
+ }
46
+ );
47
+ return projectArcaneDOMEvent(host,publication.occurrence);
48
+ }
43
49
 
44
50
  async function buildHeader(headerArray=['','']){
51
+ if(destroyed)return false;
45
52
  const tableHead = shadowRoot.querySelector('thead');
46
53
  tableHead.innerHTML = '';
47
54
  const th = tableHead.insertRow();
@@ -51,10 +58,12 @@
51
58
  th.insertCell(-1).innerText=header;
52
59
  }
53
60
  );
54
- host.dispatchEvent(headerUpdate);
61
+ emit('header-update',{}, {columnCount:headerArray.length});
62
+ return true;
55
63
  }
56
64
 
57
65
  async function buildTable(tableArray=[['', ''], ['','']]){
66
+ if(destroyed)return false;
58
67
  const tableBody = shadowRoot.querySelector('tbody');
59
68
  tableBody.innerHTML = '';
60
69
 
@@ -68,10 +77,19 @@
68
77
  );
69
78
  }
70
79
  );
71
- host.dispatchEvent(bodyUpdate);
80
+ emit('body-update',{}, {rowCount:tableArray.length});
81
+ return true;
82
+ }
83
+
84
+ function destroy(){
85
+ if(destroyed)return false;
86
+ destroyed=true;
87
+ host.ready=false;
88
+ events.dispose();
89
+ return true;
72
90
  }
73
91
 
74
92
  host.ready=true;
75
- host.dispatchEvent(tableReady);
93
+ emit('table-ready',{}, {ready:true});
76
94
 
77
95
  </script>
@@ -47,6 +47,8 @@
47
47
  <script type="module">
48
48
  const host=this;
49
49
  const root=host.shadowRoot;
50
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
51
+ const events=createArcaneEventSource(host,{source:'arcane.component.task-progress',eventTypes:['task-progress-started','task-progress-error','task-progress-complete','task-progress-change','task-progress-ready']});
50
52
  const panel=root.querySelector('#panel');
51
53
  const heading=root.querySelector('#taskProgressTitle');
52
54
  const description=root.querySelector('#description');
@@ -58,12 +60,26 @@
58
60
 
59
61
  const allowedStatuses=new Set(['pending','running','complete','failed','canceled']);
60
62
  let activeRun=Promise.resolve([]);
63
+ let destroyed=false;
64
+ let eventOperationSequence=0;
61
65
  let options={};
66
+ let runGeneration=0;
62
67
  let running=false;
63
68
  let tasks=[];
64
69
 
70
+ function emit(type,detail={},publicDetail={},operationId=null){
71
+ if(destroyed)return false;
72
+ const publication=events.dispatch(
73
+ type,
74
+ detail,
75
+ {publicDetail,operationId}
76
+ );
77
+ return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
78
+ }
79
+
65
80
  host.clear=clear;
66
81
  host.configure=configure;
82
+ host.destroy=destroy;
67
83
  host.runTasks=runTasks;
68
84
  host.setTasks=setTasks;
69
85
  host.updateTask=updateTask;
@@ -92,6 +108,7 @@
92
108
  }
93
109
 
94
110
  function setTasks(values=[]){
111
+ if(destroyed)return [];
95
112
  const ids=new Set();
96
113
  tasks=Array.from(values||[]).map((value,index)=>{
97
114
  const task=normalizeTask(value,index);
@@ -117,6 +134,7 @@
117
134
  }
118
135
 
119
136
  function updateTask(id,update={}){
137
+ if(destroyed)return null;
120
138
  const index=typeof id==='number'?id:tasks.findIndex(task=>task.id===`${id}`);
121
139
  if(index<0||index>=tasks.length){
122
140
  return null;
@@ -181,6 +199,7 @@
181
199
  }
182
200
 
183
201
  async function runTasks(title='Progress',jobs=[]){
202
+ if(destroyed)return [];
184
203
  if(running){
185
204
  return activeRun;
186
205
  }
@@ -197,36 +216,60 @@
197
216
  status:'pending'
198
217
  })));
199
218
  running=true;
219
+ const generation=++runGeneration;
220
+ const operationId=
221
+ `${events.instanceId}:task-run:${(++eventOperationSequence).toString(36)}`;
200
222
  syncOverall();
201
- host.dispatchEvent(new CustomEvent('task-progress-started',{
202
- bubbles:true,
203
- composed:true,
204
- detail:getSnapshot()
205
- }));
223
+ emit(
224
+ 'task-progress-started',
225
+ getSnapshot(),
226
+ {
227
+ reason:'task-run-started',
228
+ taskCount:tasks.length,
229
+ running:true
230
+ },
231
+ operationId
232
+ );
206
233
  activeRun=Promise.allSettled(runnable.map(async(job,index)=>{
207
234
  updateTask(index,{status:'running'});
208
235
  try{
209
236
  const value=await job.task();
237
+ if(destroyed||generation!==runGeneration)return value;
210
238
  updateTask(index,{status:'complete',message:job.completeMessage||''});
211
239
  return value;
212
240
  }catch(error){
241
+ if(destroyed||generation!==runGeneration)throw error;
213
242
  updateTask(index,{status:'failed',message:job.errorMessage||error?.message||'Unable to complete.'});
214
- host.dispatchEvent(new CustomEvent('task-progress-error',{
215
- bubbles:true,
216
- composed:true,
217
- detail:{error,id:tasks[index]?.id,index,task:{...tasks[index]}}
218
- }));
243
+ emit(
244
+ 'task-progress-error',
245
+ {error,id:tasks[index]?.id,index,task:{...tasks[index]}},
246
+ {
247
+ code:typeof error?.code==='string'
248
+ ?error.code
249
+ :'ARCANE_TASK_EXECUTION_REJECTED',
250
+ index,
251
+ reason:'task-execution-rejected'
252
+ },
253
+ operationId
254
+ );
219
255
  throw error;
220
256
  }
221
257
  }));
222
258
  const results=await activeRun;
223
259
  running=false;
260
+ if(destroyed||generation!==runGeneration)return results;
224
261
  syncOverall();
225
- host.dispatchEvent(new CustomEvent('task-progress-complete',{
226
- bubbles:true,
227
- composed:true,
228
- detail:{...getSnapshot(),results:results.slice()}
229
- }));
262
+ emit(
263
+ 'task-progress-complete',
264
+ {...getSnapshot(),results:results.slice()},
265
+ {
266
+ reason:'task-run-settled',
267
+ taskCount:tasks.length,
268
+ rejectedCount:results.filter(result=>result.status==='rejected').length,
269
+ running:false
270
+ },
271
+ operationId
272
+ );
230
273
  return results;
231
274
  }
232
275
 
@@ -258,14 +301,20 @@
258
301
  }
259
302
 
260
303
  function emitChange(task=null){
261
- host.dispatchEvent(new CustomEvent('task-progress-change',{
262
- bubbles:true,
263
- composed:true,
264
- detail:{...getSnapshot(),task:task?{...task}:null}
265
- }));
304
+ emit(
305
+ 'task-progress-change',
306
+ {...getSnapshot(),task:task?{...task}:null},
307
+ {
308
+ taskCount:tasks.length,
309
+ running,
310
+ reason:'task-state-changed',
311
+ taskStatus:task?.status?`task-${task.status}`:null
312
+ }
313
+ );
266
314
  }
267
315
 
268
316
  function clear(){
317
+ if(destroyed)return false;
269
318
  if(running){
270
319
  return false;
271
320
  }
@@ -275,8 +324,17 @@
275
324
  return true;
276
325
  }
277
326
 
327
+ function destroy(){
328
+ if(destroyed)return false;
329
+ destroyed=true;
330
+ runGeneration+=1;
331
+ host.ready=false;
332
+ events.dispose();
333
+ return true;
334
+ }
335
+
278
336
  configure(datasetOptions());
279
337
  render();
280
338
  host.ready=true;
281
- host.dispatchEvent(new CustomEvent('task-progress-ready',{bubbles:true,composed:true}));
339
+ emit('task-progress-ready',{}, {ready:true,reason:'task-progress-ready'});
282
340
  </script>
@@ -27,10 +27,12 @@
27
27
  </section>
28
28
  <script type="module">
29
29
  const {parseAnsi,stripAnsi}=await import('../modules/AnsiText.js');
30
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
30
31
  const host=this,root=host.shadowRoot;
31
32
  const tabs=root.querySelector('#tabs'),output=root.querySelector('#output'),input=root.querySelector('#input'),prompt=root.querySelector('#prompt'),promptLabel=root.querySelector('#promptLabel'),connection=root.querySelector('#connection'),search=root.querySelector('#search');
32
- const sessions=new Map();let activeId='',history=[],historyIndex=0,completions=[];
33
- host.configure=configure;host.addSession=addSession;host.removeSession=removeSession;host.activateSession=activateSession;host.append=append;host.clear=clear;host.setState=setState;host.setTheme=setTheme;host.focus=()=>input.focus();host.ready=true;
33
+ const events=createArcaneEventSource(host,{source:'arcane.component.terminal-workspace',eventTypes:['terminal-interrupt','terminal-settings','terminal-session-select','terminal-clear','terminal-submit','terminal-session-close','terminal-session-new','terminal-workspace-ready']});
34
+ const sessions=new Map();let activeId='',history=[],historyIndex=0,completions=[],destroyed=false,eventOperationSequence=0;
35
+ host.configure=configure;host.destroy=destroy;host.addSession=addSession;host.removeSession=removeSession;host.activateSession=activateSession;host.append=append;host.clear=clear;host.setState=setState;host.setTheme=setTheme;host.focus=()=>input.focus();host.ready=true;
34
36
  prompt.addEventListener('submit',event=>{event.preventDefault();submit();});
35
37
  input.addEventListener('keydown',event=>{
36
38
  if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();submit();}
@@ -60,6 +62,7 @@
60
62
  function renderText(row,text,term){const clean=stripAnsi(text);if(!term){for(const token of parseAnsi(text)){const span=document.createElement('span');span.textContent=token.text;const classes=[];if(token.style.bold)classes.push('ansi-bold');if(token.style.dim)classes.push('ansi-dim');if(token.style.italic)classes.push('ansi-italic');if(token.style.underline)classes.push('ansi-underline');if(token.style.foreground&&!token.style.foreground.startsWith('index-'))classes.push(`ansi-${token.style.foreground}`);span.className=classes.join(' ');row.append(span);}return;}let cursor=0,index=clean.toLowerCase().indexOf(term);while(index>=0){row.append(document.createTextNode(clean.slice(cursor,index)));const mark=document.createElement('mark');mark.textContent=clean.slice(index,index+term.length);row.append(mark);cursor=index+term.length;index=clean.toLowerCase().indexOf(term,cursor);}row.append(document.createTextNode(clean.slice(cursor)));}
61
63
  function toggleSearch(force){search.hidden=force===undefined?!search.hidden:!force;if(!search.hidden)search.focus();else{search.value='';render();input.focus();}}
62
64
  function resizeInput(){input.style.height='auto';input.style.height=`${Math.min(input.scrollHeight,160)}px`;}
63
- function emit(type,detail={}){host.dispatchEvent(new CustomEvent(type,{bubbles:true,composed:true,detail}));}
64
- host.dispatchEvent(new CustomEvent('terminal-workspace-ready',{bubbles:true,composed:true}));
65
+ function emit(type,detail={}){if(destroyed)return false;const operationId=`${events.instanceId}:${type}:${(++eventOperationSequence).toString(36)}`;const publication=events.dispatch(type,detail,{operationId,publicDetail:{hasSession:Boolean(detail.sessionId),lineLength:typeof detail.line==='string'?detail.line.length:0,historyCount:Array.isArray(detail.history)?detail.history.length:0,reason:type}});return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});}
66
+ function destroy(){if(destroyed)return false;destroyed=true;host.ready=false;for(const id of ['find','clear','interrupt','settings','copy'])root.querySelector(`#${id}`).onclick=null;events.dispose();return true;}
67
+ emit('terminal-workspace-ready');
65
68
  </script>
@@ -21,8 +21,11 @@
21
21
  const colors=root.querySelector('#colors');
22
22
  const status=root.querySelector('#status');
23
23
  const {default:Theme,themeColorToHex,themeTokens}=await import('../entities/Theme.js');
24
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
25
+ const events=createArcaneEventSource(host,{source:'arcane.component.theme-editor',eventTypes:['theme-preview','theme-save','theme-reset','theme-editor-ready']});
26
+ let destroyed=false;
24
27
 
25
- host.configure=configure;host.getTheme=getTheme;host.setTheme=setTheme;host.setBusy=setBusy;host.setStatus=setStatus;
28
+ host.configure=configure;host.destroy=destroy;host.getTheme=getTheme;host.setTheme=setTheme;host.setBusy=setBusy;host.setStatus=setStatus;
26
29
  colors.replaceChildren(...themeTokens.map(token=>{
27
30
  const field=document.createElement('div'),input=document.createElement('input'),label=document.createElement('label');
28
31
  field.className='color-field';input.type='color';input.id=`theme-${token.key}`;input.name=token.key;input.value=themeColorToHex(token.defaultValue);label.htmlFor=input.id;label.textContent=token.label;field.append(input,label);return field;
@@ -31,11 +34,12 @@
31
34
  form.addEventListener('submit',event=>{event.preventDefault();emit('theme-save',{theme:getTheme()});});
32
35
  root.querySelector('#reset').addEventListener('click',()=>emit('theme-reset'));
33
36
 
34
- function emit(name,detail={}){host.dispatchEvent(new CustomEvent(name,{bubbles:true,composed:true,detail}));}
37
+ function emit(name,detail={}){if(destroyed)return false;const theme=detail.theme;const publication=events.dispatch(name,detail,{publicDetail:theme?{scheme:String(theme.scheme||''),tokenCount:Object.keys(theme.tokens||{}).length}:{}});return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});}
35
38
  function getTheme(){return new Theme({name:form.elements.name.value,scheme:form.elements.scheme.value,tokens:Object.fromEntries(themeTokens.map(token=>[token.key,form.elements[token.key].value]))});}
36
39
  function setTheme(input={}){const theme=Theme.fromJSON(input);form.elements.name.value=theme.name;form.elements.scheme.value=theme.scheme;for(const token of themeTokens) form.elements[token.key].value=themeColorToHex(theme.tokens[token.key]);return theme;}
37
40
  function configure(options={}){return setTheme(options.theme||{name:'My Arcane skin'});}
38
41
  function setBusy(value=true){for(const control of form.elements) control.disabled=Boolean(value);form.setAttribute('aria-busy',String(Boolean(value)));}
39
42
  function setStatus(value=''){status.textContent=String(value||'');return status.textContent;}
40
- configure();host.ready=true;emit('theme-editor-ready');
43
+ function destroy(){if(destroyed)return false;destroyed=true;host.ready=false;events.dispose();return true;}
44
+ configure();host.ready=true;const readyPublication=events.dispatch('theme-editor-ready',{}, {publicDetail:{ready:true}});projectArcaneDOMEvent(host,readyPublication.occurrence,{bubbles:true,composed:true});
41
45
  </script>
@@ -8,13 +8,18 @@
8
8
  <div id="empty" class="empty"><strong>No conversations yet</strong><p>Connect a service in settings to bring conversations into this inbox.</p></div>
9
9
  </section>
10
10
  <script type="module">
11
- const host=this,root=host.shadowRoot,list=root.querySelector('#list'),empty=root.querySelector('#empty'),search=root.querySelector('#search'),filters=root.querySelector('#filters');let threads=[],activeId='',channel='all',providers={};
11
+ const host=this,root=host.shadowRoot,list=root.querySelector('#list'),empty=root.querySelector('#empty'),search=root.querySelector('#search'),filters=root.querySelector('#filters');
12
+ const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
13
+ const events=createArcaneEventSource(host,{source:'arcane.component.unified-inbox',eventTypes:['inbox-refresh','thread-select','unified-inbox-ready']});
14
+ let threads=[],activeId='',channel='all',providers={},destroyed=false;
15
+ function emit(type,detail={},publicDetail={}){if(destroyed)return false;const publication=events.dispatch(type,detail,{publicDetail});return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true})}
16
+ host.destroy=()=>{if(destroyed)return false;destroyed=true;host.ready=false;events.dispose();return true};
12
17
  host.configure=options=>{providers=Object.fromEntries(Array.from(options?.providers||[],item=>[item.id,item]));renderFilters(options?.channels);return host.setThreads(options?.threads||[])};
13
18
  host.setThreads=values=>{threads=Array.from(values||[]);render();return threads};host.setActive=id=>{activeId=String(id||'');render();};host.setLoading=value=>{root.querySelector('#refresh').disabled=Boolean(value)};
14
- root.querySelector('#refresh').addEventListener('click',()=>host.dispatchEvent(new CustomEvent('inbox-refresh',{bubbles:true,composed:true})));
19
+ root.querySelector('#refresh').addEventListener('click',()=>emit('inbox-refresh'));
15
20
  search.addEventListener('input',render);
16
21
  function renderFilters(values){const names=['all',...new Set(values||[])];filters.replaceChildren(...names.map(name=>{const button=document.createElement('button');button.type='button';button.className='arcane-button arcane-button--tertiary';button.textContent=name==='all'?'All':name.toUpperCase();button.setAttribute('aria-pressed',String(channel===name));button.addEventListener('click',()=>{channel=name;renderFilters(values);render()});return button}))}
17
22
  function render(){const term=search.value.trim().toLowerCase();const visible=threads.filter(item=>(channel==='all'||item.channel===channel)&&(!term||`${item.title} ${item.preview} ${(item.participants||[]).join(' ')}`.toLowerCase().includes(term)));empty.hidden=visible.length>0;list.replaceChildren(...visible.map(makeThread));}
18
- function makeThread(item){const button=document.createElement('button');button.type='button';button.className='thread';button.setAttribute('aria-current',String(item.id===activeId));const top=document.createElement('span'),title=document.createElement('span'),time=document.createElement('time'),preview=document.createElement('span'),meta=document.createElement('span'),badge=document.createElement('span'),provider=document.createElement('span');top.className='line';title.className='title';title.textContent=item.title;time.dateTime=item.updatedAt;time.textContent=new Intl.DateTimeFormat(undefined,{month:'short',day:'numeric'}).format(new Date(item.updatedAt));preview.className='preview';preview.textContent=item.preview||'No preview';meta.className='meta';badge.className='badge';badge.textContent=item.channel;provider.textContent=providers[item.providerId]?.label||item.providerId;top.append(title,time);meta.append(badge,provider);if(item.unreadCount){const unread=document.createElement('span');unread.className='unread';unread.textContent=String(item.unreadCount);meta.append(unread)}button.append(top,preview,meta);button.addEventListener('click',()=>host.dispatchEvent(new CustomEvent('thread-select',{bubbles:true,composed:true,detail:{thread:item}})));return button;}
19
- host.ready=true;host.dispatchEvent(new CustomEvent('unified-inbox-ready',{bubbles:true,composed:true}));
23
+ function makeThread(item){const button=document.createElement('button');button.type='button';button.className='thread';button.setAttribute('aria-current',String(item.id===activeId));const top=document.createElement('span'),title=document.createElement('span'),time=document.createElement('time'),preview=document.createElement('span'),meta=document.createElement('span'),badge=document.createElement('span'),provider=document.createElement('span');top.className='line';title.className='title';title.textContent=item.title;time.dateTime=item.updatedAt;time.textContent=new Intl.DateTimeFormat(undefined,{month:'short',day:'numeric'}).format(new Date(item.updatedAt));preview.className='preview';preview.textContent=item.preview||'No preview';meta.className='meta';badge.className='badge';badge.textContent=item.channel;provider.textContent=providers[item.providerId]?.label||item.providerId;top.append(title,time);meta.append(badge,provider);if(item.unreadCount){const unread=document.createElement('span');unread.className='unread';unread.textContent=String(item.unreadCount);meta.append(unread)}button.append(top,preview,meta);button.addEventListener('click',()=>emit('thread-select',{thread:item},{threadId:String(item.id||''),channel:String(item.channel||'')}));return button;}
24
+ host.ready=true;emit('unified-inbox-ready',{}, {ready:true});
20
25
  </script>