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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.1
4
+
5
+ - Refresh live development app membership from the current authored descriptor
6
+ or package-only configuration. New explicit includes, exclusions, entry paths,
7
+ and PWA settings take effect on the next relevant request without a server
8
+ restart or writes to consumer projections.
9
+ - Generate PWA metadata and offline inventories from the same app snapshot.
10
+ Coalesce concurrent metadata reads and inventory work within that snapshot,
11
+ and keep older inventory completion from replacing newer metadata.
12
+
13
+ ## 0.13.0
14
+
15
+ - Add the reusable `pwa-install.html` component and shared browser installation
16
+ lifecycle through `arcane-os/pwa`. Offer a compact themed Install action and
17
+ explicit Close, retain session dismissal, support inline placement, and surface
18
+ native prompt errors without claiming installation completion.
19
+ - Mount the component from generated PWA bootstraps independently of service
20
+ worker registration and application rendering. Use native installation and
21
+ display-mode events without polling, automatic native prompts, or focus capture.
22
+ - Return cached resources immediately while conditional background refreshes
23
+ are pending or in flight, so page requests do not wait on network revalidation.
24
+ - Document browser icon eligibility and the separate responsibilities of the
25
+ Web App Manifest, application file selection, and offline resource inventory.
26
+
3
27
  ## 0.12.0
4
28
 
