arcane-os 0.3.0 → 0.3.2

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 (153) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
  5. package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -146
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1050 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +40 -62
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +112 -779
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -187
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1252
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -677
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2960
  150. package/docs/reference/sdk-api.md +0 -6694
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -232,47 +232,6 @@
232
232
  const contentElement=host.shadowRoot.querySelector('#content');
233
233
 
234
234
  // BEGIN MARKDOWN_DOCUMENT_POLICY
235
- const MARKDOWN_DOCUMENT_ALLOWED_ELEMENTS=Object.freeze([
236
- 'A','ABBR','BLOCKQUOTE','BR','CODE','DEL','EM',
237
- 'H1','H2','H3','H4','H5','H6','HR','IMG','KBD','LI',
238
- 'OL','P','PRE','S','STRONG','SUB','SUP','TABLE','TBODY',
239
- 'TD','TFOOT','TH','THEAD','TR','UL'
240
- ]);
241
- const MARKDOWN_DOCUMENT_ALLOWED_ATTRIBUTES=Object.freeze(
242
- {
243
- A:Object.freeze(['href','title']),
244
- ABBR:Object.freeze(['title']),
245
- CODE:Object.freeze(['class']),
246
- IMG:Object.freeze(['alt','src','title']),
247
- LI:Object.freeze(['value']),
248
- OL:Object.freeze(['start']),
249
- TD:Object.freeze(['colspan','rowspan']),
250
- TH:Object.freeze(['colspan','rowspan','scope'])
251
- }
252
- );
253
- const MARKDOWN_DOCUMENT_LINK_PROTOCOLS=new Set(['http:','https:','mailto:']);
254
- const MARKDOWN_DOCUMENT_ASSET_PROTOCOLS=new Set(['http:','https:']);
255
-
256
- function markdownDocumentElementAllowed(name=''){
257
- return MARKDOWN_DOCUMENT_ALLOWED_ELEMENTS.includes(
258
- String(name||'').toUpperCase()
259
- );
260
- }
261
-
262
- function markdownDocumentAllowedAttributes(name=''){
263
- return MARKDOWN_DOCUMENT_ALLOWED_ATTRIBUTES[
264
- String(name||'').toUpperCase()
265
- ]||Object.freeze([]);
266
- }
267
-
268
- function markdownDocumentCleanURLInput(value=''){
269
- const input=String(value??'').trim();
270
- if(!input||/[\u0000-\u001F\u007F]/.test(input)){
271
- return '';
272
- }
273
- return input;
274
- }
275
-
276
235
  function markdownDocumentDecodeFragment(value=''){
277
236
  try{
278
237
  return decodeURIComponent(String(value||''));
@@ -281,106 +240,58 @@
281
240
  }
282
241
  }
283
242
 
