@ashley-shrok/viewmodel-shell 0.13.0 → 0.14.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/dist/browser.d.ts CHANGED
@@ -10,6 +10,12 @@ export declare class BrowserAdapter implements Adapter {
10
10
  * contentType is informational — the Blob's own .type takes precedence in
11
11
  * browsers. We accept the arg for adapter symmetry (other adapters use it). */
12
12
  saveFile(data: Blob, filename: string, _contentType: string): void;
13
+ /** 0.14.0 — install / clear the browser's `beforeunload` guard. The shell
14
+ * calls this on every response with `response.preventUnload ?? false`, so
15
+ * the lock-and-clear cycle is just "server sets preventUnload:true while
16
+ * work is pending, omits it (or sets false) when done." Idempotent. */
17
+ private unloadHandler;
18
+ setPreventUnload(active: boolean): void;
13
19
  transport(input: string, init: {
14
20
  method?: string;
15
21
  headers?: Record<string, string>;
package/dist/browser.js CHANGED
@@ -105,6 +105,28 @@ export class BrowserAdapter {
105
105
  setTimeout(() => URL.revokeObjectURL(url), 0);
106
106
  }
107
107
  }
108
+ /** 0.14.0 — install / clear the browser's `beforeunload` guard. The shell
109
+ * calls this on every response with `response.preventUnload ?? false`, so
110
+ * the lock-and-clear cycle is just "server sets preventUnload:true while
111
+ * work is pending, omits it (or sets false) when done." Idempotent. */
112
+ unloadHandler = null;
113
+ setPreventUnload(active) {
114
+ if (active && this.unloadHandler == null) {
115
+ this.unloadHandler = (e) => {
116
+ // Two signals because browsers historically disagreed on which they
117
+ // honor; modern Chromium/Firefox honor preventDefault, older Safari
118
+ // and ancient browsers look at returnValue. The dialog text itself is
119
+ // browser-controlled ("Leave site?…") and cannot be customized.
120
+ e.preventDefault();
121
+ e.returnValue = "";
122
+ };
123
+ window.addEventListener("beforeunload", this.unloadHandler);
124
+ }
125
+ else if (!active && this.unloadHandler != null) {
126
+ window.removeEventListener("beforeunload", this.unloadHandler);
127
+ this.unloadHandler = null;
128
+ }
129
+ }
108
130
  async transport(input, init, hooks) {
109
131
  const onUploadProgress = hooks?.onUploadProgress;
110
132
  if (!onUploadProgress) {
package/dist/index.d.ts CHANGED
@@ -32,6 +32,16 @@ export interface Adapter {
32
32
  * May return void or Promise<void>; the shell awaits the return value so
33
33
  * async I/O errors surface via onError. */
34
34
  saveFile?(data: Blob, filename: string, contentType: string): void | Promise<void>;
35
+ /** 0.14.0 — install / clear a "warn before navigating away" guard, driven
36
+ * by `ShellResponse.preventUnload` on every response (load, dispatch,
37
+ * push). Idempotent: the shell calls with the boolean from each response,
38
+ * the adapter installs when true / clears when false. Designed for
39
+ * long-running server actions where an accidental tab close would lose
40
+ * in-flight work. Fail-quiet by absence (unlike navigate/storage/saveFile)
41
+ * — this is a UX safety net, not a security guarantee, and non-browser
42
+ * targets (TUI) have no terminal equivalent. Modern browsers show a
43
+ * generic "Leave site?" dialog; the message is not customizable. */
44
+ setPreventUnload?(active: boolean): void;
35
45
  }
36
46
  export type ViewNode = PageNode | SectionNode | ListNode | ListItemNode | FormNode | FieldNode | CheckboxNode | ButtonNode | TextNode | LinkNode | ImageNode | StatBarNode | TabsNode | ProgressNode | ModalNode | TableNode | CopyButtonNode;
37
47
  export interface PageNode {
@@ -320,6 +330,12 @@ export interface ShellResponse {
320
330
  sideEffects?: ShellSideEffect[];
321
331
  /** When set, schedules the next poll at this delay (ms). Overrides pollInterval for one tick. */
322
332
  nextPollIn?: number;
333
+ /** 0.14.0 — when true, the shell asks the adapter to install a "warn before
334
+ * unload" guard; when false / absent, the guard is cleared. Drives long-
335
+ * running-work workflows: while server-side work is in flight, return
336
+ * `preventUnload: true` from each response; clear it when the work
337
+ * completes (typically via polling). See `Adapter.setPreventUnload`. */
338
+ preventUnload?: boolean;
323
339
  }
324
340
  export declare class ViewModelShell {
325
341
  private options;
package/dist/index.js CHANGED
@@ -21,6 +21,10 @@ export class ViewModelShell {
21
21
  const body = (await res.json());
22
22
  this.currentVm = body.vm;
23
23
  this.currentState = body.state;
24
+ // 0.14.0 — apply the unload guard from the initial-load response too. The
25
+ // server may legitimately want it on at first paint (e.g. the page was
26
+ // refreshed mid-work and the long action is still pending server-side).
27
+ adapter.setPreventUnload?.(body.preventUnload ?? false);
24
28
  adapter.render(body.vm, (action) => this.dispatch(action));
25
29
  this.schedulePoll(body.nextPollIn);
26
30
  }
@@ -138,6 +142,11 @@ export class ViewModelShell {
138
142
  void this.download(effect.url, effect.filename);
139
143
  }
140
144
  }
145
+ // 0.14.0 — apply the unload guard before the redirect/render branch so it's
146
+ // in place (or cleared) consistently across both branches. A server that
147
+ // wants a redirect to NOT be blocked by its own guard simply omits
148
+ // preventUnload (or sets it false) on that response — standard pattern.
149
+ adapter.setPreventUnload?.(body.preventUnload ?? false);
141
150
  if (body.redirect) {
142
151
  if (this.options.onRedirect) {
143
152
  this.options.onRedirect(body.redirect);
package/dist/server.d.ts CHANGED
@@ -18,6 +18,10 @@ export interface ShellResponseBody<TState> {
18
18
  redirect?: string;
19
19
  sideEffects?: ShellSideEffect[];
20
20
  nextPollIn?: number;
21
+ /** 0.14.0 — install / clear the browser's "warn before unload" guard. Omit
22
+ * (or set false) to clear; set true while a long-running server action is
23
+ * in flight so an accidental tab-close doesn't lose work. */
24
+ preventUnload?: boolean;
21
25
  }
22
26
  /** Build a redirect response (Vm and State omitted; shell navigates the browser). */
23
27
  export declare function shellRedirect<TState = unknown>(url: string): ShellResponseBody<TState>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashley-shrok/viewmodel-shell",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "A server-driven UI framework where the wire format is structured enough that agents can build full-stack apps without ever opening a browser and all UI tests are pure unit tests with no browser runtime. Server returns a JSON tree of typed nodes; a thin TypeScript adapter renders it to DOM. Backend-agnostic — a .NET reference backend ships with the repo, but any language can produce the JSON contract.",
5
5
  "type": "module",
6
6
  "license": "MIT",