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.
- package/CHANGELOG.md +29 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
- package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
- package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
- package/browser-runtime/ai/browser-speech.mjs +2 -0
- package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
- package/browser-runtime/ai/model-controller.mjs +285 -95
- package/browser-runtime/ai/speech-worker-client.mjs +247 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
- package/browser-runtime/event-manager.mjs +1097 -1
- package/docs/architecture.md +2 -2
- package/docs/event-manager.md +155 -27
- package/docs/reference/README.md +27 -27
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1162 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +6 -3
- package/docs/reference/behavioral-testing.md +29 -6
- package/docs/reference/cli.md +117 -9
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/event-manager.md +577 -32
- package/docs/reference/inventory/package-api.json +478 -2
- package/docs/reference/inventory/runtime-components.json +108 -44
- package/docs/reference/inventory/runtime-modules.json +131 -53
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +157 -43
- package/docs/reference/runtime-components.md +258 -83
- package/docs/reference/runtime-modules.md +613 -77
- package/docs/reference/sdk-api.md +1014 -25
- package/package.json +5 -4
- package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
- package/runtime/arcane/components/app-bar.html +34 -13
- package/runtime/arcane/components/assistant-panel.html +110 -57
- package/runtime/arcane/components/calculator.html +7 -4
- package/runtime/arcane/components/chart.html +58 -17
- package/runtime/arcane/components/chat.html +606 -136
- package/runtime/arcane/components/conversation-view.html +13 -6
- package/runtime/arcane/components/dashboard-config.html +96 -59
- package/runtime/arcane/components/data-maintenance.html +69 -14
- package/runtime/arcane/components/data-view.html +53 -7
- package/runtime/arcane/components/directory-picker.html +118 -32
- package/runtime/arcane/components/document-inspector.html +47 -10
- package/runtime/arcane/components/file-drop.html +81 -35
- package/runtime/arcane/components/file-inspector.html +72 -22
- package/runtime/arcane/components/file-manager.html +374 -79
- package/runtime/arcane/components/integration-settings.html +12 -5
- package/runtime/arcane/components/local-ai-status.html +48 -19
- package/runtime/arcane/components/markdown-document.html +161 -68
- package/runtime/arcane/components/markdown-editor.html +110 -33
- package/runtime/arcane/components/media-embed.html +8 -5
- package/runtime/arcane/components/modal.html +15 -5
- package/runtime/arcane/components/output-panel.html +28 -23
- package/runtime/arcane/components/preferences-form.html +22 -4
- package/runtime/arcane/components/record-timeline.html +18 -2
- package/runtime/arcane/components/relationship-board.html +23 -3
- package/runtime/arcane/components/screen-capture.html +10 -4
- package/runtime/arcane/components/source-code-viewer.html +76 -9
- package/runtime/arcane/components/source-explanation.html +23 -3
- package/runtime/arcane/components/speech.html +462 -384
- package/runtime/arcane/components/summary-strip.html +22 -11
- package/runtime/arcane/components/table.html +39 -21
- package/runtime/arcane/components/task-progress.html +79 -21
- package/runtime/arcane/components/terminal-workspace.html +7 -4
- package/runtime/arcane/components/theme-editor.html +7 -3
- package/runtime/arcane/components/unified-inbox.html +9 -4
- package/runtime/arcane/components/voice-transcription.html +639 -98
- package/runtime/arcane/components/weather-widget.html +5 -3
- package/runtime/arcane/components/web-navigator.html +48 -8
- package/runtime/arcane/entities/Chat.js +1 -1
- package/runtime/arcane/entities/User.js +110 -23
- package/runtime/arcane/modules/AI.js +2109 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
- package/runtime/arcane/modules/AIRuntimeState.js +109 -52
- package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
- package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
- package/runtime/arcane/modules/CalculatorEngine.js +63 -3
- package/runtime/arcane/modules/CommunicationAppController.js +588 -28
- package/runtime/arcane/modules/CommunicationHub.js +590 -10
- package/runtime/arcane/modules/ComponentContracts.js +470 -0
- package/runtime/arcane/modules/ConversationTimebox.js +152 -33
- package/runtime/arcane/modules/DBLS.js +40 -7
- package/runtime/arcane/modules/DBOPFS.js +35 -11
- package/runtime/arcane/modules/DataMaintenance.js +12 -2
- package/runtime/arcane/modules/Errors.js +65 -7
- package/runtime/arcane/modules/HTMLImport.js +198 -14
- package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
- package/runtime/arcane/modules/Mail.js +738 -115
- package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
- package/runtime/arcane/modules/MailTransport.mjs +197 -39
- package/runtime/arcane/modules/Ollama.js +36 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
- package/runtime/arcane/modules/PreferenceStore.js +367 -33
- package/runtime/arcane/modules/RecordReviewStore.js +322 -23
- package/runtime/arcane/modules/ScreenCapture.js +1397 -15
- package/runtime/arcane/modules/SpeechPlayback.js +438 -41
- package/runtime/arcane/modules/TerminalClient.js +277 -12
- package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
- package/runtime/arcane/modules/ThemeManager.js +39 -7
- package/runtime/arcane/modules/TimeGuard.js +131 -20
- package/runtime/arcane/modules/WaitForComponent.js +386 -33
- package/schemas/arcane-lock.schema.json +2 -2
- package/src/cli/main.mjs +435 -9
- package/src/event-manager.mjs +1097 -1
- package/src/import-map.mjs +21 -3
- package/src/index.mjs +13 -0
- package/src/installed-sdk-runtime.mjs +112 -0
- package/src/mail-api.mjs +22 -0
- package/src/mail-credentials.mjs +667 -0
- package/src/mail-server.mjs +1769 -0
- package/src/mail.mjs +261 -0
- package/src/sdk-browser-runtime.mjs +85 -41
- package/src/testing-loader.mjs +7 -0
- package/src/toolchain.mjs +3 -0
- package/src/workspace.mjs +1 -1
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
const host=this;
|
|
34
34
|
const root=host.shadowRoot;
|
|
35
35
|
const {default:DirectoryPicker}=await import('../modules/DirectoryPicker.js');
|
|
36
|
+
const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
|
|
37
|
+
const events=createArcaneEventSource(host,{source:'arcane.component.directory-picker',eventTypes:['directory-picker-cancel','directory-picker-change','directory-picker-error','directory-picker-ready']});
|
|
38
|
+
const lifecycleController=new AbortController();
|
|
36
39
|
const field=root.querySelector('#field');
|
|
37
40
|
const label=root.querySelector('#label');
|
|
38
41
|
const pathDisplay=root.querySelector('#path');
|
|
@@ -45,18 +48,37 @@
|
|
|
45
48
|
let disabled=false;
|
|
46
49
|
let pending=false;
|
|
47
50
|
let picker=new DirectoryPicker();
|
|
51
|
+
let operationGeneration=0;
|
|
52
|
+
let selectionRevision=0;
|
|
53
|
+
let operationSequence=0;
|
|
54
|
+
let destroyed=false;
|
|
48
55
|
|
|
49
56
|
host.configure=configure;
|
|
57
|
+
host.destroy=destroy;
|
|
50
58
|
host.focus=focus;
|
|
51
59
|
host.select=select;
|
|
52
60
|
|
|
53
61
|
Object.defineProperties(host,{
|
|
54
|
-
disabled:{get:()=>disabled,set:setDisabled},
|
|
55
|
-
options:{get:getOptions,set:configure},
|
|
56
|
-
value:{get:()=>currentValue,set:setValue}
|
|
62
|
+
disabled:{configurable:true,get:()=>disabled,set:setDisabled},
|
|
63
|
+
options:{configurable:true,get:getOptions,set:configure},
|
|
64
|
+
value:{configurable:true,get:()=>currentValue,set:setValue}
|
|
57
65
|
});
|
|
58
66
|
|
|
59
|
-
choose.addEventListener(
|
|
67
|
+
choose.addEventListener(
|
|
68
|
+
'click',
|
|
69
|
+
()=>select().catch(
|
|
70
|
+
error=>console.error('Unable to select a directory:',error)
|
|
71
|
+
),
|
|
72
|
+
{signal:lifecycleController.signal}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
function emit(type,detail={},publicDetail={},operationId=null){
|
|
76
|
+
if(destroyed){
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const publication=events.dispatch(type,detail,{publicDetail,operationId});
|
|
80
|
+
return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
|
|
81
|
+
}
|
|
60
82
|
|
|
61
83
|
function datasetOptions(){
|
|
62
84
|
return {
|
|
@@ -71,32 +93,47 @@
|
|
|
71
93
|
}
|
|
72
94
|
|
|
73
95
|
function configure(input={}){
|
|
96
|
+
if(destroyed){
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
74
99
|
if(!input||typeof input!=='object'||Array.isArray(input)){
|
|
75
100
|
throw new TypeError('Directory picker configuration must be an object.');
|
|
76
101
|
}
|
|
102
|
+
const replacesSelection=Object.prototype.hasOwnProperty.call(input,'picker')
|
|
103
|
+
||Object.prototype.hasOwnProperty.call(input,'value');
|
|
77
104
|
const next={...datasetOptions(),...options,...input};
|
|
78
105
|
const nextDisabled=Object.prototype.hasOwnProperty.call(input,'disabled')||!host.ready
|
|
79
106
|
?Boolean(next.disabled)
|
|
80
107
|
:disabled;
|
|
108
|
+
const nextPicker=input.picker===undefined?picker:input.picker;
|
|
81
109
|
if(input.picker!==undefined){
|
|
82
|
-
if(!
|
|
110
|
+
if(!nextPicker||typeof nextPicker.select!=='function'){
|
|
83
111
|
throw new TypeError('picker must expose a select(options) function.');
|
|
84
112
|
}
|
|
85
|
-
picker=input.picker;
|
|
86
113
|
}
|
|
87
|
-
|
|
114
|
+
const nextOptions={
|
|
88
115
|
buttonLabel:text(next.buttonLabel,'Choose folder'),
|
|
89
116
|
help:text(next.help,'Choose one folder from this device.'),
|
|
90
117
|
label:text(next.label,'Folder'),
|
|
91
118
|
placeholder:text(next.placeholder,'No folder selected'),
|
|
92
119
|
title:text(next.title,next.label||'Choose folder')
|
|
93
120
|
};
|
|
121
|
+
const replacesValue=Object.prototype.hasOwnProperty.call(input,'value')
|
|
122
|
+
||!host.ready;
|
|
123
|
+
const nextValue=replacesValue
|
|
124
|
+
?normalizeValue(next.value??'')
|
|
125
|
+
:currentValue;
|
|
126
|
+
if(replacesSelection){
|
|
127
|
+
reviseSelection();
|
|
128
|
+
}
|
|
129
|
+
picker=nextPicker;
|
|
130
|
+
options=nextOptions;
|
|
94
131
|
label.innerText=options.label;
|
|
95
132
|
help.innerText=options.help;
|
|
96
133
|
choose.innerText=options.buttonLabel;
|
|
97
134
|
pathDisplay.placeholder=options.placeholder;
|
|
98
|
-
if(
|
|
99
|
-
|
|
135
|
+
if(replacesValue){
|
|
136
|
+
applyValue(nextValue);
|
|
100
137
|
}
|
|
101
138
|
setDisabled(nextDisabled);
|
|
102
139
|
return getOptions();
|
|
@@ -112,17 +149,33 @@
|
|
|
112
149
|
}
|
|
113
150
|
|
|
114
151
|
function setValue(value=''){
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
throw new TypeError('The directory path must be bounded plain text.');
|
|
152
|
+
if(destroyed){
|
|
153
|
+
return currentValue;
|
|
118
154
|
}
|
|
119
|
-
|
|
155
|
+
const next=normalizeValue(value);
|
|
156
|
+
reviseSelection();
|
|
157
|
+
return applyValue(next);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function applyValue(value=''){
|
|
161
|
+
currentValue=normalizeValue(value);
|
|
120
162
|
pathDisplay.value=currentValue;
|
|
121
163
|
host.dataset.hasValue=`${Boolean(currentValue)}`;
|
|
122
164
|
return currentValue;
|
|
123
165
|
}
|
|
124
166
|
|
|
167
|
+
function normalizeValue(value=''){
|
|
168
|
+
const next=`${value??''}`.trim();
|
|
169
|
+
if(next.length>4096||/[\u0000-\u001f\u007f]/.test(next)){
|
|
170
|
+
throw new TypeError('The directory path must be bounded plain text.');
|
|
171
|
+
}
|
|
172
|
+
return next;
|
|
173
|
+
}
|
|
174
|
+
|
|
125
175
|
function setDisabled(value=false){
|
|
176
|
+
if(destroyed){
|
|
177
|
+
return disabled;
|
|
178
|
+
}
|
|
126
179
|
disabled=Boolean(value);
|
|
127
180
|
pathDisplay.disabled=disabled;
|
|
128
181
|
choose.disabled=disabled||pending;
|
|
@@ -132,12 +185,18 @@
|
|
|
132
185
|
}
|
|
133
186
|
|
|
134
187
|
function focus(options){
|
|
188
|
+
if(destroyed){
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
135
191
|
choose.focus(options);
|
|
136
192
|
return document.activeElement===host||root.activeElement===choose;
|
|
137
193
|
}
|
|
138
194
|
|
|
139
195
|
async function select(){
|
|
140
|
-
if(disabled||pending) return null;
|
|
196
|
+
if(destroyed||disabled||pending) return null;
|
|
197
|
+
const generation=++operationGeneration;
|
|
198
|
+
const revision=selectionRevision;
|
|
199
|
+
const operationId=`${events.descriptor.instanceId}:select:${++operationSequence}`;
|
|
141
200
|
const previousPath=currentValue;
|
|
142
201
|
setPending(true);
|
|
143
202
|
setStatus('Opening the folder selector.');
|
|
@@ -146,34 +205,39 @@
|
|
|
146
205
|
...(currentValue?{initialPath:currentValue}:{}),
|
|
147
206
|
title:options.title||options.label||'Choose folder'
|
|
148
207
|
});
|
|
208
|
+
if(destroyed
|
|
209
|
+
||generation!==operationGeneration
|
|
210
|
+
||revision!==selectionRevision){
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
149
213
|
if(result.cancelled){
|
|
150
214
|
setStatus('Folder selection canceled.');
|
|
151
|
-
|
|
152
|
-
bubbles:true,
|
|
153
|
-
composed:true,
|
|
154
|
-
detail:{path:currentValue}
|
|
155
|
-
}));
|
|
215
|
+
emit('directory-picker-cancel',{path:currentValue,reason:'directory-selection-cancelled'},{hasPath:Boolean(currentValue),reason:'directory-selection-cancelled'},operationId);
|
|
156
216
|
return result;
|
|
157
217
|
}
|
|
158
|
-
|
|
218
|
+
applyValue(result.path);
|
|
159
219
|
setStatus('Folder selected.');
|
|
160
|
-
|
|
161
|
-
bubbles:true,
|
|
162
|
-
composed:true,
|
|
163
|
-
detail:{path:currentValue,previousPath,source:'picker'}
|
|
164
|
-
}));
|
|
220
|
+
emit('directory-picker-change',{path:currentValue,previousPath,source:'picker'},{hasPath:Boolean(currentValue),changed:currentValue!==previousPath},operationId);
|
|
165
221
|
return result;
|
|
166
222
|
}catch(error){
|
|
223
|
+
if(destroyed
|
|
224
|
+
||generation!==operationGeneration
|
|
225
|
+
||revision!==selectionRevision){
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
167
228
|
const message=error?.message||'The folder selector could not open.';
|
|
168
229
|
setStatus(message,'error');
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
detail:{error,message}
|
|
173
|
-
}));
|
|
230
|
+
const code=String(error?.code||'ARCANE_DIRECTORY_PICKER_SELECTION_FAILED');
|
|
231
|
+
const reason='directory-selection-failed';
|
|
232
|
+
emit('directory-picker-error',{error,message,code,reason},{code,reason},operationId);
|
|
174
233
|
throw error;
|
|
175
234
|
}finally{
|
|
176
|
-
|
|
235
|
+
if(!destroyed&&generation===operationGeneration){
|
|
236
|
+
setPending(false);
|
|
237
|
+
if(revision!==selectionRevision){
|
|
238
|
+
setStatus('Folder selection superseded.');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
177
241
|
}
|
|
178
242
|
}
|
|
179
243
|
|
|
@@ -185,13 +249,35 @@
|
|
|
185
249
|
return pending;
|
|
186
250
|
}
|
|
187
251
|
|
|
252
|
+
function reviseSelection(){
|
|
253
|
+
selectionRevision+=1;
|
|
254
|
+
if(pending){
|
|
255
|
+
setStatus('Folder selection superseded; waiting for the selector to close.');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
188
259
|
function setStatus(message='',tone=''){
|
|
189
260
|
status.innerText=`${message||''}`;
|
|
190
261
|
status.dataset.tone=tone;
|
|
191
262
|
return status.innerText;
|
|
192
263
|
}
|
|
193
264
|
|
|
265
|
+
function destroy(){
|
|
266
|
+
if(destroyed){
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
destroyed=true;
|
|
270
|
+
operationGeneration+=1;
|
|
271
|
+
selectionRevision+=1;
|
|
272
|
+
lifecycleController.abort('directory-picker-destroyed');
|
|
273
|
+
pending=false;
|
|
274
|
+
picker?.destroy?.();
|
|
275
|
+
events.dispose();
|
|
276
|
+
host.ready=false;
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
194
280
|
configure(datasetOptions());
|
|
195
281
|
host.ready=true;
|
|
196
|
-
|
|
282
|
+
emit('directory-picker-ready',{}, {ready:true});
|
|
197
283
|
</script>
|
|
@@ -70,17 +70,24 @@
|
|
|
70
70
|
const notesLabel=shadow.querySelector('[data-notes-label]');
|
|
71
71
|
const saveState=shadow.querySelector('.save-state');
|
|
72
72
|
const {default:MD}=await import('../modules/MD.js');
|
|
73
|
+
const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
|
|
74
|
+
const events=createArcaneEventSource(host,{source:'arcane.component.document-inspector',eventTypes:['document-review-change','document-inspector-ready']});
|
|
75
|
+
const lifecycleController=new AbortController();
|
|
73
76
|
let rawMarkdown='';
|
|
74
77
|
let renderedMarkdown='';
|
|
75
78
|
let markdownMessage=null;
|
|
76
79
|
let activeRecordId='';
|
|
77
80
|
let loadSequence=0;
|
|
81
|
+
let loadController=null;
|
|
82
|
+
let operationSequence=0;
|
|
83
|
+
let destroyed=false;
|
|
78
84
|
|
|
79
85
|
host.loadDocument=loadDocument;
|
|
80
86
|
host.selectView=selectView;
|
|
87
|
+
host.destroy=destroy;
|
|
81
88
|
host.ready=true;
|
|
82
89
|
|
|
83
|
-
tabs.forEach(tab=>tab.addEventListener('click',()=>selectView(tab.dataset.view)));
|
|
90
|
+
tabs.forEach(tab=>tab.addEventListener('click',()=>selectView(tab.dataset.view),{signal:lifecycleController.signal}));
|
|
84
91
|
tabs.forEach(tab=>tab.addEventListener('keydown',event=>{
|
|
85
92
|
if(!['ArrowLeft','ArrowRight','Home','End'].includes(event.key)) return;
|
|
86
93
|
event.preventDefault();
|
|
@@ -88,16 +95,18 @@
|
|
|
88
95
|
const index=available.indexOf(tab);
|
|
89
96
|
const next=event.key==='Home'?0:event.key==='End'?available.length-1:(index+(event.key==='ArrowRight'?1:-1)+available.length)%available.length;
|
|
90
97
|
selectView(available[next].dataset.view); available[next].focus();
|
|
91
|
-
}));
|
|
92
|
-
searchInput.addEventListener('input',()=>highlightSearch(searchInput.value));
|
|
98
|
+
},{signal:lifecycleController.signal}));
|
|
99
|
+
searchInput.addEventListener('input',()=>highlightSearch(searchInput.value),{signal:lifecycleController.signal});
|
|
93
100
|
reviewForm.addEventListener('submit',event=>{
|
|
94
101
|
event.preventDefault();
|
|
95
102
|
const attributes={};
|
|
96
103
|
reviewFields.querySelectorAll('[data-review-field]').forEach(field=>{attributes[field.dataset.reviewField]=field.value;});
|
|
97
104
|
const detail={recordId:activeRecordId,status:reviewStatus.value,classification:reviewClassification.value||'unassigned',attributes,notes:reviewNotes.value};
|
|
98
105
|
saveState.textContent='Saving…';
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
const operationId=`${events.descriptor.instanceId}:review-change:${++operationSequence}`;
|
|
107
|
+
const publication=events.dispatch('document-review-change',detail,{operationId,publicDetail:{hasRecord:Boolean(activeRecordId),attributeCount:Object.keys(attributes).length,hasNotes:Boolean(reviewNotes.value)}});
|
|
108
|
+
projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
|
|
109
|
+
},{signal:lifecycleController.signal});
|
|
101
110
|
|
|
102
111
|
function sameOriginUrl(value){
|
|
103
112
|
if(!String(value||'').trim()) throw new TypeError('A document source is required.');
|
|
@@ -117,6 +126,9 @@
|
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
function selectView(view='pdf'){
|
|
129
|
+
if(destroyed){
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
120
132
|
const available=tabs.filter(tab=>!tab.hidden);
|
|
121
133
|
const selected=available.some(tab=>tab.dataset.view===view)?view:available[0]?.dataset.view;
|
|
122
134
|
tabs.forEach(tab=>{const active=tab.dataset.view===selected;tab.setAttribute('aria-selected',String(active));tab.tabIndex=active?0:-1;});
|
|
@@ -172,6 +184,12 @@
|
|
|
172
184
|
}
|
|
173
185
|
|
|
174
186
|
async function loadDocument(config={}){
|
|
187
|
+
if(destroyed){
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
loadController?.abort('document-replaced');
|
|
191
|
+
const controller=new AbortController();
|
|
192
|
+
loadController=controller;
|
|
175
193
|
const sequence=++loadSequence;
|
|
176
194
|
activeRecordId=String(config.recordId||'');
|
|
177
195
|
saveState.textContent=''; searchInput.value=''; matchStatus.textContent='';
|
|
@@ -231,22 +249,41 @@
|
|
|
231
249
|
if(hasText){
|
|
232
250
|
showSurfaceMessage(markdownSurface,'Loading document text…');
|
|
233
251
|
try{
|
|
234
|
-
const response=await fetch(sameOriginUrl(config.markdownUrl),{cache:'no-store',credentials:'same-origin'});
|
|
252
|
+
const response=await fetch(sameOriginUrl(config.markdownUrl),{cache:'no-store',credentials:'same-origin',signal:controller.signal});
|
|
235
253
|
if(!response.ok) throw new Error(`Document text unavailable (${response.status}).`);
|
|
236
254
|
const markdown=await response.text();
|
|
237
|
-
if(sequence!==loadSequence) return false;
|
|
255
|
+
if(destroyed||sequence!==loadSequence) return false;
|
|
238
256
|
rawMarkdown=markdown; renderedMarkdown=safeMarkdown(rawMarkdown); showMarkdown();
|
|
239
257
|
}catch(error){
|
|
240
|
-
if(sequence!==loadSequence) return false;
|
|
258
|
+
if(destroyed||sequence!==loadSequence||controller.signal.aborted) return false;
|
|
241
259
|
markdownMessage={message:error.message,className:'error'}; showMarkdown();
|
|
242
260
|
}
|
|
243
261
|
}else{
|
|
244
262
|
markdownMessage={message:'No document text is available.',className:'loading'};
|
|
245
263
|
showMarkdown();
|
|
246
264
|
}
|
|
265
|
+
if(loadController===controller){
|
|
266
|
+
loadController=null;
|
|
267
|
+
}
|
|
268
|
+
return !destroyed&&sequence===loadSequence;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function destroy(){
|
|
272
|
+
if(destroyed){
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
destroyed=true;
|
|
276
|
+
loadSequence+=1;
|
|
277
|
+
loadController?.abort('document-inspector-destroyed');
|
|
278
|
+
loadController=null;
|
|
279
|
+
lifecycleController.abort('document-inspector-destroyed');
|
|
280
|
+
pdfPanel.replaceChildren();
|
|
281
|
+
events.dispose();
|
|
282
|
+
host.ready=false;
|
|
247
283
|
return true;
|
|
248
284
|
}
|
|
249
285
|
|
|
250
|
-
host.markSaved=message=>{saveState.textContent=String(message||'Saved');};
|
|
251
|
-
|
|
286
|
+
host.markSaved=message=>{if(destroyed)return false;saveState.textContent=String(message||'Saved');return true;};
|
|
287
|
+
const readyPublication=events.dispatch('document-inspector-ready',{}, {publicDetail:{ready:true}});
|
|
288
|
+
projectArcaneDOMEvent(host,readyPublication.occurrence,{bubbles:true,composed:true});
|
|
252
289
|
</script>
|
|
@@ -46,6 +46,9 @@
|
|
|
46
46
|
<script type="module">
|
|
47
47
|
const host=this;
|
|
48
48
|
const root=host.shadowRoot;
|
|
49
|
+
const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
|
|
50
|
+
const events=createArcaneEventSource(host,{source:'arcane.component.file-drop',eventTypes:['file-drop-selected','file-drop-state','file-drop-progress','file-drop-error','file-drop-ready']});
|
|
51
|
+
const lifecycleController=new AbortController();
|
|
49
52
|
const dropZone=root.querySelector('#dropZone');
|
|
50
53
|
const title=root.querySelector('#fileDropTitle');
|
|
51
54
|
const hint=root.querySelector('#fileDropHint');
|
|
@@ -62,27 +65,38 @@
|
|
|
62
65
|
let currentFiles=[];
|
|
63
66
|
let progress=null;
|
|
64
67
|
let options={};
|
|
68
|
+
let operationSequence=0;
|
|
69
|
+
let destroyed=false;
|
|
65
70
|
|
|
66
71
|
host.clear=clear;
|
|
67
72
|
host.configure=configure;
|
|
73
|
+
host.destroy=destroy;
|
|
68
74
|
host.openPicker=openPicker;
|
|
69
75
|
host.setBusy=setBusy;
|
|
70
76
|
host.setError=setError;
|
|
71
77
|
host.setProgress=setProgress;
|
|
72
78
|
|
|
73
79
|
Object.defineProperties(host,{
|
|
74
|
-
busy:{get:()=>busy,set:setBusy},
|
|
75
|
-
files:{get:()=>currentFiles.slice()},
|
|
76
|
-
options:{get:getOptions,set:configure},
|
|
77
|
-
progress:{get:()=>progress,set:setProgress}
|
|
80
|
+
busy:{configurable:true,get:()=>busy,set:setBusy},
|
|
81
|
+
files:{configurable:true,get:()=>currentFiles.slice()},
|
|
82
|
+
options:{configurable:true,get:getOptions,set:configure},
|
|
83
|
+
progress:{configurable:true,get:()=>progress,set:setProgress}
|
|
78
84
|
});
|
|
79
85
|
|
|
80
|
-
choose.addEventListener('click',openPicker);
|
|
81
|
-
picker.addEventListener('change',()=>acceptFiles(picker.files,'picker'));
|
|
82
|
-
dropZone.addEventListener('dragenter',dragEnter);
|
|
83
|
-
dropZone.addEventListener('dragover',dragOver);
|
|
84
|
-
dropZone.addEventListener('dragleave',dragLeave);
|
|
85
|
-
dropZone.addEventListener('drop',dropFiles);
|
|
86
|
+
choose.addEventListener('click',openPicker,{signal:lifecycleController.signal});
|
|
87
|
+
picker.addEventListener('change',()=>acceptFiles(picker.files,'picker'),{signal:lifecycleController.signal});
|
|
88
|
+
dropZone.addEventListener('dragenter',dragEnter,{signal:lifecycleController.signal});
|
|
89
|
+
dropZone.addEventListener('dragover',dragOver,{signal:lifecycleController.signal});
|
|
90
|
+
dropZone.addEventListener('dragleave',dragLeave,{signal:lifecycleController.signal});
|
|
91
|
+
dropZone.addEventListener('drop',dropFiles,{signal:lifecycleController.signal});
|
|
92
|
+
|
|
93
|
+
function emit(type,detail={},publicDetail={},operationId=null){
|
|
94
|
+
if(destroyed){
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const publication=events.dispatch(type,detail,{publicDetail,operationId});
|
|
98
|
+
return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
|
|
99
|
+
}
|
|
86
100
|
|
|
87
101
|
function datasetOptions(){
|
|
88
102
|
return {
|
|
@@ -96,6 +110,9 @@
|
|
|
96
110
|
}
|
|
97
111
|
|
|
98
112
|
function configure(input={}){
|
|
113
|
+
if(destroyed){
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
99
116
|
const previous={...datasetOptions(),...options};
|
|
100
117
|
options={
|
|
101
118
|
...previous,
|
|
@@ -121,7 +138,7 @@
|
|
|
121
138
|
}
|
|
122
139
|
|
|
123
140
|
function openPicker(){
|
|
124
|
-
if(busy){
|
|
141
|
+
if(destroyed||busy){
|
|
125
142
|
return false;
|
|
126
143
|
}
|
|
127
144
|
picker.click();
|
|
@@ -163,12 +180,23 @@
|
|
|
163
180
|
}
|
|
164
181
|
|
|
165
182
|
function acceptFiles(fileList=[],source='api'){
|
|
183
|
+
if(destroyed){
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const operationId=`${events.descriptor.instanceId}:select:${++operationSequence}`;
|
|
166
187
|
const files=Array.from(fileList||[]).filter(Boolean);
|
|
167
188
|
if(!files.length){
|
|
168
189
|
return false;
|
|
169
190
|
}
|
|
170
191
|
if(options.maxFiles>0&&files.length>options.maxFiles){
|
|
171
|
-
setError(
|
|
192
|
+
setError(
|
|
193
|
+
`Select no more than ${options.maxFiles} file${options.maxFiles===1?'':'s'}.`,
|
|
194
|
+
{
|
|
195
|
+
code:'ARCANE_FILE_DROP_FILE_COUNT_LIMIT_EXCEEDED',
|
|
196
|
+
reason:'file-count-limit-exceeded'
|
|
197
|
+
},
|
|
198
|
+
operationId
|
|
199
|
+
);
|
|
172
200
|
return false;
|
|
173
201
|
}
|
|
174
202
|
currentFiles=options.multiple?files:files.slice(0,1);
|
|
@@ -176,16 +204,14 @@
|
|
|
176
204
|
feedback.hidden=false;
|
|
177
205
|
statusElement.className='status';
|
|
178
206
|
statusElement.innerText=`${currentFiles.length} file${currentFiles.length===1?'':'s'} selected.`;
|
|
179
|
-
|
|
180
|
-
bubbles:true,
|
|
181
|
-
cancelable:false,
|
|
182
|
-
composed:true,
|
|
183
|
-
detail:{files:currentFiles.slice(),source}
|
|
184
|
-
}));
|
|
207
|
+
emit('file-drop-selected',{files:currentFiles.slice(),source},{fileCount:currentFiles.length,source:String(source)},operationId);
|
|
185
208
|
return currentFiles.slice();
|
|
186
209
|
}
|
|
187
210
|
|
|
188
211
|
function setBusy(value=true,message=''){
|
|
212
|
+
if(destroyed){
|
|
213
|
+
return busy;
|
|
214
|
+
}
|
|
189
215
|
busy=Boolean(value);
|
|
190
216
|
choose.disabled=busy;
|
|
191
217
|
picker.disabled=busy;
|
|
@@ -196,15 +222,14 @@
|
|
|
196
222
|
statusElement.className='status';
|
|
197
223
|
statusElement.innerText=`${message}`;
|
|
198
224
|
}
|
|
199
|
-
|
|
200
|
-
bubbles:true,
|
|
201
|
-
composed:true,
|
|
202
|
-
detail:{busy,progress}
|
|
203
|
-
}));
|
|
225
|
+
emit('file-drop-state',{busy,progress},{busy,hasProgress:progress!==null});
|
|
204
226
|
return busy;
|
|
205
227
|
}
|
|
206
228
|
|
|
207
229
|
function setProgress(value=null,label=''){
|
|
230
|
+
if(destroyed){
|
|
231
|
+
return progress;
|
|
232
|
+
}
|
|
208
233
|
const numeric=Number(value);
|
|
209
234
|
progress=value===null||value===undefined||!Number.isFinite(numeric)
|
|
210
235
|
?null
|
|
@@ -218,30 +243,35 @@
|
|
|
218
243
|
progressElement.removeAttribute('value');
|
|
219
244
|
progressLabel.innerText='';
|
|
220
245
|
}
|
|
221
|
-
|
|
222
|
-
bubbles:true,
|
|
223
|
-
composed:true,
|
|
224
|
-
detail:{progress,label:progressLabel.innerText}
|
|
225
|
-
}));
|
|
246
|
+
emit('file-drop-progress',{progress,label:progressLabel.innerText},{progress});
|
|
226
247
|
return progress;
|
|
227
248
|
}
|
|
228
249
|
|
|
229
|
-
function setError(message=''){
|
|
250
|
+
function setError(message='',errorDetail={},operationId=null){
|
|
251
|
+
if(destroyed){
|
|
252
|
+
return '';
|
|
253
|
+
}
|
|
230
254
|
const text=`${message||''}`;
|
|
231
255
|
feedback.hidden=!text&&progress===null;
|
|
232
256
|
statusElement.innerText=text;
|
|
233
257
|
statusElement.className=text?'status arcane-error':'status';
|
|
234
258
|
if(text){
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
259
|
+
const code=String(errorDetail?.code||'ARCANE_FILE_DROP_INPUT_REJECTED');
|
|
260
|
+
const reason=String(errorDetail?.reason||'file-input-rejected');
|
|
261
|
+
emit(
|
|
262
|
+
'file-drop-error',
|
|
263
|
+
{message:text,code,reason},
|
|
264
|
+
{code,reason},
|
|
265
|
+
operationId||`${events.descriptor.instanceId}:error:${++operationSequence}`
|
|
266
|
+
);
|
|
240
267
|
}
|
|
241
268
|
return text;
|
|
242
269
|
}
|
|
243
270
|
|
|
244
271
|
function clear(){
|
|
272
|
+
if(destroyed){
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
245
275
|
currentFiles=[];
|
|
246
276
|
picker.value='';
|
|
247
277
|
setBusy(false);
|
|
@@ -256,9 +286,25 @@
|
|
|
256
286
|
return Number.isFinite(numeric)&&numeric>=0?numeric:fallback;
|
|
257
287
|
}
|
|
258
288
|
|
|
289
|
+
function destroy(){
|
|
290
|
+
if(destroyed){
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
destroyed=true;
|
|
294
|
+
lifecycleController.abort('file-drop-destroyed');
|
|
295
|
+
dragDepth=0;
|
|
296
|
+
busy=false;
|
|
297
|
+
currentFiles=[];
|
|
298
|
+
progress=null;
|
|
299
|
+
picker.value='';
|
|
300
|
+
events.dispose();
|
|
301
|
+
host.ready=false;
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
259
305
|
configure(datasetOptions());
|
|
260
306
|
dropZone.dataset.dragActive='false';
|
|
261
307
|
setBusy(false);
|
|
262
308
|
host.ready=true;
|
|
263
|
-
|
|
309
|
+
emit('file-drop-ready',{}, {ready:true});
|
|
264
310
|
</script>
|