5
29
  - Refresh the selected authored app descriptor's package projection before
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.12.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.13.0` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -30,6 +30,9 @@ page-load revalidation and persistent resource caches. The SDK records one
30
30
  completed-check timestamp per app/cache in DBOPFS and checks after 120 seconds
31
31
  in development or 15 minutes in packaged browser delivery. Apps retain ownership
32
32
  of branding, offline page selection and network-dependent product behavior.
33
+ Enabled browser delivery also mounts the shared, themed installation suggestion.
34
+ It appears when the browser offers installation, includes Install and Close,
35
+ and remembers dismissal for the tab session without interrupting page startup.
33
36
 
34
37
  That registry query is a maintainer action, not an application behavior. Apps
35
38
  never poll npm for SDK updates or replace their own SDK or synchronized runtime.
@@ -0,0 +1,242 @@
1
+ import Is from './dependencies/strong-type/index.js';
2
+ import {createArcaneEventSource} from './event-manager.mjs';
3
+
4
+ const is = new Is(false);
5
+ export const PWA_INSTALL_STATE_EVENT = 'arcane.pwa.install.state';
6
+ let sharedOwner = null;
7
+ let mountedPrompt = null;
8
+
9
+ /** Capture native installation availability once per page, before loading UI. */
10
+ export function getPwaInstall() {
11
+ if (sharedOwner && sharedOwner.state.status !== 'disposed') {
12
+ return sharedOwner;
13
+ }
14
+ const owner = {get state() { return snapshot(); }, subscribe, prompt, dismiss, dispose};
15
+ const source = createArcaneEventSource(owner, {
16
+ source: 'arcane.pwa.install', eventTypes: [PWA_INSTALL_STATE_EVENT]
17
+ });
18
+ const listeners = [];
19
+ const displayMode = globalThis.matchMedia?.(
20
+ '(display-mode: standalone), (display-mode: minimal-ui), '
21
+ + '(display-mode: fullscreen), (display-mode: window-controls-overlay)'
22
+ );
23
+ const manifestUrl = globalThis.document?.querySelector('link[rel~="manifest"]')?.href;
24
+ const dismissalKey = `arcane.pwa.install.dismissed:${manifestUrl ?? globalThis.location?.href ?? ''}`;
25
+ let deferredPrompt = null;
26
+ let disposed = false;
27
+ let dismissed = false;
28
+ let status = isRunningAsApp() ? 'running' : 'waiting';
29
+ let outcome = null;
30
+ let error = null;
31
+ try {
32
+ dismissed = globalThis.sessionStorage?.getItem(dismissalKey) === 'true';
33
+ } catch (storageError) {
34
+ console.warn('Arcane PWA install dismissal could not be read:', storageError);
35
+ }
36
+
37
+ function isRunningAsApp() {
38
+ return displayMode?.matches === true || globalThis.navigator?.standalone === true;
39
+ }
40
+
41
+ function snapshot() {
42
+ return {status, available: deferredPrompt !== null && !disposed,
43
+ dismissed, outcome, error};
44
+ }
45
+
46
+ function publish(nextStatus, nextError = null) {
47
+ if (disposed) return;
48
+ status = nextStatus;
49
+ error = nextError;
50
+ source.dispatch(PWA_INSTALL_STATE_EVENT, snapshot());
51
+ }
52
+
53
+ function observe(target, type, listener) {
54
+ if (!is.function(target?.addEventListener)) return;
55
+ target.addEventListener(type, listener);
56
+ listeners.push(function removeInstallListener() {
57
+ target.removeEventListener(type, listener);
58
+ });
59
+ }
60
+
61
+ function rememberDismissal() {
62
+ dismissed = true;
63
+ try {
64
+ globalThis.sessionStorage?.setItem(dismissalKey, 'true');
65
+ } catch (storageError) {
66
+ console.warn('Arcane PWA install dismissal could not be saved:', storageError);
67
+ }
68
+ }
69
+
70
+ function onBeforeInstallPrompt(event) {
71
+ if (disposed || isRunningAsApp() || status === 'installed' || status === 'accepted') return;
72
+ event.preventDefault();
73
+ deferredPrompt = event;
74
+ outcome = null;
75
+ publish('available');
76
+ }
77
+
78
+ function onInstalled() {
79
+ deferredPrompt = null;
80
+ // This event may precede Android's completion of WebAPK creation.
81
+ publish('installed');
82
+ }
83
+
84
+ function onDisplayModeChange() {
85
+ if (isRunningAsApp()) {
86
+ deferredPrompt = null;
87
+ publish('running');
88
+ } else if (status === 'running') {
89
+ publish('waiting');
90
+ }
91
+ }
92
+
93
+ function onPageHide(event) {
94
+ if (!event.persisted) dispose();
95
+ }
96
+
97
+ function subscribe(listener, {emitCurrent = true, signal} = {}) {
98
+ function forwardInstallState(event) { listener(event.detail); }
99
+ const unsubscribe = source.on(PWA_INSTALL_STATE_EVENT, forwardInstallState,
100
+ signal ? {signal} : undefined);
101
+ try {
102
+ if (emitCurrent && !signal?.aborted) listener(snapshot());
103
+ } catch (listenerError) {
104
+ unsubscribe();
105
+ throw listenerError;
106
+ }
107
+ return unsubscribe;
108
+ }
109
+
110
+ function prompt() {
111
+ if (disposed || !deferredPrompt) return Promise.resolve(null);
112
+ const event = deferredPrompt;
113
+ deferredPrompt = null;
114
+ publish('prompting');
115
+ let result;
116
+ try {
117
+ // Native user activation must reach prompt() in the same click stack.
118
+ result = event.prompt();
119
+ } catch (promptError) {
120
+ return rejectPrompt(promptError);
121
+ }
122
+ return Promise.resolve(result).then(async function receiveInstallChoice(value) {
123
+ const choice = value ?? await event.userChoice;
124
+ if (!choice || !['accepted', 'dismissed'].includes(choice.outcome)) {
125
+ throw new Error('The browser did not return an installation choice.');
126
+ }
127
+ if (!disposed) {
128
+ outcome = choice.outcome;
129
+ if (status === 'installed' || status === 'running') {
130
+ publish(status);
131
+ } else {
132
+ if (outcome === 'dismissed') rememberDismissal();
133
+ publish(outcome);
134
+ }
135
+ }
136
+ return choice;
137
+ }).catch(rejectPrompt);
138
+ }
139
+
140
+ function rejectPrompt(promptError) {
141
+ if (status !== 'installed' && status !== 'running') publish('error', promptError);
142
+ return Promise.reject(promptError);
143
+ }
144
+
145
+ function dismiss() {
146
+ if (disposed) return snapshot();
147
+ rememberDismissal();
148
+ publish(status, error);
149
+ return snapshot();
150
+ }
151
+
152
+ function dispose() {
153
+ if (disposed) return;
154
+ deferredPrompt = null;
155
+ publish('disposed');
156
+ disposed = true;
157
+ for (const removeListener of listeners) removeListener();
158
+ listeners.length = 0;
159
+ source.dispose();
160
+ }
161
+
162
+ observe(globalThis, 'beforeinstallprompt', onBeforeInstallPrompt);
163
+ observe(globalThis, 'appinstalled', onInstalled);
164
+ observe(displayMode, 'change', onDisplayModeChange);
165
+ observe(globalThis, 'pagehide', onPageHide);
166
+ sharedOwner = owner;
167
+ return owner;
168
+ }
169
+
170
+ /** Mount one shared, initially hidden install component without delaying the app. */
171
+ export function mountPwaInstallPrompt({appName = ''} = {}) {
172
+ const owner = getPwaInstall();
173
+ if (mountedPrompt) return mountedPrompt;
174
+ mountedPrompt = mountComponent().catch(function releaseFailedMount(error) {
175
+ mountedPrompt = null;
176
+ throw error;
177
+ });
178
+ return mountedPrompt;
179
+
180
+ async function mountComponent() {
181
+ if (!globalThis.document) return null;
182
+ // Both modules may start independently; saved theme loading is not a barrier.
183
+ await Promise.all([
184
+ import(new URL('../modules/HTMLImport.js', import.meta.url).href),
185
+ import(new URL('../modules/ThemeBootstrap.js', import.meta.url).href)
186
+ ]);
187
+ if (owner.state.status === 'disposed') return null;
188
+ if (!document.body) {
189
+ await new Promise(function waitForComponentParent(resolve) {
190
+ const unsubscribe = owner.subscribe(function observeParentWaitDisposal(state) {
191
+ if (state.status === 'disposed') completeParentWait();
192
+ }, {emitCurrent: false});
193
+ function completeParentWait() {
194
+ document.removeEventListener('DOMContentLoaded', completeParentWait);
195
+ unsubscribe();
196
+ resolve();
197
+ }
198
+ document.addEventListener('DOMContentLoaded', completeParentWait, {once: true});
199
+ });
200
+ }
201
+ if (owner.state.status === 'disposed') return null;
202
+ const host = document.createElement('html-import');
203
+ host.hidden = true;
204
+ host.dataset.appName = String(appName);
205
+ host.dataset.arcanePwaInstall = '';
206
+ host.setAttribute('href', new URL('../components/pwa-install.html', import.meta.url).href);
207
+ return new Promise(function waitForInstallComponent(resolve, reject) {
208
+ const observer = new MutationObserver(function observeRemovedInstallComponent() {
209
+ if (!host.isConnected) cancelMount();
210
+ });
211
+ const unsubscribe = owner.subscribe(function observeDisposedInstallOwner(state) {
212
+ if (state.status === 'disposed') cancelMount();
213
+ }, {emitCurrent: false});
214
+ function cleanup() {
215
+ observer.disconnect();
216
+ unsubscribe();
217
+ host.removeEventListener('html-import-ready', onReady);
218
+ host.removeEventListener('html-import-error', onError);
219
+ }
220
+ function cancelMount() {
221
+ cleanup();
222
+ host.remove();
223
+ const error = new Error('The PWA install component was removed before it became ready.');
224
+ error.name = 'AbortError';
225
+ reject(error);
226
+ }
227
+ function onReady() {
228
+ cleanup();
229
+ resolve(host);
230
+ }
231
+ function onError(event) {
232
+ cleanup();
233
+ host.remove();
234
+ reject(event.detail?.error ?? new Error('The PWA install component could not load.'));
235
+ }
236
+ host.addEventListener('html-import-ready', onReady);
237
+ host.addEventListener('html-import-error', onError);
238
+ document.body.append(host);
239
+ observer.observe(document.documentElement, {childList: true, subtree: true});
240
+ });
241
+ }
242
+ }
@@ -1,5 +1,7 @@
1
1
  import {createArcaneEventSource} from './event-manager.mjs';
2
2
 
3
+ export {PWA_INSTALL_STATE_EVENT, getPwaInstall, mountPwaInstallPrompt} from './pwa-install.mjs';
4
+
3
5
  export const PWA_STATE_EVENT = 'arcane.pwa.state';
4
6
 
5
7
  export function registerPwa({workerUrl = './arcane-sw.js', scope} = {}) {
@@ -116,6 +116,15 @@ development-refresh lock, then releases the lock before binding the listener.
116
116
  The authored descriptor remains unchanged, and package-only apps retain their
117
117
  existing path. An enabled PWA receives generated manifests directly from this
118
118
  source server without creating `dist` output.
119
+ While serving, the SDK rereads changed metadata for the selected app before
120
+ application, root-navigation, and generated-PWA requests. It projects the current
121
+ authored descriptor in memory, or reads the current package configuration for a
122
+ package-only app, without writing either file. Include/exclude rules, entry
123
+ selection, and PWA settings share one request snapshot. Overlapping reads reuse
124
+ one metadata task; unchanged files reuse their parsed selection. Shared runtime
125
+ requests do not wait for that refresh. Full offline inventories are enumerated
126
+ only for worker/offline-manifest requests, with separate state for each selected
127
+ app snapshot so an older inventory cannot overwrite newer generated metadata.
119
128
  Changed resource requests return the complete current saved source without
120
129
  packaging, copying files into `dist`, or restarting the server. Conditional
121
130
  requests for unchanged resources return `304`. Enabled PWAs check on page load
@@ -6,7 +6,7 @@
6
6
  "minimumVersion": "22.23.2 for Node entrypoints",
7
7
  "moduleSystem": "ESM"
8
8
  },
9
- "memberCount": 205,
9
+ "memberCount": 208,
10
10
  "members": [
11
11
  {
12
12
  "id": "root:APP_BUNDLE_DESCRIPTOR_NAME",
@@ -2996,6 +2996,48 @@
2996
2996
  "protocol": "Native registration, updatefound, statechange and controllerchange events",
2997
2997
  "normalization": "Synchronous owner exposes ready, state, subscribe, update and dispose; current state replays by default, failures remain observable, disposal does not unregister the worker or delete saved data."
2998
2998
  },
2999
+ {
3000
+ "id": "pwa:PWA_INSTALL_STATE_EVENT",
3001
+ "name": "PWA_INSTALL_STATE_EVENT",
3002
+ "displayName": "PWA_INSTALL_STATE_EVENT",
3003
+ "kind": "constant",
3004
+ "signature": "const PWA_INSTALL_STATE_EVENT",
3005
+ "entrypoints": ["arcane-os/pwa"],
3006
+ "primaryImport": "arcane-os/pwa",
3007
+ "group": "Progressive web applications",
3008
+ "summary": "Names the shared native installation availability and choice state event.",
3009
+ "availability": "Browser installation lifecycle; importable without starting observation",
3010
+ "protocol": "Existing Arcane event owner and native browser installation events",
3011
+ "normalization": "The exact event name is arcane.pwa.install.state."
3012
+ },
3013
+ {
3014
+ "id": "pwa:getPwaInstall",
3015
+ "name": "getPwaInstall",
3016
+ "displayName": "getPwaInstall()",
3017
+ "kind": "function",
3018
+ "signature": "getPwaInstall()",
3019
+ "entrypoints": ["arcane-os/pwa"],
3020
+ "primaryImport": "arcane-os/pwa",
3021
+ "group": "Progressive web applications",
3022
+ "summary": "Returns one shared page owner for native installation availability, prompt choices and session dismissal.",
3023
+ "availability": "Browser native installation events; waiting state while no prompt is available",
3024
+ "protocol": "Native beforeinstallprompt, appinstalled and display-mode change events",
3025
+ "normalization": "Synchronous owner exposes state, subscribe, prompt, dismiss and dispose; subscriptions replay current state, prompt invokes the native event directly in the user click and consumes it once, and session dismissal preserves a retained event for explicit inline installation."
3026
+ },
3027
+ {
3028
+ "id": "pwa:mountPwaInstallPrompt",
3029
+ "name": "mountPwaInstallPrompt",
3030
+ "displayName": "mountPwaInstallPrompt()",
3031
+ "kind": "function",
3032
+ "signature": "mountPwaInstallPrompt({appName = ''} = {})",
3033
+ "entrypoints": ["arcane-os/pwa"],
3034
+ "primaryImport": "arcane-os/pwa",
3035
+ "group": "Progressive web applications",
3036
+ "summary": "Starts shared installation observation and mounts one initially hidden, themed installation suggestion without delaying application rendering.",
3037
+ "availability": "Browser document and managed HTML import; resolves to null without a document",
3038
+ "protocol": "Shared native install owner and pwa-install.html component lifecycle",
3039
+ "normalization": "Repeated calls return the same mounting promise; the first appName initializes the component, success resolves to its ready html-import host, absence of a document or disposal before mounting resolves to null, and component loading failures reject. Removal or disposal during loading rejects with AbortError. A rejected mount permits another explicit attempt. Generated PWA bootstraps invoke it automatically."
3040
+ },
2999
3041
  {
3000
3042
  "id": "browser-speech:BROWSER_SPEECH_ARTIFACT_GRAPH_PROTOCOL",
3001
3043
  "name": "BROWSER_SPEECH_ARTIFACT_GRAPH_PROTOCOL",
@@ -7,7 +7,7 @@
7
7
  "path": "runtime/arcane/components",
8
8
  "sdkVersion": "0.7.2"
9
9
  },
10
- "componentCount": 40,
10
+ "componentCount": 41,
11
11
  "loader": "/arcane/modules/HTMLImport.js",
12
12
  "artifacts": [
13
13
  {
@@ -645,6 +645,34 @@
645
645
  "transport": "HTMLImport + DOM; injected Arcane/provider modules where listed",
646
646
  "normalization": "Normalized form values"
647
647
  },
648
+ {
649
+ "file": "runtime/arcane/components/pwa-install.html",
650
+ "name": "pwa-install.html",
651
+ "purpose": "Presents a dismissible browser installation action with floating or inline placement.",
652
+ "methods": [
653
+ "configure()",
654
+ "install()",
655
+ "dismiss()",
656
+ "destroy()",
657
+ "state",
658
+ "ready"
659
+ ],
660
+ "events": [
661
+ "pwa-install-ready",
662
+ "pwa-install-change",
663
+ "pwa-install-dismissed"
664
+ ],
665
+ "slots": [],
666
+ "dependencies": [
667
+ "strong-type",
668
+ "arcane-os/pwa",
669
+ "arcane-os/event-manager",
670
+ "arcane-os/logging"
671
+ ],
672
+ "availability": "Browser installation requires a browser-provided beforeinstallprompt event; otherwise hidden",
673
+ "transport": "HTMLImport + DOM; shared PWA installation owner and browser-native prompt",
674
+ "normalization": "Browser install availability and outcome supplied by the shared PWA owner"
675
+ },
648
676
  {
649
677
  "file": "runtime/arcane/components/record-timeline.html",
650
678
  "name": "record-timeline.html",
@@ -2,7 +2,9 @@
2
2
 
3
3
  An application supplies its installation identity and offline resource selection.
4
4
  The SDK generates the Web App Manifest, offline inventory, service worker and
5
- nonblocking registration module. Native packages keep their existing lifecycle.
5
+ nonblocking registration module. Its shared installation owner and dismissible
6
+ component expose the browser's available install action. Native packages keep
7
+ their existing lifecycle.
6
8
 
7
9
  ## Application configuration
8
10
 
@@ -63,7 +65,7 @@ Browser packaging emits these files at the selected deployment root:
63
65
  | `arcane.webmanifest` | Browser installation metadata. |
64
66
  | `arcane-offline.json` | App ID/version, SDK version, deployment revision, resource URLs and explicit navigation aliases. |
65
67
  | `arcane-sw.js` | Stable worker URL with the selected offline manifest embedded in its source. |
66
- | `arcane-pwa.mjs` | Independent registration module importing the SDK client. |
68
+ | `arcane-pwa.mjs` | Independent registration and installation-component bootstrap importing the SDK client. |
67
69
 
68
70
  Each packaged output gets one deployment revision shared by its offline
69
71
  manifest and worker. It distinguishes separately generated outputs even when
@@ -74,6 +76,9 @@ It follows actual resource references to include meaningful query variants.
74
76
  Generated application pages receive a manifest link and an `async` module
75
77
  marked `data-arcane-pwa`. Existing application scripts retain their order.
76
78
  PWA registration does not wait for models, storage, preferences or page rendering.
79
+ The same bootstrap starts one initially hidden `pwa-install.html` component with
80
+ the generated manifest's app name. Component loading and worker registration
81
+ proceed independently.
77
82
 
78
83
  The selected PWA browser delivery removes `v` and `arcaneVersion` from actual
79
84
  local resource references, including the managed import map. Other query fields,
@@ -91,7 +96,10 @@ Package-only applications retain their existing descriptor workflow.
91
96
 
92
97
  Add a file or directory to `package.include` to make it part of the app's
93
98
  resources. A new file inside an already included directory needs no separate
94
- entry. If `package.pwa.offline.include` is nonempty, the resource must also
99
+ entry. `package.include` is an application resource selection, not a file list
100
+ inside the Web App Manifest. `arcane.webmanifest` contains browser installation
101
+ metadata; `arcane-offline.json` contains the selected offline resource inventory.
102
+ If `package.pwa.offline.include` is nonempty, the resource must also
95
103
  match that offline selection and must not match `offline.exclude`. Adding a
96
104
  path only to the offline selection does not add it to the app's resources.
97
105
  Restart after changing descriptor settings. Edits to selected source files are
@@ -104,6 +112,8 @@ and packaged-preview serving, including conditional resource responses.
104
112
  Every Arcane development server and packaged browser preview serves HTTPS,
105
113
  including localhost. Configure the workspace certificate pair before starting
106
114
  the ordinary command; see [development HTTPS setup](cli.md#development-https-setup).
115
+ `--public` selects the IPv4 wildcard bind address; it does not enable PWA
116
+ configuration, change manifest metadata, or determine browser installability.
107
117
 
108
118
  Source inventory work begins when the browser requests the worker or current
109
119
  offline manifest, after the page can start. It traverses the selected route
@@ -153,7 +163,15 @@ the previous timestamp so the next page load can retry. Each cached response
153
163
  retains its own `Last-Modified` header, but there are no per-file check times.
154
164
  The SDK imposes no age-based cache deletion and
155
165
  retains resource bodies across app and SDK version changes. Requests for a page
156
- do not wait for the complete resource inventory to finish checking. The SDK
166
+ do not wait for the complete resource inventory to finish checking. A file
167
+ already in the current resource cache also returns immediately when its own
168
+ conditional check is pending or in flight. That background check keeps its
169
+ existing owner and updates the stored response for subsequent requests.
170
+ The page receives the cached response's original status, commonly `200`, even
171
+ when the separate conditional network response is `304`. Status alone does
172
+ not identify a network transfer; use the browser's response source and timing
173
+ details to distinguish cache access from worker startup, queueing and network.
174
+ The SDK
157
175
  uses at most four concurrent background resource requests and starts no timer
158
176
  or polling loop between page loads.
159
177
 
@@ -205,6 +223,151 @@ Switching a server from a packaged release to live development does not replace
205
223
  an already active release worker inside an open document. The same native
206
224
  worker lifecycle applies.
207
225
 
226
+ ## Installation component
227
+
228
+ Starting with SDK `0.13.0`, enabled PWA pages automatically mount the shared
229
+ [`pwa-install.html` component](runtime-components.md#pwa-installhtml). It appears
230
+ when the browser supplies an installation prompt, offers **Install** and a
231
+ clearly labeled close control, and does not move focus when it appears.
232
+ The floating suggestion has no automatic dismissal timer. Closing it remembers
233
+ the choice for the current tab session and manifest URL, so another page
234
+ load does not immediately show it again. A storage failure leaves the current
235
+ page's dismissal functional and reports the error through console diagnostics.
236
+
237
+ An application can also place the same component inline through `html-import`
238
+ with `data-presentation="inline"`. Both presentations share one page-owned
239
+ native installation event. Dismissing the floating suggestion does not consume
240
+ that event or disable an explicitly placed inline component. Closing an inline
241
+ instance hides only that instance. See the component reference for its
242
+ configuration, methods and events.
243
+
244
+ The browser controls the URL-bar installation indicator and native prompt.
245
+ The SDK cannot force either to appear. Without a captured
246
+ `beforeinstallprompt`, the component remains hidden; that waiting state does
247
+ not establish that installation is unsupported. The browser may still be
248
+ evaluating the app, may already have it installed, or may only support a
249
+ manual browser-menu installation path.
250
+
251
+ ### Browser installation requirements
252
+
253
+ Inspect the loaded page's manifest link and the browser's manifest diagnostics
254
+ when an install action is missing. Confirm that the generated manifest has the
255
+ intended name, `start_url`, scope and app display mode, and that its icon URLs
256
+ resolve to actual images with the declared dimensions. For Chromium's manifest
257
+ install promotion, provide a `purpose: "any"` icon, or omit `purpose` to use
258
+ that default, in PNG, SVG or WebP format. Its strict installation icon selector
259
+ excludes JPEG even when the same image renders successfully on the page.
260
+ Do not change a file's extension or MIME declaration without converting the
261
+ actual image at the application's asset owner. See Chromium's
262
+ [icon selection implementation](https://raw.githubusercontent.com/chromium/chromium/main/third_party/blink/common/manifest/manifest_icon_selector.cc).
263
+
264
+ Providing 192-by-192 and 512-by-512 raster icons follows the
265
+ [browser guidance](https://web.dev/articles/add-manifest). Their absence alone
266
+ does not prove the failure: Chromium can select one larger supported icon.
267
+ Keep actual icon dimensions in `sizes`. Browser diagnostics about missing
268
+ `screenshots` concern the richer installation dialog; screenshots are optional
269
+ and are separate from a usable installation icon.
270
+
271
+ Browser installation requires HTTPS or the browser's localhost/loopback
272
+ exception. A device-facing LAN address is not loopback. Arcane's development
273
+ server still follows its own HTTPS serving contract above. Browser engagement,
274
+ installation state and platform support also affect whether native promotion
275
+ appears; worker cache readiness is not an installation UI prerequisite. See
276
+ [browser installation requirements](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable).
277
+
278
+ Browsers without `beforeinstallprompt` can offer manual installation. For
279
+ example, current iPhone Safari uses Share, **Add to Home Screen**, **Open as
280
+ Web App**, then **Add**. A product may explain that browser-owned path in its
281
+ help, but should not present it as a programmatic SDK install action. The SDK
282
+ does not infer installation support from the user-agent string. See
283
+ [Apple's installation instructions](https://support.apple.com/guide/iphone/open-as-web-app-iphea86e5236/ios).
284
+
285
+ ## getPwaInstall()
286
+
287
+ Import `getPwaInstall` and `PWA_INSTALL_STATE_EVENT` from `arcane-os/pwa`.
288
+ `getPwaInstall()` synchronously returns the shared page owner with `state`,
289
+ `subscribe`, `prompt`, `dismiss` and `dispose`. Call it early when owning a
290
+ separate install entry point so it can capture `beforeinstallprompt` before
291
+ loading the UI. The generated bootstrap already does this through
292
+ `mountPwaInstallPrompt()`.
293
+
294
+ `state` contains `status`, `available`, `dismissed`, `outcome` and `error`.
295
+ Status is `waiting`, `available`, `prompting`, `accepted`, `dismissed`,
296
+ `installed`, `running`, `error` or `disposed`. `available` means a native event
297
+ is retained; a dismissed floating suggestion can still have `available: true`.
298
+ `outcome` is the browser's `accepted` or `dismissed` choice, or `null` before a
299
+ choice. `error` carries the complete prompt error, or `null`.
300
+
301
+ `subscribe(listener, {emitCurrent: true, signal} = {})` immediately replays
302
+ state by default and returns an unsubscribe function. Later state travels
303
+ through the existing Arcane event owner using `PWA_INSTALL_STATE_EVENT`
304
+ (`arcane.pwa.install.state`). A subscription does not wait for worker
305
+ registration, storage initialization or model readiness.
306
+
307
+ Call `prompt()` directly from the user's install click, before any asynchronous
308
+ wait. It invokes the browser prompt in the same call stack, consumes the event
309
+ once, and returns a promise for the browser's choice. It resolves to `null`
310
+ when there is no retained event or the owner is disposed. Failure publishes
311
+ `error` state and rejects. A new native event is required for another prompt.
312
+
313
+ ```javascript
314
+ import {getPwaInstall} from 'arcane-os/pwa';
315
+
316
+ const install = getPwaInstall();
317
+ const installButton = document.querySelector('#install');
318
+
319
+ install.subscribe(function showInstallAvailability(state) {
320
+ installButton.hidden = !state.available;
321
+ });
322
+ installButton.addEventListener('click', function requestInstallation() {
323
+ install.prompt().catch(function reportInstallFailure(error) {
324
+ console.error(error);
325
+ });
326
+ });
327
+ ```
328
+
329
+ `dismiss()` remembers the session choice without consuming the retained event
330
+ and returns the current state. A browser-native dismissed choice is remembered
331
+ too. `appinstalled` clears the event and publishes `installed`; running in an
332
+ app display mode publishes `running` and suppresses the prompt. These states
333
+ do not establish offline readiness. On Android, `appinstalled` can arrive
334
+ before WebAPK creation finishes. See the
335
+ [browser lifecycle distinction](https://web.dev/learn/pwa/detection/).
336
+
337
+ `dispose()` removes the shared owner's native listeners and subscriptions.
338
+ Leaving the page disposes it automatically, except when the browser retains
339
+ the page in its back/forward cache.
340
+ Because the owner is shared, an individual component should dispose its own
341
+ subscription instead. A later `getPwaInstall()` creates a new owner after
342
+ disposal; it cannot recover a native event that was already consumed.
343
+
344
+ ## mountPwaInstallPrompt()
345
+
346
+ `mountPwaInstallPrompt({appName = ''} = {})` starts native install observation
347
+ synchronously, then loads the shared HTML import and theme modules concurrently
348
+ and appends one initially hidden component when the document body is available.
349
+ It returns the same mounting promise on repeated calls; the first call supplies
350
+ the initial app name. The promise resolves to the ready `html-import` host, or
351
+ `null` without a document or when the owner is disposed before mounting. It
352
+ rejects if component loading fails, or with `AbortError` when the loading host
353
+ is removed or its owner disposed. A rejected mount releases its slot so an
354
+ explicit later call can try again. Observe the rejection without making page
355
+ rendering wait for it.
356
+
357
+ The generated PWA bootstrap calls this automatically using the manifest name.
358
+ Applications need not add another floating suggestion. A separate entry point
359
+ can call it explicitly:
360
+
361
+ ```javascript
362
+ import {mountPwaInstallPrompt} from 'arcane-os/pwa';
363
+
364
+ mountPwaInstallPrompt({appName: 'Example Library'}).catch(
365
+ function reportInstallComponentFailure(error) {
366
+ console.error(error);
367
+ }
368
+ );
369
+ ```
370
+
208
371
  ## registerPwa()
209
372
 
210
373
  Import `registerPwa` and `PWA_STATE_EVENT` from `arcane-os/pwa` through the