284
- function markdownDocumentSourceURL(value='',baseURL='',pageOrigin=''){
243
+ function markdownDocumentSourceURL(value='',baseURL=''){
285
244
  const fallback=baseURL||document.baseURI;
286
- const expectedOrigin=pageOrigin
287
- ||globalThis.location?.origin
288
- ||new URL(fallback).origin;
289
- const input=markdownDocumentCleanURLInput(value||fallback);
290
- if(!input){
291
- return '';
292
- }
293
-
294
245
  try{
295
- const url=new URL(input,fallback);
296
- if(
297
- !MARKDOWN_DOCUMENT_ASSET_PROTOCOLS.has(url.protocol)
298
- ||url.origin!==expectedOrigin
299
- ||url.username
300
- ||url.password
301
- ){
302
- return '';
303
- }
304
- return url.href;
246
+ return new URL(String(value||fallback),fallback).href;
305
247
  }catch{
306
248
  return '';
307
249
  }
308
250
  }
309
251
 
310
- function markdownDocumentLinkPolicy(value='',sourceURL='',pageOrigin=''){
311
- const input=markdownDocumentCleanURLInput(value);
252
+ function markdownDocumentLinkPolicy(value='',sourceURL=''){
253
+ const input=String(value??'');
312
254
  if(!input){
313
255
  return null;
314
256
  }
315
257
 
316
- const expectedOrigin=pageOrigin
317
- ||globalThis.location?.origin
318
- ||new URL(sourceURL).origin;
319
-
320
258
  if(input.startsWith('#')){
321
- const fragment=markdownDocumentDecodeFragment(input.slice(1))
322
- .replace(/[\u0000-\u001F\u007F]/g,'')
323
- .slice(0,256);
259
+ const fragment=markdownDocumentDecodeFragment(input.substring(1));
324
260
  if(!fragment){
325
261
  return null;
326
262
  }
327
- return Object.freeze(
328
- {
329
- external:false,
330
- fragment,
331
- href:`#${encodeURIComponent(fragment)}`,
332
- kind:'fragment',
333
- targetURL:new URL(`#${encodeURIComponent(fragment)}`,sourceURL).href
334
- }
335
- );
263
+ return {
264
+ external:false,
265
+ fragment,
266
+ href:input,
267
+ kind:'fragment',
268
+ targetURL:new URL(input,sourceURL).href
269
+ };
336
270
  }
337
271
 
338
272
  try{
339
273
  const url=new URL(input,sourceURL);
340
- if(
341
- !MARKDOWN_DOCUMENT_LINK_PROTOCOLS.has(url.protocol)
342
- ||url.username
343
- ||url.password
344
- ){
345
- return null;
346
- }
347
-
348
- const sameOrigin=url.origin===expectedOrigin;
274
+ const sameOrigin=url.origin===new URL(sourceURL).origin;
349
275
  const markdown=sameOrigin&&/\.(?:md|markdown)$/i.test(url.pathname);
350
- return Object.freeze(
351
- {
352
- external:!sameOrigin,
353
- fragment:markdownDocumentDecodeFragment(url.hash.slice(1)).slice(0,256),
354
- href:url.href,
355
- kind:markdown?'markdown':'link',
356
- targetURL:url.href
357
- }
358
- );
276
+ return {
277
+ external:!sameOrigin,
278
+ fragment:markdownDocumentDecodeFragment(url.hash.substring(1)),
279
+ href:url.href,
280
+ kind:markdown?'markdown':'link',
281
+ targetURL:url.href
282
+ };
359
283
  }catch{
360
284
  return null;
361
285
  }
362
286
  }
363
287
 
364
- function markdownDocumentAssetURL(value='',sourceURL='',pageOrigin=''){
365
- const input=markdownDocumentCleanURLInput(value);
288
+ function markdownDocumentAssetURL(value='',sourceURL=''){
289
+ const input=String(value??'');
366
290
  if(!input){
367
291
  return '';
368
292
  }
369
-
370
- const expectedOrigin=pageOrigin
371
- ||globalThis.location?.origin
372
- ||new URL(sourceURL).origin;
373
293
  try{
374
- const url=new URL(input,sourceURL);
375
- if(
376
- !MARKDOWN_DOCUMENT_ASSET_PROTOCOLS.has(url.protocol)
377
- ||url.origin!==expectedOrigin
378
- ||url.username
379
- ||url.password
380
- ){
381
- return '';
382
- }
383
- return url.href;
294
+ return new URL(input,sourceURL).href;
384
295
  }catch{
385
296
  return '';
386
297
  }
@@ -393,8 +304,7 @@
393
304
  .toLowerCase()
394
305
  .replace(/&/g,' and ')
395
306
  .replace(/[^a-z0-9]+/g,'-')
396
- .replace(/^-+|-+$/g,'')
397
- .slice(0,80);
307
+ .replace(/^-+|-+$/g,'');
398
308
  return identifier||'section';
399
309
  }
400
310
 
@@ -416,22 +326,18 @@
416
326
  document.baseURI,
417
327
  pageOrigin
418
328
  );
419
- const defaultOptions=Object.freeze(
420
- {
421
- labels:Object.freeze(
422
- {
423
- content:'Document content',
424
- empty:'No document content is available.',
425
- error:'Unable to display this document.',
426
- loading:'Loading document…',
427
- tableOfContents:'On this page'
428
- }
429
- ),
430
- onNavigate:null,
431
- showTableOfContents:true,
432
- sourceURL:defaultSourceURL
433
- }
434
- );
329
+ const defaultOptions={
330
+ labels:{
331
+ content:'Document content',
332
+ empty:'No document content is available.',
333
+ error:'Unable to display this document.',
334
+ loading:'Loading document',
335
+ tableOfContents:'On this page'
336
+ },
337
+ onNavigate:null,
338
+ showTableOfContents:true,
339
+ sourceURL:defaultSourceURL
340
+ };
435
341
 
436
342
  let currentMarkdown='';
437
343
  let currentSourceURL=defaultSourceURL;
@@ -503,7 +409,7 @@
503
409
  pageOrigin
504
410
  );
505
411
  if(!sourceURL){
506
- throw new TypeError('Markdown document sourceURL must resolve to a same-origin HTTP(S) URL.');
412
+ throw new TypeError('Markdown document sourceURL must resolve to a valid URL.');
507
413
  }
508
414
 
509
415
  const onNavigate=input.onNavigate===undefined
@@ -520,14 +426,12 @@
520
426
  throw new TypeError('Markdown document showTableOfContents must be boolean.');
521
427
  }
