@signal9/era-ui 3.15.0 → 3.16.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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from './ui/index.js';
2
2
  export * as OS from './os/index.js';
3
3
  export * as AI from './ai/index.js';
4
- export type { Point, Size, Rect, Insets, Frame, Workspace, WindowState, SnapZone, AppWindow, AppDefinition, AppComponent, AppProps } from './os/index.js';
4
+ export type { Point, Size, Rect, Insets, Frame, Workspace, WindowState, JsonValue, WindowSnapshot, WorkspaceSnapshot, SnapZone, AppWindow, AppDefinition, AppComponent, AppProps } from './os/index.js';
5
5
  export { cn, tv, hotkeys, keys, parseKeybinding, matchKeyBindingPress, createKeybindingsHandler, formatKeybinding, isApplePlatform, modKey, type KeyBindingMap, type KeyBindingPress, type KeyBindingOptions, type PartProps, type VariantProps } from './utils/index.js';
@@ -1,4 +1,4 @@
1
- export { WindowManager, getWindowManager, setWindowManagerContext, type Point, type Size, type Rect, type Insets, type Frame, type AppDefinition, type AppWindow, type AppComponent, type AppProps, type WindowState, type SnapZone, type Workspace } from './wm.svelte.js';
1
+ export { WindowManager, getWindowManager, setWindowManagerContext, type Point, type Size, type Rect, type Insets, type Frame, type AppDefinition, type AppWindow, type AppComponent, type AppProps, type WindowState, type JsonValue, type WindowSnapshot, type WorkspaceSnapshot, SNAPSHOT_VERSION, readSnapshot, type SnapZone, type Workspace } from './wm.svelte.js';
2
2
  export { NotificationService, getNotifications, setNotificationContext, type AppNotification, type NotifyOptions, type NotificationAction, type NotificationTone } from './notifications.svelte.js';
3
3
  export { LAYER } from './layers.js';
4
4
  export { default as Desktop } from './desktop.svelte';
package/dist/os/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // Two stores (window manager + notifications) shared via context, and the
3
3
  // components that render them: Desktop host, Window, Taskbar (with its
4
4
  // built-in command bar), Toaster, NotificationCenter.
5
- export { WindowManager, getWindowManager, setWindowManagerContext } from './wm.svelte.js';
5
+ export { WindowManager, getWindowManager, setWindowManagerContext, SNAPSHOT_VERSION, readSnapshot } from './wm.svelte.js';
6
6
  export { NotificationService, getNotifications, setNotificationContext } from './notifications.svelte.js';
7
7
  export { LAYER } from './layers.js';
8
8
  export { default as Desktop } from './desktop.svelte';
@@ -27,6 +27,87 @@ export interface Frame {
27
27
  pos: Point;
28
28
  size: Size | null;
29
29
  }
