arcane-os 0.16.3 → 0.17.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.0
4
+
5
+ - Add neutral `modal.configure({dismissible})` configuration, defaulting to
6
+ `true`. Setting it to `false` hides the close button and prevents Escape,
7
+ backdrop and close-button dismissal while preserving the owner's existing
8
+ programmatic close/destroy lifecycle and running-task behavior.
9
+ - Retain the configuration through population, open/close cycles and task
10
+ completion. Keep focus within useful content when hiding a focused close
11
+ control, and preserve default modal behavior for existing consumers.
12
+
3
13
  ## 0.16.3
4
14
 
5
15
  - Remember browser-reported PWA installation in app-scoped DBOPFS and suppress
@@ -79,7 +79,7 @@ appropriate.
79
79
  | [`markdown-document.html`](#markdown-documenthtml) | Renders and navigates a complete Markdown document with focusable fragments. | `configure()`<br>`load()`<br>`render()`<br>`clear()`<br>`fail()`<br>`focus()`<br>`focusFragment()`<br>`destroy()` | `markdown-document-ready`<br>`markdown-document-state`<br>`markdown-document-loading`<br>`markdown-document-rendered`<br>`markdown-document-empty`<br>`markdown-document-error`<br>`markdown-document-navigate` | Complete Markdown/state normalized; malformed input and Marked/DOM failures remain visible |
80
80
  | [`markdown-editor.html`](#markdown-editorhtml) | Configurable Markdown authoring, toolbar, preview, title, and save surface. | `configure()`<br>`focus()`<br>`clear()`<br>`saveEntry()`<br>`destroy()` | `markdown-editor-ready`<br>`markdown-editor-change`<br>`markdown-editor-saved` | Editor values normalized; injected save result mixed |
81
81
  | [`media-embed.html`](#media-embedhtml) | Loads a parsed YouTube video or playlist embed with ordinary hosting by default, optional privacy enhancement, and an external-platform action. | `configure()`<br>`load()`<br>`destroy()` | `media-embed-ready`<br>`media-load`<br>`media-error`<br>`media-open-platform` | URL/error normalized; iframe/platform behavior native |
82
- | [`modal.html`](#modalhtml) | Generic modal with population, open/close, actions, and sequential task execution. | `populate()`<br>`open()`<br>`close()`<br>`runTasks()`<br>`destroy()` | `modal-ready`<br>`modal-opened`<br>`modal-closed`<br>`modal-action` | Modal state normalized; injected task results mixed |
82
+ | [`modal.html`](#modalhtml) | Generic modal with population, configurable user dismissal, actions, and concurrent task execution. | `configure()`<br>`populate()`<br>`open()`<br>`close()`<br>`runTasks()`<br>`destroy()`<br>`running`<br>`opened` | `modal-ready`<br>`modal-opened`<br>`modal-closed`<br>`modal-action` | Modal state normalized; injected task results mixed |
83
83
  | [`output-panel.html`](#output-panelhtml) | Presents status, output, body, coverage, actions, pending, error, and cleared states. | `configure()`<br>`setOutput()`<br>`setBody()`<br>`setCoverage()`<br>`setActions()`<br>`setPending()`<br>`setStatus()`<br>`setError()`<br>`clear()`<br>`destroy()` | `output-panel-ready`<br>`output-panel-state`<br>`output-panel-change`<br>`output-panel-action`<br>`output-panel-error`<br>`output-panel-cleared` | DOM-normalized |
84
84
  | [`preferences-form.html`](#preferences-formhtml) | Builds a schema-driven preferences form with submit, reset, busy, and status behavior. | `configure()`<br>`getValues()`<br>`setValues()`<br>`setBusy()`<br>`setStatus()`<br>`destroy()` | `preferences-form-ready`<br>`preferences-change`<br>`preferences-submit`<br>`preferences-reset` | Normalized form values |
85
85
  | [`pwa-install.html`](#pwa-installhtml) | Presents a dismissible browser installation action with floating or inline placement. | `configure()`<br>`install()`<br>`dismiss()`<br>`destroy()`<br>`state`<br>`ready` | `pwa-install-ready`<br>`pwa-install-change`<br>`pwa-install-dismissed` | Browser install availability and outcome supplied by the shared PWA owner |
@@ -944,11 +944,28 @@ Shared dependencies: [`YouTubeMedia.js`](runtime-modules.md#youtubemediajs).
944
944
 
945
945
  ### Overview
946
946
 
947
- Generic modal with population, open/close, actions, and sequential task execution.
947
+ Generic modal with population, configurable user dismissal, actions, and concurrent task execution.
948
948
 
949
949
  ### Public surface
950
950
 
951
- Methods/properties: `populate()`, `open()`, `close()`, `runTasks()`, `destroy()`.
951
+ Methods/properties: `configure()`, `populate()`, `open()`, `close()`, `runTasks()`,
952
+ `destroy()`, `running`, `opened`.
953
+
954
+ `configure({dismissible})` synchronously returns the current `{dismissible}`
955
+ configuration, or `false` after destruction. `dismissible` starts as `true`;
956
+ omitting it preserves the current choice. A supplied value must be a boolean.
957
+ Setting it to `false` hides the close button and prevents user dismissal through
958
+ Escape, backdrop clicks and the close button. Setting it back to `true` restores
959
+ normal user dismissal. The choice persists across population, open/close cycles
960
+ and task completion. If the close button held focus when disabled, focus moves
961
+ to a content control or the focusable modal body.
962
+
963
+ This option does not prevent the owning application from calling `close()` or
964
+ `destroy()`. The existing running-task behavior remains: `close()` leaves a
965
+ running modal open unless called with its existing second `force` argument set
966
+ to `true`; `destroy()` releases the component. Product-specific conditions for
967
+ closing a modal belong to the application. Content, actions and task results
968
+ are preserved, and `runTasks()` continues to execute independent jobs concurrently.
952
969
 
953
970
  Events: `modal-ready`, `modal-opened`, `modal-closed`, `modal-action`.
954
971
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.16.3",
3
+ "version": "0.17.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -68,7 +68,7 @@
68
68
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
69
69
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
70
70
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
71
- "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
71
+ "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
72
72
  "test:integration": "node ./bin/arcane-test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
73
73
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
74
74
  "check": "node tools/check-source.mjs && npm test",
@@ -150,7 +150,7 @@
150
150
  <button id="close" class="modal-button" part="close" type="button" aria-label="Close" title="Close">&#10005;</button>
151
151
  <header id="modal-header" class="modal-header" part="header" hidden><slot id="header-slot" name="header"></slot></header>
152
152
  <input type="search" id="modal-search" class="modal-search hidden" part="search" placeholder="Search..." aria-label="Search modal content">
153
- <div id="modal-content" class="modal-content" part="body"><slot name="body"></slot></div>
153
+ <div id="modal-content" class="modal-content" part="body" tabindex="-1"><slot name="body"></slot></div>
154
154
  <footer id="modal-footer" class="modal-footer" part="footer" hidden><slot id="footer-slot" name="footer"></slot></footer>
155
155
  </div>
156
156
  </dialog>
@@ -174,11 +174,13 @@
174
174
  const modalStack=window.modalStack=window.modalStack||[];
175
175
 
176
176
  let running=false;
177
+ let dismissible = true;
177
178
  let disposed=false;
178
179
  let activeRun=Promise.resolve([]);
179
180
  let returnFocus=null;
180
181
 
181
182
  host.ready=false;
183
+ host.configure = configure;
182
184
  host.populate=populate;
183
185
  host.open=open;
184
186
  host.close=close;
@@ -195,13 +197,18 @@
195
197
  opened:{get:function getOpenedState(){return dialog.open;}}
196
198
  });
197
199
 
198
- dialog.addEventListener('cancel',function preventRunningDialogCancellation(event){
199
- event.preventDefault();
200
- close(event);
201
- });
200
+ dialog.addEventListener(
201
+ 'cancel',
202
+ function handleDialogCancellation(event) {
203
+ event.preventDefault();
204
+ if (dismissible) {
205
+ close(event);
206
+ }
207
+ }
208
+ );
202
209
 
203
210
  dialog.addEventListener('click',function closeFromDialogBackdrop(event){
204
- if(event.target!==dialog||running){
211
+ if (event.target !== dialog || running || !dismissible) {
205
212
  return;
206
213
  }
207
214
  const bounds=dialog.getBoundingClientRect();
@@ -214,9 +221,45 @@
214
221
  }
215
222
  });
216
223
 
217
- closeButton.addEventListener('click',function closeFromButton(event){
218
- close(event);
219
- });
224
+ closeButton.addEventListener(
225
+ 'click',
226
+ function closeFromButton(event) {
227
+ if (dismissible) {
228
+ close(event);
229
+ }
230
+ }
231
+ );
232
+
233
+ function configure(input = {}) {
234
+ if (disposed) return false;
235
+ if (input === null || !is.object(input) || is.array(input)) {
236
+ throw new TypeError('Modal configuration must be an object.');
237
+ }
238
+ if (input.dismissible !== undefined) {
239
+ if (!is.boolean(input.dismissible)) {
240
+ throw new TypeError('Modal dismissible must be a boolean.');
241
+ }
242
+ dismissible = input.dismissible;
243
+ }
244
+ const closeButtonFocused = shadowRoot.activeElement === closeButton;
245
+ updateCloseButton();
246
+ if (dialog.open && !dismissible && closeButtonFocused) {
247
+ focusOpenedModal();
248
+ }
249
+ return {dismissible};
250
+ }
251
+
252
+ function updateCloseButton() {
253
+ closeButton.classList.toggle('hidden', running || !dismissible);
254
+ }
255
+
256
+ function focusOpenedModal() {
257
+ const focusTarget = modalContent.querySelector('[autofocus],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),a[href]')
258
+ || (running || !dismissible ? modalContent : closeButton);
259
+ focusTarget.focus(
260
+ {preventScroll: true}
261
+ );
262
+ }
220
263
 
221
264
  for(const slot of [headerSlot,footerSlot]){
222
265
  const region=slot.parentElement;
@@ -253,10 +296,7 @@
253
296
  ?document.activeElement
254
297
  :null;
255
298
  dialog.showModal();
256
- queueMicrotask(function focusOpenedModal(){
257
- const focusTarget=modalContent.querySelector('[autofocus],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),a[href]')||closeButton;
258
- focusTarget?.focus({preventScroll:true});
259
- });
299
+ queueMicrotask(focusOpenedModal);
260
300
  emit('modal-opened',{}, {opened:true});
261
301
  }
262
302
  return true;
@@ -274,7 +314,7 @@
274
314
  if(wasOpen){
275
315
  dialog.close();
276
316
  }
277
- closeButton.classList.remove('hidden');
317
+ updateCloseButton();
278
318
 
279
319
  const stackIndex=modalStack.indexOf(host);
280
320
  if(stackIndex>-1){
@@ -317,7 +357,7 @@
317
357
  }
318
358
 
319
359
  async function populate(content='<p>Content goes here</p>',search=false){
320
- closeButton.classList.remove('hidden');
360
+ updateCloseButton();
321
361
  modalSearch.classList.toggle('hidden',!search);
322
362
  if(content instanceof Node){
323
363
  modalContent.replaceChildren(content);
@@ -349,7 +389,7 @@
349
389
  }
350
390
 
351
391
  running=true;
352
- closeButton.classList.add('hidden');
392
+ updateCloseButton();
353
393
  modalSearch.classList.add('hidden');
354
394
  modalContent.replaceChildren();
355
395
 
@@ -399,16 +439,20 @@
399
439
  return result.status==='rejected';
400
440
  });
401
441
  running=false;
402
- closeButton.classList.remove('hidden');
442
+ updateCloseButton();
403
443
 
404
444
  if(failed){
405
445
  heading.innerText='Some Tasks Could Not Be Completed';
406
- message.innerText='You may close this message and try again.';
446
+ message.innerText = dismissible
447
+ ? 'You may close this message and try again.'
448
+ : 'Review the failed tasks and try again.';
407
449
  return results;
408
450
  }
409
451
 
410
452
  heading.innerText='All Tasks Complete';
411
- message.innerText='Every task completed. You may close this message.';
453
+ message.innerText = dismissible
454
+ ? 'Every task completed. You may close this message.'
455
+ : 'Every task completed.';
412
456
  return results;
413
457
  }
414
458