522
428
 
523
- return Object.freeze(
524
- {
525
- labels:Object.freeze(labels),
526
- onNavigate,
527
- showTableOfContents,
528
- sourceURL
529
- }
530
- );
429
+ return {
430
+ labels,
431
+ onNavigate,
432
+ showTableOfContents,
433
+ sourceURL
434
+ };
531
435
  }
532
436
 
533
437
  function getOptions(){
@@ -712,11 +616,10 @@
712
616
  return true;
713
617
  }
714
618
 
715
- // MD.safeRendered is the shared first pass. The detached template used
716
- // below is inert while the stricter positive policy removes anything
717
- // that the shared sanitizer intentionally leaves available.
619
+ // MD.safeRendered owns Markdown rendering. The detached template keeps
620
+ // source-relative links and images coherent before insertion.
718
621
  const safeRendered=new MD(markdown).safeRendered;
719
- const fragment=strictMarkdownFragment(safeRendered,sourceURL);
622
+ const fragment=renderedMarkdownFragment(safeRendered,sourceURL);
720
623
  const meaningful=Boolean(
721
624
  fragment.textContent?.trim()
722
625
  ||fragment.querySelector('img,hr,table')
@@ -741,136 +644,38 @@
741
644
  return true;
742
645
  }
743
646
 