30
+ /**
31
+ * Anything that survives `JSON.parse(JSON.stringify(x))` unchanged.
32
+ *
33
+ * This is the whole serialization contract, enforced by the compiler instead of
34
+ * by a runtime schema. A `Date`, a `Map`, a function, `undefined` inside an
35
+ * array — none of them type-check here, so a snapshot that compiles is a
36
+ * snapshot that round-trips. It costs nothing at runtime and adds no dependency,
37
+ * which matters for a UI library: a schema package would land in every
38
+ * consumer's bundle to validate data era itself produced.
39
+ *
40
+ * The untrusted direction still needs real checking — see `readSnapshot`, which
41
+ * is where JSON from a file or localStorage is actually interrogated.
42
+ */
43
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
44
+ [k: string]: JsonValue;
45
+ };
46
+ /**
47
+ * Bumped whenever a snapshot's shape changes in a way older data cannot satisfy.
48
+ * `readSnapshot` refuses anything it does not recognise rather than guessing —
49
+ * a half-understood layout is worse than a fresh one.
50
+ */
51
+ export declare const SNAPSHOT_VERSION = 1;
52
+ /**
53
+ * One window, reduced to what a reload cannot reconstruct.
54
+ *
55
+ * Deliberately NOT a partial AppWindow. A live window holds a `component` and an
56
+ * `icon`, which are functions; those come from the app registry and are looked
57
+ * up again on restore by `appId`. Storing an id instead of a reference is also
58
+ * what lets a snapshot outlive a refactor of the app itself.
59
+ */
60
+ export interface WindowSnapshot {
61
+ /** The registered app this window rehydrates from. */
62
+ appId: string;
63
+ /** The workspace it belongs to. */
64
+ workspaceId: string;
65
+ /** Placement when `state` is 'normal'. */
66
+ frame: Frame;
67
+ state: WindowState;
68
+ /** Where to land on un-maximize / un-snap; null if it was never moved. */
69
+ restore: Frame | null;
70
+ /** Stacking order within its workspace. */
71
+ z: number;
72
+ /** Only when the window was retitled away from its app's default. */
73
+ title?: string;
74
+ /**
75
+ * App-specific state — the note being edited, a scroll offset, a filter.
76
+ *
77
+ * The escape hatch every windowing system needs: era cannot know what an app
78
+ * considers worth keeping, so it carries the value verbatim and hands it back
79
+ * on restore. Typed JsonValue rather than `unknown`, so an app cannot stash
80
+ * something that silently fails to survive the round trip.
81
+ */
82
+ props?: JsonValue;
83
+ }
84
+ /** A whole desktop, exportable as one JSON document. */
85
+ export interface WorkspaceSnapshot {
86
+ version: number;
87
+ /**
88
+ * Epoch milliseconds. A number, not an ISO string: timezone-free, sortable,
89
+ * and it cannot drift on a round trip through a formatter. Never used for
90
+ * logic — it exists so a human can tell two exports apart.
91
+ */
92
+ savedAt: number;
93
+ activeWorkspaceId: string;
94
+ workspaces: Workspace[];
95
+ windows: WindowSnapshot[];
96
+ }
97
+ /**
98
+ * Parse untrusted JSON into a snapshot, or return null.
99
+ *
100
+ * The counterpart to JsonValue. The compiler guarantees what era WRITES; this
101
+ * guards what it reads, which may be a hand-edited file, a stale localStorage
102
+ * entry, or a snapshot from a future version. It checks shape rather than
103
+ * trusting `as`, and refuses an unknown `version` outright — silently importing
104
+ * a layout you only half understand is worse than declining it.
105
+ *
106
+ * Individual windows are filtered, not fatal: one malformed entry should not
107
+ * cost the user the rest of the layout. applySnapshot() drops unknown appIds the
108
+ * same way, for the same reason.
109
+ */
110
+ export declare function readSnapshot(json: string): WorkspaceSnapshot | null;
30
111
  export type WindowState = 'normal' | 'minimized' | 'maximized';
31
112
  /** Snap targets: the four edges (halves) or four corners (quadrants). */
32
113
  export type SnapZone = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
@@ -57,6 +138,12 @@ export interface AppDefinition {
57
138
  /** Extra classes for the window body (Pane.Content) — e.g. `p-(--era-gap)`
58
139
  * for a body of controls, or `p-0` for a full-bleed app. */
59
140
  bodyClass?: string;
141
+ /**
142
+ * Starting app state for every window spawned from this app — the value a
143
+ * window carries in its snapshot's `props`. An app reads it back off
144
+ * `window.props` (AppProps hands the whole record over).
145
+ */
146
+ props?: JsonValue;
60
147
  /** Extra title-bar controls, rendered immediately LEFT of the close button.
61
148
  * For per-window affordances that belong on the chrome rather than in the
62
149
  * body — a help/usage launcher, a pin, a view toggle. Assignable on the
@@ -86,6 +173,8 @@ export interface AppWindow {
86
173
  minimizable: boolean;
87
174
  bodyClass?: string;
88
175
  actions?: Snippet;
176
+ /** App-specific state, carried verbatim through a snapshot. */
177
+ props?: JsonValue;
89
178
  }
90
179
  /** A named virtual desktop. Every window belongs to exactly one; only the
91
180
  * active workspace's windows are shown, the rest keep their geometry and
@@ -158,6 +247,26 @@ export declare class WindowManager {
158
247
  * The Desktop calls this whenever the work area changes; chrome itself
159
248
  * (the taskbar) never resizes with the viewport. */
160
249
  relayout(): void;
