arcane-os 0.12.0 → 0.13.1
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 +24 -0
- package/README.md +4 -1
- package/browser-runtime/pwa-install.mjs +242 -0
- package/browser-runtime/pwa.mjs +2 -0
- package/docs/architecture.md +9 -0
- package/docs/reference/inventory/package-api.json +43 -1
- package/docs/reference/inventory/runtime-components.json +29 -1
- package/docs/reference/pwa.md +167 -4
- package/docs/reference/runtime-components.md +64 -0
- package/docs/reference/sdk-api.md +97 -1
- package/package.json +2 -2
- package/runtime/arcane/components/pwa-install.html +216 -0
- package/src/dev-server.mjs +146 -41
- package/src/pwa-worker.mjs +4 -6
- package/src/pwa.mjs +7 -1
|
@@ -82,6 +82,7 @@ appropriate.
|
|
|
82
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 |
|
|
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
|
+
| [`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 |
|
|
85
86
|
| [`record-timeline.html`](#record-timelinehtml) | Displays complete chronological records/evidence and emits open actions. | `setItems()`<br>`populate()`<br>`destroy()` | `record-timeline-ready`<br>`record-timeline-open` | Complete item fields and inventories preserved |
|
|
86
87
|
| [`relationship-board.html`](#relationship-boardhtml) | Displays complete normalized relationship nodes/edges in graph and list forms. | `setGraph()`<br>`populate()`<br>`destroy()` | `relationship-board-ready`<br>`relationship-node-open`<br>`relationship-edge-open` | Complete graph inventories and fields preserved |
|
|
87
88
|
| [`screen-capture.html`](#screen-capturehtml) | Presents image, video, or GIF display-capture workflow. | `capture` (`ScreenCapture` instance)<br>`destroy()` | `screen-capture-ready`<br>`screen-capture-result` | State/result normalized; media permission/codec failures mixed |
|
|
@@ -1018,6 +1019,69 @@ Events: `preferences-form-ready`, `preferences-change`, `preferences-submit`, `p
|
|
|
1018
1019
|
</html-import>
|
|
1019
1020
|
```
|
|
1020
1021
|
|
|
1022
|
+
## pwa-install.html
|
|
1023
|
+
|
|
1024
|
+
### Overview
|
|
1025
|
+
|
|
1026
|
+
A compact installation suggestion using the shared [PWA installation owner](pwa.md).
|
|
1027
|
+
The default floating panel appears near the top right only when the browser offers
|
|
1028
|
+
installation. It has an Install action and an explicit close button, takes no
|
|
1029
|
+
focus automatically, and has no dismissal timer. It uses the Arcane theme and
|
|
1030
|
+
primitives, wraps complete labels and errors, and scrolls its own content when
|
|
1031
|
+
the available height is limited. The parent page loads `ThemeBootstrap.js` to
|
|
1032
|
+
apply the user's appearance preferences.
|
|
1033
|
+
|
|
1034
|
+
### Public surface
|
|
1035
|
+
|
|
1036
|
+
`configure({appName, installLabel, closeLabel, promptingLabel, description,
|
|
1037
|
+
presentation})` updates display configuration and returns its current record.
|
|
1038
|
+
Labels remain complete strings. `appName` initially uses `data-app-name` or
|
|
1039
|
+
`this app`; the default button label is `Install`. `presentation` is `floating`
|
|
1040
|
+
by default or `inline`, initially read from `data-presentation`. Inline placement
|
|
1041
|
+
uses the parent's layout. Set `description` to an empty string when a compact
|
|
1042
|
+
placement needs no supporting text; supplied descriptions remain visible.
|
|
1043
|
+
|
|
1044
|
+
`install()` calls the shared owner's native prompt synchronously and returns its
|
|
1045
|
+
promise of the browser outcome or `null` when no prompt is available. Call it
|
|
1046
|
+
directly from a user action. The component's Install button already does this.
|
|
1047
|
+
An explicit request's complete failure message remains visible until dismissed;
|
|
1048
|
+
the method rejects with the same error. The browser controls actual installation.
|
|
1049
|
+
Acceptance hides the suggestion without claiming installation has completed.
|
|
1050
|
+
|
|
1051
|
+
`dismiss()` closes the component. Floating dismissal also uses the owner's
|
|
1052
|
+
session dismissal; an explicit inline component ignores that shared dismissal
|
|
1053
|
+
and closes only its own instance. Closing retains any unused native prompt at
|
|
1054
|
+
the shared owner. `destroy()` removes the component's listeners and subscription,
|
|
1055
|
+
disposes its event source, hides its host, and marks `ready` false; it does not
|
|
1056
|
+
dispose the shared owner or change browser installation state. Both methods
|
|
1057
|
+
return true while active and false after destruction; `destroy()` is idempotent.
|
|
1058
|
+
|
|
1059
|
+
The readonly `state` property returns the shared owner's current install-state
|
|
1060
|
+
record; `ready` becomes true after methods and the state subscription are attached.
|
|
1061
|
+
`pwa-install-ready` carries `{ready, state}`; `pwa-install-change` carries
|
|
1062
|
+
`{state, visible}` for each observed owner update; `pwa-install-dismissed` carries
|
|
1063
|
+
`{presentation, state}`. These follow the canonical event projection contract.
|
|
1064
|
+
|
|
1065
|
+
### Availability and normalization
|
|
1066
|
+
|
|
1067
|
+
The component requires HTMLImport and a DOM renderer. Native installation
|
|
1068
|
+
availability comes from the browser's `beforeinstallprompt` event through
|
|
1069
|
+
`getPwaInstall()`. Without an available event, the suggestion stays hidden;
|
|
1070
|
+
absence does not identify why installation is unavailable. Installed-app events,
|
|
1071
|
+
accepted prompts, and an already running installed display mode hide it.
|
|
1072
|
+
|
|
1073
|
+
### Example
|
|
1074
|
+
|
|
1075
|
+
```html
|
|
1076
|
+
<html-import
|
|
1077
|
+
id="install-app"
|
|
1078
|
+
href="/arcane/components/pwa-install.html"
|
|
1079
|
+
data-app-name="Example Library"
|
|
1080
|
+
data-presentation="inline"
|
|
1081
|
+
hidden>
|
|
1082
|
+
</html-import>
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1021
1085
|
## record-timeline.html
|
|
1022
1086
|
|
|
1023
1087
|
### Overview
|
|
@@ -39,7 +39,7 @@ for the installed-inventory-derived physical-runtime contract in SDK `0.5.18`.
|
|
|
39
39
|
| `arcane-os/speech-playback` | Portable speech preparation, playback state, and injected media adapters. |
|
|
40
40
|
| `arcane-os/speech-text` | Speech-input formatting cleanup for complete text and streamed chunks. |
|
|
41
41
|
| `arcane-os/browser-device` | Synchronous mobile or desktop identity hints for application-owned settings. |
|
|
42
|
-
| `arcane-os/pwa` | Nonblocking PWA registration,
|
|
42
|
+
| `arcane-os/pwa` | Nonblocking PWA registration, worker updates, native installation state and a dismissible installation component. |
|
|
43
43
|
| `arcane-os/ai/browser-wasm` | Caller-selected browser-local Wllama inference, complete DBOPFS model storage, streaming, cancellation, and structural tool-call results. |
|
|
44
44
|
| `arcane-os/ai/browser-speech` | Caller-selected browser-local Whisper STT and Kokoro TTS provider mechanisms, ordinary upstream assets, materialized/native routing, Workers, and cancellation. |
|
|
45
45
|
| `arcane-os/mail` | Portable Mail runtime, durable outbox, complete transport responses, and provider-neutral acceptance contracts. |
|
|
@@ -129,6 +129,9 @@ browser map are cataloged separately in [Runtime modules](runtime-modules.md).
|
|
|
129
129
|
| `getBrowserDeviceClass()` | function | `arcane-os/browser-device` | Browser device settings | Node and Browser; synchronous identity hint with no model, storage, or GPU operation |
|
|
130
130
|
| `PWA_STATE_EVENT` | constant | `arcane-os/pwa` | Progressive web applications | Browser lifecycle event name; importable without registration |
|
|
131
131
|
| `registerPwa()` | function | `arcane-os/pwa` | Progressive web applications | Browser service workers; synchronous unsupported state when unavailable |
|
|
132
|
+
| `PWA_INSTALL_STATE_EVENT` | constant | `arcane-os/pwa` | Progressive web applications | Browser installation event name; importable without starting observation |
|
|
133
|
+
| `getPwaInstall()` | function | `arcane-os/pwa` | Progressive web applications | Browser native installation events; waiting state while no prompt is available |
|
|
134
|
+
| `mountPwaInstallPrompt()` | function | `arcane-os/pwa` | Progressive web applications | Browser document and managed HTML import; resolves to null without a document |
|
|
132
135
|
| `createDbopfsSpeechArtifactStore()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser with ready DBOPFS, Web Locks, Fetch, File/Blob, and object URLs |
|
|
133
136
|
| `removeBrowserSpeechModelCache()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser CacheStorage; explicit removal of one selected upstream model |
|
|
134
137
|
| `createNativeBuildPlan()` | function | `arcane-os` | Targets, native plans, and providers | Node; selected browser/native target or provider as documented |
|
|
@@ -6124,6 +6127,99 @@ current-state replay, disposal, offline resource ownership and update behavior.
|
|
|
6124
6127
|
The generated bootstrap registers automatically; a manual caller owns a separate
|
|
6125
6128
|
entry point and must observe its `ready` rejection.
|
|
6126
6129
|
|
|
6130
|
+
## PWA_INSTALL_STATE_EVENT
|
|
6131
|
+
|
|
6132
|
+
### Overview
|
|
6133
|
+
|
|
6134
|
+
The exact event name `arcane.pwa.install.state` identifies native installation
|
|
6135
|
+
availability and choice state through the existing Arcane event owner. It is
|
|
6136
|
+
separate from service-worker registration state. Importing it starts no
|
|
6137
|
+
observation or installation.
|
|
6138
|
+
|
|
6139
|
+
### Example
|
|
6140
|
+
|
|
6141
|
+
```javascript
|
|
6142
|
+
import {PWA_INSTALL_STATE_EVENT} from 'arcane-os/pwa';
|
|
6143
|
+
console.log(PWA_INSTALL_STATE_EVENT);
|
|
6144
|
+
```
|
|
6145
|
+
|
|
6146
|
+
See the [installation state contract](pwa.md#getpwainstall) for the complete
|
|
6147
|
+
payload and current-state subscription behavior.
|
|
6148
|
+
|
|
6149
|
+
## getPwaInstall()
|
|
6150
|
+
|
|
6151
|
+
### Overview
|
|
6152
|
+
|
|
6153
|
+
`getPwaInstall()` returns one synchronous page owner exposing `state`,
|
|
6154
|
+
`subscribe`, `prompt`, `dismiss` and `dispose`. It captures the browser's
|
|
6155
|
+
`beforeinstallprompt` event and observes `appinstalled` and app display-mode
|
|
6156
|
+
changes. A missing event leaves installation availability unknown and the owner
|
|
6157
|
+
waiting; it does not prove browser incompatibility.
|
|
6158
|
+
|
|
6159
|
+
State contains `status`, `available`, `dismissed`, `outcome` and `error`.
|
|
6160
|
+
Subscriptions replay current state by default. `prompt()` must be called
|
|
6161
|
+
directly within the install click to preserve native user activation. It
|
|
6162
|
+
consumes the event once and returns the browser choice, resolves to `null` when
|
|
6163
|
+
unavailable, or rejects with the actual error. `dismiss()` remembers the
|
|
6164
|
+
session's floating-suggestion dismissal without consuming a retained event.
|
|
6165
|
+
|
|
6166
|
+
### Example
|
|
6167
|
+
|
|
6168
|
+
```javascript
|
|
6169
|
+
import {getPwaInstall} from 'arcane-os/pwa';
|
|
6170
|
+
|
|
6171
|
+
const install = getPwaInstall();
|
|
6172
|
+
const button = document.querySelector('#install');
|
|
6173
|
+
install.subscribe(function updateInstallButton(state) {
|
|
6174
|
+
button.hidden = !state.available;
|
|
6175
|
+
});
|
|
6176
|
+
button.addEventListener('click', function requestInstallation() {
|
|
6177
|
+
install.prompt().catch(function reportInstallFailure(error) {
|
|
6178
|
+
console.error(error);
|
|
6179
|
+
});
|
|
6180
|
+
});
|
|
6181
|
+
```
|
|
6182
|
+
|
|
6183
|
+
The [PWA guide](pwa.md#getpwainstall) defines every status, event lifetime,
|
|
6184
|
+
session dismissal, errors and disposal. Disposing this shared owner removes
|
|
6185
|
+
its page-level observation; components normally unsubscribe only their own
|
|
6186
|
+
listener. Installation state does not establish offline readiness or completed
|
|
6187
|
+
Android WebAPK creation.
|
|
6188
|
+
|
|
6189
|
+
## mountPwaInstallPrompt()
|
|
6190
|
+
|
|
6191
|
+
### Overview
|
|
6192
|
+
|
|
6193
|
+
`mountPwaInstallPrompt({appName = ''} = {})` starts the shared install observer
|
|
6194
|
+
before asynchronously mounting one initially hidden `pwa-install.html`
|
|
6195
|
+
component. Repeated calls return the same mounting promise. It resolves to the
|
|
6196
|
+
ready `html-import` host, or `null` without a document or when the owner is
|
|
6197
|
+
disposed before mounting. Component-load failure rejects; removal or disposal
|
|
6198
|
+
during loading rejects with `AbortError`. A rejected mount permits another
|
|
6199
|
+
explicit attempt. The first call
|
|
6200
|
+
supplies the initial app name.
|
|
6201
|
+
|
|
6202
|
+
The generated PWA bootstrap calls this automatically with the manifest name.
|
|
6203
|
+
Component mounting and worker registration run independently, and application
|
|
6204
|
+
rendering must not await them. The floating component offers Install and a
|
|
6205
|
+
manual close control; an application can also place the same component inline.
|
|
6206
|
+
|
|
6207
|
+
### Example
|
|
6208
|
+
|
|
6209
|
+
```javascript
|
|
6210
|
+
import {mountPwaInstallPrompt} from 'arcane-os/pwa';
|
|
6211
|
+
|
|
6212
|
+
mountPwaInstallPrompt({appName: 'Example Library'}).catch(
|
|
6213
|
+
function reportInstallComponentFailure(error) {
|
|
6214
|
+
console.error(error);
|
|
6215
|
+
}
|
|
6216
|
+
);
|
|
6217
|
+
```
|
|
6218
|
+
|
|
6219
|
+
See the [mounting contract](pwa.md#mountpwainstallprompt) and
|
|
6220
|
+
[component reference](runtime-components.md#pwa-installhtml). The browser owns
|
|
6221
|
+
native installation eligibility and URL-bar promotion.
|
|
6222
|
+
|
|
6127
6223
|
## createBrowserWasmLlmProvider()
|
|
6128
6224
|
|
|
6129
6225
|
### Overview
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
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",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
|
|
68
68
|
"test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
|
|
69
69
|
"test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.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",
|
|
70
|
-
"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-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",
|
|
70
|
+
"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
71
|
"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",
|
|
72
72
|
"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",
|
|
73
73
|
"check": "node tools/check-source.mjs && npm test",
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
<link rel="stylesheet" href="./arcane/css/theme.css?v=1">
|
|
2
|
+
<link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
|
|
3
|
+
<style>
|
|
4
|
+
:host{
|
|
5
|
+
display:block;
|
|
6
|
+
inset-block-start:4.5rem;
|
|
7
|
+
inset-inline-end:1rem;
|
|
8
|
+
max-width:calc(100vw - 2rem);
|
|
9
|
+
min-width:0;
|
|
10
|
+
position:fixed;
|
|
11
|
+
width:22rem;
|
|
12
|
+
z-index:110;
|
|
13
|
+
}
|
|
14
|
+
:host([hidden]){display:none}
|
|
15
|
+
:host([data-presentation="inline"]){
|
|
16
|
+
max-width:100%;
|
|
17
|
+
position:static;
|
|
18
|
+
width:fit-content;
|
|
19
|
+
}
|
|
20
|
+
.panel{
|
|
21
|
+
background:var(--arcane-surface-raised);
|
|
22
|
+
border:1px solid var(--arcane-border);
|
|
23
|
+
border-radius:var(--arcane-radius-medium);
|
|
24
|
+
box-shadow:var(--arcane-shadow-medium);
|
|
25
|
+
box-sizing:border-box;
|
|
26
|
+
color:var(--text-color);
|
|
27
|
+
display:grid;
|
|
28
|
+
gap:var(--arcane-space-2);
|
|
29
|
+
max-height:calc(100vh - 5.5rem);
|
|
30
|
+
max-height:calc(100dvh - 5.5rem);
|
|
31
|
+
overflow:auto;
|
|
32
|
+
padding:var(--arcane-space-3);
|
|
33
|
+
}
|
|
34
|
+
.heading{align-items:start;display:flex;gap:var(--arcane-space-2)}
|
|
35
|
+
.copy{flex:1;min-width:0}
|
|
36
|
+
h2,p{margin:0;overflow-wrap:anywhere;white-space:pre-wrap}
|
|
37
|
+
h2{font-size:1rem;line-height:1.4}
|
|
38
|
+
.description{color:var(--arcane-muted);font-size:.9rem;line-height:1.45}
|
|
39
|
+
.close{flex:none}
|
|
40
|
+
.install{justify-self:start;max-width:100%;overflow-wrap:anywhere;white-space:normal}
|
|
41
|
+
.status{font-size:.9rem;line-height:1.45}
|
|
42
|
+
.status[data-error="true"]{color:var(--arcane-danger)}
|
|
43
|
+
:host([data-presentation="inline"]) .panel{box-shadow:none}
|
|
44
|
+
@media(prefers-reduced-motion:reduce){.install{transition:none}}
|
|
45
|
+
@media(forced-colors:active){.panel,.install,.close{border:1px solid CanvasText}}
|
|
46
|
+
</style>
|
|
47
|
+
|
|
48
|
+
<section class="panel" aria-labelledby="installTitle">
|
|
49
|
+
<div class="heading">
|
|
50
|
+
<div class="copy">
|
|
51
|
+
<h2 id="installTitle">Install this app</h2>
|
|
52
|
+
<p id="description" class="description">Open this app directly from your device.</p>
|
|
53
|
+
</div>
|
|
54
|
+
<button id="close" class="close arcane-close-button" type="button" aria-label="Close installation suggestion">✕</button>
|
|
55
|
+
</div>
|
|
56
|
+
<button id="install" class="install arcane-button" type="button">Install</button>
|
|
57
|
+
<p id="status" class="status" role="status" aria-live="polite" hidden></p>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<script type="module">
|
|
61
|
+
const host=this;
|
|
62
|
+
host.hidden=true;
|
|
63
|
+
const [
|
|
64
|
+
{default:Is},
|
|
65
|
+
{getPwaInstall},
|
|
66
|
+
{createArcaneEventSource,projectArcaneDOMEvent},
|
|
67
|
+
{arcaneLogging}
|
|
68
|
+
]=await Promise.all([
|
|
69
|
+
import('strong-type'),
|
|
70
|
+
import('arcane-os/pwa'),
|
|
71
|
+
import('arcane-os/event-manager'),
|
|
72
|
+
import('arcane-os/logging')
|
|
73
|
+
]);
|
|
74
|
+
const is=new Is(false);
|
|
75
|
+
const root=host.shadowRoot;
|
|
76
|
+
const title=root.querySelector('#installTitle');
|
|
77
|
+
const description=root.querySelector('#description');
|
|
78
|
+
const installButton=root.querySelector('#install');
|
|
79
|
+
const closeButton=root.querySelector('#close');
|
|
80
|
+
const status=root.querySelector('#status');
|
|
81
|
+
const lifecycleController=new AbortController();
|
|
82
|
+
const events=createArcaneEventSource(host,{
|
|
83
|
+
source:'arcane.component.pwa-install',
|
|
84
|
+
eventTypes:['pwa-install-ready','pwa-install-change','pwa-install-dismissed']
|
|
85
|
+
});
|
|
86
|
+
const owner=getPwaInstall();
|
|
87
|
+
let options={
|
|
88
|
+
appName:host.dataset.appName||'this app',
|
|
89
|
+
installLabel:'Install',
|
|
90
|
+
closeLabel:'Close installation suggestion',
|
|
91
|
+
promptingLabel:'Choose an option in the browser installation prompt.',
|
|
92
|
+
description:'Open this app directly from your device.',
|
|
93
|
+
presentation:host.dataset.presentation||'floating'
|
|
94
|
+
};
|
|
95
|
+
let currentState=owner.state;
|
|
96
|
+
let locallyDismissed=false;
|
|
97
|
+
let pending=false;
|
|
98
|
+
let requestError=null;
|
|
99
|
+
let destroyed=false;
|
|
100
|
+
|
|
101
|
+
host.configure=configure;
|
|
102
|
+
host.install=install;
|
|
103
|
+
host.dismiss=dismiss;
|
|
104
|
+
host.destroy=destroy;
|
|
105
|
+
Object.defineProperty(host,'state',{configurable:true,get:getState});
|
|
106
|
+
|
|
107
|
+
function emit(type,detail){
|
|
108
|
+
if(destroyed)return false;
|
|
109
|
+
const publication=events.dispatch(type,detail);
|
|
110
|
+
return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getState(){
|
|
114
|
+
return {...currentState};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function configure(input={}){
|
|
118
|
+
if(destroyed)return false;
|
|
119
|
+
if(input===null||!is.object(input)||is.array(input)){
|
|
120
|
+
throw new TypeError('PWA install configuration must be an object.');
|
|
121
|
+
}
|
|
122
|
+
const next={...options,...input};
|
|
123
|
+
for(const key of ['appName','installLabel','closeLabel','promptingLabel','description']){
|
|
124
|
+
if(!is.string(next[key]))throw new TypeError(`PWA install ${key} must be a string.`);
|
|
125
|
+
}
|
|
126
|
+
if(next.presentation!=='floating'&&next.presentation!=='inline'){
|
|
127
|
+
throw new TypeError('PWA install presentation must be floating or inline.');
|
|
128
|
+
}
|
|
129
|
+
options=next;
|
|
130
|
+
host.dataset.presentation=options.presentation;
|
|
131
|
+
title.textContent=`Install ${options.appName}`;
|
|
132
|
+
description.textContent=options.description;
|
|
133
|
+
installButton.textContent=options.installLabel;
|
|
134
|
+
closeButton.setAttribute('aria-label',options.closeLabel);
|
|
135
|
+
render();
|
|
136
|
+
return {...options};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function render(){
|
|
140
|
+
if(destroyed)return;
|
|
141
|
+
const terminal=['accepted','installed','running','disposed'].includes(currentState.status);
|
|
142
|
+
if(terminal)requestError=null;
|
|
143
|
+
const dismissed=locallyDismissed
|
|
144
|
+
||(options.presentation==='floating'&¤tState.dismissed);
|
|
145
|
+
host.hidden=terminal||dismissed||(!currentState.available&&!pending&&!requestError);
|
|
146
|
+
installButton.disabled=pending||!currentState.available;
|
|
147
|
+
const message=requestError
|
|
148
|
+
?is.string(requestError.message)?requestError.message:String(requestError)
|
|
149
|
+
:pending?options.promptingLabel:'';
|
|
150
|
+
status.textContent=message;
|
|
151
|
+
status.hidden=!message;
|
|
152
|
+
status.dataset.error=String(Boolean(requestError));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function observeInstallState(state){
|
|
156
|
+
if(destroyed)return;
|
|
157
|
+
currentState=state;
|
|
158
|
+
render();
|
|
159
|
+
emit('pwa-install-change',{state:getState(),visible:!host.hidden});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function install(){
|
|
163
|
+
if(destroyed||pending||!currentState.available)return Promise.resolve(null);
|
|
164
|
+
locallyDismissed=false;
|
|
165
|
+
requestError=null;
|
|
166
|
+
pending=true;
|
|
167
|
+
render();
|
|
168
|
+
let operation;
|
|
169
|
+
try{
|
|
170
|
+
// Keep the browser prompt inside the initiating click's user activation.
|
|
171
|
+
operation=owner.prompt();
|
|
172
|
+
}catch(error){
|
|
173
|
+
operation=Promise.reject(error);
|
|
174
|
+
}
|
|
175
|
+
return Promise.resolve(operation).catch(function reportPromptError(error){
|
|
176
|
+
if(!destroyed)requestError=error;
|
|
177
|
+
throw error;
|
|
178
|
+
}).finally(function settlePrompt(){
|
|
179
|
+
if(destroyed)return;
|
|
180
|
+
pending=false;
|
|
181
|
+
render();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function dismiss(){
|
|
186
|
+
if(destroyed)return false;
|
|
187
|
+
locallyDismissed=true;
|
|
188
|
+
requestError=null;
|
|
189
|
+
if(options.presentation==='floating')owner.dismiss();
|
|
190
|
+
render();
|
|
191
|
+
emit('pwa-install-dismissed',{presentation:options.presentation,state:getState()});
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function destroy(){
|
|
196
|
+
if(destroyed)return false;
|
|
197
|
+
destroyed=true;
|
|
198
|
+
lifecycleController.abort('pwa-install-destroyed');
|
|
199
|
+
events.dispose();
|
|
200
|
+
host.hidden=true;
|
|
201
|
+
host.ready=false;
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
installButton.addEventListener('click',function requestInstallation(){
|
|
206
|
+
install().catch(function reportInstallationFailure(error){
|
|
207
|
+
arcaneLogging.error('Unable to open the browser installation prompt:',error);
|
|
208
|
+
});
|
|
209
|
+
},{signal:lifecycleController.signal});
|
|
210
|
+
closeButton.addEventListener('click',dismiss,{signal:lifecycleController.signal});
|
|
211
|
+
|
|
212
|
+
configure();
|
|
213
|
+
owner.subscribe(observeInstallState,{signal:lifecycleController.signal});
|
|
214
|
+
host.ready=true;
|
|
215
|
+
emit('pwa-install-ready',{ready:true,state:getState()});
|
|
216
|
+
</script>
|