744
- function strictMarkdownFragment(html='',sourceURL=options.sourceURL){
647
+ function renderedMarkdownFragment(html='',sourceURL=options.sourceURL){
745
648
  const template=document.createElement('template');
746
649
  template.innerHTML=html;
747
650
 
748
- const elements=Array.from(template.content.querySelectorAll('*'));
749
- for(const element of elements){
750
- const name=element.tagName.toUpperCase();
751
- if(!markdownDocumentElementAllowed(name)){
752
- // Removing the complete subtree rejects custom elements such as
753
- // html-import rather than preserving executable descendants.
754
- element.remove();
651
+ for(const link of template.content.querySelectorAll('a[href]')){
652
+ const originalHref=link.getAttribute('href')||'';
653
+ const policy=markdownDocumentLinkPolicy(originalHref,sourceURL,pageOrigin);
654
+ if(!policy){
755
655
  continue;
756
656
  }
757
-
758
- const allowedAttributes=new Set(markdownDocumentAllowedAttributes(name));
759
- for(const attribute of Array.from(element.attributes)){
760
- if(!allowedAttributes.has(attribute.name.toLowerCase())){
761
- element.removeAttribute(attribute.name);
762
- }
763
- }
764
-
765
- sanitizeAllowedAttributeValues(element,name);
766
- if(name==='A'){
767
- sanitizeLink(element,sourceURL);
768
- }else if(name==='IMG'){
769
- sanitizeImage(element,sourceURL);
657
+ link.setAttribute('href',policy.href);
658
+ link.dataset.markdownKind=policy.kind;
659
+ link.dataset.markdownOriginalHref=originalHref;
660
+ link.dataset.markdownTargetUrl=policy.targetURL;
661
+ link.dataset.markdownFragment=policy.fragment;
662
+ }
663
+
664
+ for(const image of template.content.querySelectorAll('img[src]')){
665
+ const resolved=markdownDocumentAssetURL(
666
+ image.getAttribute('src')||'',
667
+ sourceURL,
668
+ pageOrigin
669
+ );
670
+ if(resolved){
671
+ image.setAttribute('src',resolved);
770
672
  }
673
+ image.alt=image.getAttribute('alt')||'';
771
674
  }
772
675
 
773
- const comments=[];
774
- const commentWalker=document.createTreeWalker(
775
- template.content,
776
- globalThis.NodeFilter?.SHOW_COMMENT||128
777
- );
778
- while(commentWalker.nextNode()){
779
- comments.push(commentWalker.currentNode);
780
- }
781
- comments.forEach(comment=>comment.remove());
782
676
  return template.content;
783
677
  }
784
678
 
785
- function sanitizeAllowedAttributeValues(element,name){
786
- for(const attribute of Array.from(element.attributes)){
787
- if(attribute.value.length>2048){
788
- element.setAttribute(attribute.name,attribute.value.slice(0,2048));
789
- }
790
- }
791
-
792
- if(name==='CODE'&&element.hasAttribute('class')){
793
- const className=element.getAttribute('class').trim();
794
- if(!/^language-[a-z0-9][a-z0-9_+-]{0,63}$/i.test(className)){
795
- element.removeAttribute('class');
796
- }
797
- }
798
-
799
- for(const attribute of ['colspan','rowspan']){
800
- if(element.hasAttribute(attribute)){
801
- sanitizeIntegerAttribute(element,attribute,1,100);
802
- }
803
- }
804
- if(name==='OL'&&element.hasAttribute('start')){
805
- sanitizeIntegerAttribute(element,'start',-100000,100000);
806
- }
807
- if(name==='LI'&&element.hasAttribute('value')){
808
- sanitizeIntegerAttribute(element,'value',-100000,100000);
809
- }
810
- if(name==='TH'&&element.hasAttribute('scope')){
811
- const scope=element.getAttribute('scope').toLowerCase();
812
- if(!['col','colgroup','row','rowgroup'].includes(scope)){
813
- element.removeAttribute('scope');
814
- }else{
815
- element.setAttribute('scope',scope);
816
- }
817
- }
818
- }
819
-
820
- function sanitizeIntegerAttribute(element,name,min,max){
821
- const value=Number(element.getAttribute(name));
822
- if(!Number.isSafeInteger(value)||value<min||value>max){
823
- element.removeAttribute(name);
824
- return;
825
- }
826
- element.setAttribute(name,String(value));
827
- }
828
-
829
- function sanitizeLink(link,sourceURL){
830
- const originalHref=link.getAttribute('href')||'';
831
- const policy=markdownDocumentLinkPolicy(
832
- originalHref,
833
- sourceURL,
834
- pageOrigin
835
- );
836
- if(!policy){
837
- link.removeAttribute('href');
838
- return;
839
- }
840
-
841
- link.setAttribute('href',policy.href);
842
- link.dataset.markdownKind=policy.kind;
843
- link.dataset.markdownOriginalHref=originalHref.slice(0,2048);
844
- link.dataset.markdownTargetUrl=policy.targetURL;
845
- link.dataset.markdownFragment=policy.fragment;
846
-
847
- if(
848
- policy.external
849
- &&/^https?:$/i.test(new URL(policy.targetURL).protocol)
850
- ){
851
- link.target='_blank';
852
- link.rel='noopener noreferrer';
853
- link.referrerPolicy='no-referrer';
854
- }
855
- }
856
-
857
- function sanitizeImage(image,sourceURL){
858
- const resolved=markdownDocumentAssetURL(
859
- image.getAttribute('src')||'',
860
- sourceURL,
861
- pageOrigin
862
- );
863
- if(resolved){
864
- image.setAttribute('src',resolved);
865
- }else{
866
- image.removeAttribute('src');
867
- }
868
- image.alt=image.getAttribute('alt')||'';
869
- image.loading='lazy';
870
- image.decoding='async';
871
- image.referrerPolicy='no-referrer';
872
- }
873
-
874
679
  function decorateHeadings(fragment){
875
680
  const usedHeadingIds=new Set();
876
681
  return Array.from(fragment.querySelectorAll('h1,h2,h3,h4,h5,h6'))
@@ -884,7 +689,7 @@
884
689
  const level=Number(heading.tagName.slice(1));
885
690
  heading.id=id;
886
691
  heading.tabIndex=-1;
887
- return Object.freeze({id,level,text});
692
+ return {id,level,text};
888
693
  }
889
694
  );
890
695
  }
@@ -904,7 +709,7 @@
904
709
  const label=headingText?`${prefix}: ${headingText}`:prefix;
905
710
 
906
711
  table.tabIndex=0;
907
- table.setAttribute('aria-label',label.slice(0,256));
712
+ table.setAttribute('aria-label',label);
908
713
  }
909
714
  );