250
+ /**
251
+ * The whole desktop as one JSON-safe document.
252
+ *
253
+ * Reads every workspace and every window across ALL of them, not just the
254
+ * active one — a snapshot that only captured what was on screen would quietly
255
+ * drop the other workspaces the moment it was restored.
256
+ */
257
+ snapshot(): WorkspaceSnapshot;
258
+ /**
259
+ * Replace the desktop with a snapshot.
260
+ *
261
+ * Named applySnapshot, not restore: `restore(id)` already means "un-maximize
262
+ * this window", and one verb cannot mean both.
263
+ *
264
+ * Windows whose `appId` is no longer registered are SKIPPED rather than
265
+ * failing the whole restore: an app can be removed between export and import,
266
+ * and losing one window beats losing the layout. Same for a window pointing at
267
+ * a workspace the snapshot does not contain.
268
+ */
269
+ applySnapshot(snapshot: WorkspaceSnapshot): void;
161
270
  /** Half / quadrant tiling against the work area. */
162
271
  snap(id: string, zone: SnapZone): void;
163
272
  /** Create a workspace (optionally switching to it) and return its id. */
@@ -1,4 +1,80 @@
1
1
  import { getContext, setContext } from 'svelte';
2
+ /**
3
+ * Bumped whenever a snapshot's shape changes in a way older data cannot satisfy.
4
+ * `readSnapshot` refuses anything it does not recognise rather than guessing —
5
+ * a half-understood layout is worse than a fresh one.
6
+ */
7
+ export const SNAPSHOT_VERSION = 1;
8
+ /**
9
+ * Parse untrusted JSON into a snapshot, or return null.
10
+ *
11
+ * The counterpart to JsonValue. The compiler guarantees what era WRITES; this
12
+ * guards what it reads, which may be a hand-edited file, a stale localStorage
13
+ * entry, or a snapshot from a future version. It checks shape rather than
14
+ * trusting `as`, and refuses an unknown `version` outright — silently importing
15
+ * a layout you only half understand is worse than declining it.
16
+ *
17
+ * Individual windows are filtered, not fatal: one malformed entry should not
18
+ * cost the user the rest of the layout. applySnapshot() drops unknown appIds the
19
+ * same way, for the same reason.
20
+ */
21
+ export function readSnapshot(json) {
22
+ let raw;
23
+ try {
24
+ raw = JSON.parse(json);
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ if (typeof raw !== 'object' || raw === null)
30
+ return null;
31
+ const o = raw;
32
+ if (o.version !== SNAPSHOT_VERSION)
33
+ return null;
34
+ if (typeof o.activeWorkspaceId !== 'string')
35
+ return null;
36
+ if (!Array.isArray(o.workspaces) || !Array.isArray(o.windows))
37
+ return null;
38
+ const isPoint = (v) => typeof v === 'object' &&
39
+ v !== null &&
40
+ typeof v.x === 'number' &&
41
+ typeof v.y === 'number';
42
+ const isSize = (v) => typeof v === 'object' &&
43
+ v !== null &&
44
+ typeof v.width === 'number' &&
45
+ typeof v.height === 'number';
46
+ const isFrame = (v) => {
47
+ if (typeof v !== 'object' || v === null)
48
+ return false;
49
+ const f = v;
50
+ return isPoint(f.pos) && (f.size === null || isSize(f.size));
51
+ };
52
+ const workspaces = o.workspaces.filter((w) => typeof w === 'object' &&
53
+ w !== null &&
54
+ typeof w.id === 'string' &&
55
+ typeof w.name === 'string');
56
+ if (workspaces.length === 0)
57
+ return null;
58
+ const states = ['normal', 'minimized', 'maximized'];
59
+ const windows = o.windows.filter((w) => {
60
+ if (typeof w !== 'object' || w === null)
61
+ return false;
62
+ const x = w;
63
+ return (typeof x.appId === 'string' &&
64
+ typeof x.workspaceId === 'string' &&
65
+ typeof x.z === 'number' &&
66
+ states.includes(x.state) &&
67
+ isFrame(x.frame) &&
68
+ (x.restore === null || isFrame(x.restore)));
69
+ });
70
+ return {
71
+ version: SNAPSHOT_VERSION,
72
+ savedAt: typeof o.savedAt === 'number' ? o.savedAt : 0,
73
+ activeWorkspaceId: o.activeWorkspaceId,
74
+ workspaces,
75
+ windows
76
+ };
77
+ }
2
78
  const DEFAULT_SIZE = { width: 480, height: 320 };
3
79
  /**
4
80
  * The source of truth for the desktop: which workspaces and windows exist,
@@ -106,7 +182,8 @@ export class WindowManager {
106
182
  resizable: overrides?.resizable ?? app.resizable ?? true,
107
183
  minimizable: app.minimizable ?? true,
108
184
  bodyClass: app.bodyClass,
109
- actions: app.actions
185
+ actions: app.actions,
186
+ props: app.props
110
187
  };
111
188
  this.windows.push(win);
112
189
  return id;
@@ -205,6 +282,94 @@ export class WindowManager {
205
282
  }
206
283
  }
207
284
  }
285
+ /* -------------------------------------------------------------- */
286
+ /* Snapshot / restore */
287
+ /* -------------------------------------------------------------- */
288
+ /**
289
+ * The whole desktop as one JSON-safe document.
290
+ *
291
+ * Reads every workspace and every window across ALL of them, not just the
292
+ * active one — a snapshot that only captured what was on screen would quietly
293
+ * drop the other workspaces the moment it was restored.
294
+ */
295
+ snapshot() {
296
+ return {
297
+ version: SNAPSHOT_VERSION,
298
+ savedAt: Date.now(),
299
+ activeWorkspaceId: this.activeWorkspaceId,
300
+ workspaces: this.workspaces.map((w) => ({ ...w })),
301
+ windows: this.windows.map((w) => {
302
+ const app = this.apps[w.appId];
303
+ const snap = {
304
+ appId: w.appId,
305
+ workspaceId: w.workspaceId,
306
+ frame: { pos: { ...w.pos }, size: w.size ? { ...w.size } : null },
307
+ state: w.state,
308
+ restore: w.restore
309
+ ? { pos: { ...w.restore.pos }, size: w.restore.size ? { ...w.restore.size } : null }
310
+ : null,
311
+ z: w.z
312
+ };
313
+ // Only record a title the user changed. Storing the app's own default
314
+ // would pin it, so renaming the app in code would not reach any
315
+ // window restored from an older snapshot.
316
+ if (app && w.title !== app.title)
317
+ snap.title = w.title;
318
+ if (w.props !== undefined)
319
+ snap.props = w.props;
320
+ return snap;
321
+ })
322
+ };
323
+ }
324
+ /**
325
+ * Replace the desktop with a snapshot.
326
+ *
327
+ * Named applySnapshot, not restore: `restore(id)` already means "un-maximize
328
+ * this window", and one verb cannot mean both.
329
+ *
330
+ * Windows whose `appId` is no longer registered are SKIPPED rather than
331
+ * failing the whole restore: an app can be removed between export and import,
332
+ * and losing one window beats losing the layout. Same for a window pointing at
333
+ * a workspace the snapshot does not contain.
334
+ */
335
+ applySnapshot(snapshot) {
336
+ this.windows = [];
337
+ this.workspaces = snapshot.workspaces.map((w) => ({ ...w }));
338
+ if (this.workspaces.length === 0)
339
+ this.workspaces = [{ id: 'w1', name: '1' }];
340
+ // A plain array, not a Set: there are a handful of workspaces, and a Set in
341
+ // a .svelte.ts file is a reactivity trap the lint rightly flags.
342
+ const known = this.workspaces.map((w) => w.id);
343
+ this.activeWorkspaceId = known.includes(snapshot.activeWorkspaceId)
344
+ ? snapshot.activeWorkspaceId
345
+ : this.workspaces[0].id;
346
+ let z = 0;
347
+ for (const s of snapshot.windows) {
348
+ const app = this.apps[s.appId];
349
+ if (!app || !known.includes(s.workspaceId))
350
+ continue;
351
+ this.windows.push({
352
+ id: `${s.appId}#${++this.#seq}`,
353
+ appId: s.appId,
354
+ workspaceId: s.workspaceId,
355
+ title: s.title ?? app.title,
356
+ icon: app.icon,
357
+ pos: { ...s.frame.pos },
358
+ size: s.frame.size ? { ...s.frame.size } : null,
359
+ z: (z = Math.max(z, s.z)),
360
+ state: s.state,
361
+ restore: s.restore,
362
+ component: app.component,
363
+ resizable: app.resizable ?? true,
364
+ minimizable: app.minimizable ?? true,
365
+ bodyClass: app.bodyClass,
366
+ actions: app.actions,
367
+ props: s.props ?? app.props
368
+ });
369
+ }
370
+ this.#z = z;
371
+ this.focusedId = this.activeWindows.at(-1)?.id ?? null;
372
+ }
208
373
  /** Half / quadrant tiling against the work area. */
209
374
  snap(id, zone) {
210
375
  const w = this.#find(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "3.15.0",
3
+ "version": "3.16.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",