910
715
  }
@@ -963,7 +768,7 @@
963
768
  :String(error||options.labels.error);
964
769
  const code=String(error?.code||'ARCANE_MARKDOWN_DOCUMENT_RENDER_FAILED');
965
770
  const reason=String(error?.reason||'markdown-document-render-failed');
966
- transition('error',{message:message.slice(0,2048),code,reason},operationId);
771
+ transition('error',{message,code,reason},operationId);
967
772
  return false;
968
773
  }
969
774
 
@@ -974,13 +779,11 @@
974
779
  state=nextState;
975
780
  updateStateDOM();
976
781
 
977
- const eventDetail=Object.freeze(
978
- {
979
- ...detail,
980
- sourceURL:currentSourceURL,
981
- state
982
- }
983
- );
782
+ const eventDetail={
783
+ ...detail,
784
+ sourceURL:currentSourceURL,
785
+ state
786
+ };
984
787
  const publicDetail={
985
788
  characters:Number(detail.characters||currentMarkdown.length||0),
986
789
  headingCount:Array.isArray(detail.headings)?detail.headings.length:tableOfContents.length,
@@ -1055,15 +858,13 @@
1055
858
  return;
1056
859
  }
1057
860
 
1058
- const detail=Object.freeze(
1059
- {
1060
- fragment:link.dataset.markdownFragment||'',
1061
- href:link.dataset.markdownOriginalHref||link.getAttribute('href'),
1062
- kind:link.dataset.markdownKind||'link',
1063
- sourceURL:currentSourceURL,
1064
- targetURL:link.dataset.markdownTargetUrl||link.href
1065
- }
1066
- );
861
+ const detail={
862
+ fragment:link.dataset.markdownFragment||'',
863
+ href:link.dataset.markdownOriginalHref||link.getAttribute('href'),
864
+ kind:link.dataset.markdownKind||'link',
865
+ sourceURL:currentSourceURL,
866
+ targetURL:link.dataset.markdownTargetUrl||link.href
867
+ };
1067
868
  operationSequence+=1;
1068
869
  const operationId=`${events.descriptor.instanceId}:navigate:${operationSequence}`;
1069
870
  const publication=events.dispatch('markdown-document-navigate',detail,{
@@ -2,16 +2,12 @@
2
2
  <style>
3
3
  :host{
4
4
  display:block;
5
- height:100%;
6
- min-height:0;
7
5
  }
8
6
 
9
7
  .markdown-editor{
10
8
  display:grid;
11
9
  gap:.75em;
12
- grid-template-rows:auto minmax(8em,1fr) auto minmax(10em,1fr) auto;
13
- height:100%;
14
- min-height:0;
10
+ grid-template-rows:auto auto auto auto auto;
15
11
  }
16
12
 
17
13
  .entry-title,
@@ -25,7 +21,7 @@
25
21
  background:var(--background);
26
22
  border:1px solid var(--border-color);
27
23
  border-radius:.5em;
28
- overflow:auto;
24
+ overflow:visible;
29
25
  padding:1em;
30
26
  }
31
27
 
@@ -235,7 +231,7 @@
235
231
  markdown:textarea.value,
236
232
  title:titleInput.value
237
233
  };
238
- emit('markdown-editor-change',detail,{markdownLength:detail.markdown.length,titleLength:detail.title.length});
234
+ emit('markdown-editor-change',detail,detail);
239
235
  if(options.onChange){
240
236
  Promise.resolve(options.onChange(detail)).catch(
241
237
  error=>console.error('Markdown change callback failed:',error)
@@ -265,7 +261,7 @@
265
261
  preview.querySelectorAll('a').forEach(
266
262
  link=>{
267
263
  link.target='_blank';
268
- link.rel='noopener noreferrer';
264
+ link.removeAttribute('rel');
269
265
  }
270
266
  );
271
267
  }
@@ -299,7 +295,7 @@
299
295
  return false;
300
296
  }
301
297
  const markdown=textarea.value;
302
- const entryTitle=titleInput.value.trim();
298
+ const entryTitle=titleInput.value;
303
299
  if(!markdown.trim()){
304
300
  status.innerText=options.labels.emptyError;
305
301
  textarea.focus();
@@ -330,7 +326,8 @@
330
326
  resetEditor(false);
331
327
  }
332
328
  status.innerText=options.labels.saved;
333
- emit('markdown-editor-saved',{title:entryTitle,markdown},{markdownLength:markdown.length,titleLength:entryTitle.length},operationId);
329
+ const detail={title:entryTitle,markdown};
330
+ emit('markdown-editor-saved',detail,detail,operationId);
334
331
  return true;
335
332
  }catch(error){
336
333
  if(destroyed||generation!==saveGeneration||controller.signal.aborted){
@@ -1,11 +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
- <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
- <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));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;
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;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
+ <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></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));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=false,destroyed=false,operationSequence=0;
5
5
  function nextOperationId(kind){operationSequence+=1;return `${events.descriptor.instanceId}:${kind}:${operationSequence}`}
6
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}}
7
+ host.configure=options=>{if(destroyed)return false;platformUrl=String(options?.platformUrl||platformUrl);if(Object.hasOwn(options||{},'privacyEnhanced'))privacyEnhanced=options.privacyEnhanced===true;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',()=>{const detail={url:platformUrl,destination:'platform'};emit('media-open-platform',detail,detail,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}),detail={locator,url};frame.src=url;frame.hidden=false;empty.hidden=true;status.textContent=locator.type==='playlist'?'Playlist loaded.':'Video loaded.';emit('media-load',detail,detail,operationId);return locator}catch(error){const code=String(error?.code||'ARCANE_MEDIA_EMBED_SOURCE_INVALID'),reason='media-source-invalid',detail={error,code,reason};status.textContent=error.message;frame.hidden=true;empty.hidden=false;emit('media-error',detail,detail,operationId);return null}}
10
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
11
  host.ready=true;emit('media-embed-ready',{}, {ready:true});</script>
@@ -1,14 +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
- <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
- <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>
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:auto;background:var(--arcane-surface-muted);border:1px solid var(--arcane-border);border-radius:var(--arcane-radius-large)}.preview :where(img,video){display:block}.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
+ <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 the selected display as an 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
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
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
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}
7
+ function cleanupCaptureStart(originalError=null){const cleanupErrors=[];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
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
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
10
  root.querySelector('.actions').addEventListener('click',onCapture,{signal:listeners.signal});root.querySelector('#stop').addEventListener('click',onStop,{signal:listeners.signal});
11
11
  function setButtons(value){for(const button of root.querySelectorAll('[data-capture]'))button.disabled=value}
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}`)}
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.';setButtons(false);return emit('screen-capture-result',{result},{result},`screen-capture-${events.instanceId}-${generation}`)}
13
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
14
  host.capture=capture;host.destroy=destroy;host.ready=true;emit('screen-capture-ready',{}, {ready:true},`screen-capture-ready-${events.instanceId}`);</script>
@@ -36,7 +36,7 @@
36
36
  <label>Alleged actor<select data-actor></select></label>
37
37
  <label>Disposition<select data-disposition></select></label>
38
38
  <label>Confidence<select data-confidence></select></label>
39
- <label class="notes">Notes, contrary facts, and follow-up<textarea data-notes maxlength="10000"></textarea></label>
39
+ <label class="notes">Notes, contrary facts, and follow-up<textarea data-notes></textarea></label>
40
40
  <div class="save-row"><button type="submit">Save decision</button><span class="save-state" data-save-state aria-live="polite"></span></div>
41
41
  </form>
42
42
  </section>
@@ -82,7 +82,7 @@
82
82
 
83
83
  function showFinding(value={},config={}){
84
84
  finding=value&&typeof value==='object'?value:null; if(!finding) throw new TypeError('A finding is required.');
85
- sources=(Array.isArray(finding.sources)?finding.sources:[]).slice(0,40);
85
+ sources=Array.isArray(finding.sources)?finding.sources:[];
86
86
  title.textContent=String(finding.label||'Investigative lead'); assessment.textContent=String(finding.assessment||finding.conclusion||'Human review is required before reliance.');
87
87
  badges.replaceChildren();
88
88
  for(const value of [finding.allegedActorLabel?`Alleged actor: ${finding.allegedActorLabel}`:'Actor not assigned',finding.kind||'Investigative lead',finding.confidence?`${finding.confidence} confidence`:'Confidence not assigned']){const badge=document.createElement('span'); badge.className='badge'; badge.textContent=value; badges.append(badge);}