@signal9/era-ui 3.16.0 → 3.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.
@@ -23,10 +23,16 @@
23
23
 
24
24
  <!-- md with xxs buttons, matching the app's other bars. It was sm + size="icon"
25
25
  — a 22px bar of 14px buttons around 10px glyphs, the smallest combination in
26
- the ladder, which is why the formatting controls read as unhittable. -->
26
+ the ladder, which is why the formatting controls read as unhittable.
27
+
28
+ The border is a deliberate exception to "shadow, not border" (design rule 7):
29
+ this panel floats over the DOCUMENT, not over chrome, so on a light theme its
30
+ elevated fill sits on a page of nearly the same value and the shadow alone
31
+ leaves no discernible edge. border-divider-faded is the token'd hairline, so
32
+ it still tracks the surface rather than being a drawn-on line. -->
27
33
  <Bar
28
34
  size="md"
29
- class="w-max gap-(--era-xxs-inset-md) bg-elevated px-(--era-xxs-inset-md) shadow-lg glass-blur"
35
+ class="w-max gap-(--era-xxs-inset-md) border border-divider-faded bg-elevated px-(--era-xxs-inset-md) shadow-lg glass-blur"
30
36
  >
31
37
  {#each items as item (item.mark)}
32
38
  {@const Icon = item.icon}
@@ -1,4 +1,6 @@
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';
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 } from './wm.svelte.js';
2
+ export { persistWorkspace, loadWorkspace, saveWorkspace, clearWorkspace, WORKSPACE_STORAGE_KEY } from './workspace-storage.svelte.js';
3
+ export { type SnapZone, type Workspace } from './wm.svelte.js';
2
4
  export { NotificationService, getNotifications, setNotificationContext, type AppNotification, type NotifyOptions, type NotificationAction, type NotificationTone } from './notifications.svelte.js';
3
5
  export { LAYER } from './layers.js';
4
6
  export { default as Desktop } from './desktop.svelte';
package/dist/os/index.js CHANGED
@@ -3,6 +3,8 @@
3
3
  // components that render them: Desktop host, Window, Taskbar (with its
4
4
  // built-in command bar), Toaster, NotificationCenter.
5
5
  export { WindowManager, getWindowManager, setWindowManagerContext, SNAPSHOT_VERSION, readSnapshot } from './wm.svelte.js';
6
+ export { persistWorkspace, loadWorkspace, saveWorkspace, clearWorkspace, WORKSPACE_STORAGE_KEY } from './workspace-storage.svelte.js';
7
+ export {} from './wm.svelte.js';
6
8
  export { NotificationService, getNotifications, setNotificationContext } from './notifications.svelte.js';
7
9
  export { LAYER } from './layers.js';
8
10
  export { default as Desktop } from './desktop.svelte';
@@ -0,0 +1,25 @@
1
+ import { type WindowManager } from './wm.svelte.js';
2
+ export declare const WORKSPACE_STORAGE_KEY = "era-ui:workspace";
3
+ /** Read a saved snapshot and apply it. Returns whether anything was restored. */
4
+ export declare function loadWorkspace(wm: WindowManager, key?: string): boolean;
5
+ /** Write the current desktop. Returns false when storage rejected it. */
6
+ export declare function saveWorkspace(wm: WindowManager, key?: string): boolean;
7
+ export declare function clearWorkspace(key?: string): void;
8
+ /**
9
+ * Restore on setup, then save whenever the desktop changes.
10
+ *
11
+ * Call from a component's script (it registers an `$effect`, so it needs a
12
+ * component context). The load is synchronous and happens immediately, before
13
+ * the first paint.
14
+ *
15
+ * Writes are debounced: dragging a window mutates `pos` on every pointer move,
16
+ * and serialising the whole desktop per frame would be pointless work. The
17
+ * trailing write always lands because the effect's teardown flushes a pending
18
+ * timer.
19
+ */
20
+ export declare function persistWorkspace(wm: WindowManager, options?: {
21
+ key?: string;
22
+ debounce?: number;
23
+ }): {
24
+ restored: boolean;
25
+ };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Persisting the desktop across reloads.
3
+ *
4
+ * WHY localStorage, and not something newer
5
+ * -----------------------------------------
6
+ * The payload is a few kB of JSON that must be read BEFORE the first paint. That
7
+ * one requirement rules out every modern alternative, because they are all async:
8
+ *
9
+ * IndexedDB async. Large, structured, transactional — none of which this
10
+ * needs, and the desktop would paint empty and then pop windows
11
+ * in a frame later.
12
+ * OPFS async, and built for large or binary files with real
13
+ * read/write handles. A 2kB JSON blob is not that.
14
+ * Storage Buckets async, and still Chromium-only. Its actual feature is
15
+ * per-bucket eviction and quota policy, which matters when you
16
+ * have several independent stores to prioritise between. One
17
+ * layout snapshot is not that either.
18
+ *
19
+ * localStorage is synchronous, which here is the feature rather than the
20
+ * compromise: the snapshot is read and applied during setup, so the desktop's
21
+ * first paint is already the restored layout — no flash of an empty workspace.
22
+ *
23
+ * The genuinely modern piece worth adopting IS used: navigator.storage.persist()
24
+ * asks the browser to exempt this origin from eviction under storage pressure.
25
+ * Without it localStorage is "best effort" and can be cleared when the device is
26
+ * low on space. It is requested once, non-blocking, and its refusal is harmless.
27
+ */
28
+ import { untrack } from 'svelte';
29
+ import { readSnapshot } from './wm.svelte.js';
30
+ export const WORKSPACE_STORAGE_KEY = 'era-ui:workspace';
31
+ /** Read a saved snapshot and apply it. Returns whether anything was restored. */
32
+ export function loadWorkspace(wm, key = WORKSPACE_STORAGE_KEY) {
33
+ if (typeof localStorage === 'undefined')
34
+ return false;
35
+ let raw = null;
36
+ try {
37
+ raw = localStorage.getItem(key);
38
+ }
39
+ catch {
40
+ return false; // private mode, or storage disabled
41
+ }
42
+ if (!raw)
43
+ return false;
44
+ // readSnapshot is the gate: this string is untrusted (hand-edited, stale, or
45
+ // written by a newer version), so it is validated rather than cast.
46
+ const snapshot = readSnapshot(raw);
47
+ if (!snapshot)
48
+ return false;
49
+ wm.applySnapshot(snapshot);
50
+ return true;
51
+ }
52
+ /** Write the current desktop. Returns false when storage rejected it. */
53
+ export function saveWorkspace(wm, key = WORKSPACE_STORAGE_KEY) {
54
+ if (typeof localStorage === 'undefined')
55
+ return false;
56
+ try {
57
+ localStorage.setItem(key, JSON.stringify(wm.snapshot()));
58
+ return true;
59
+ }
60
+ catch {
61
+ // Quota exceeded or private mode. A layout is convenience state, so this
62
+ // stays silent — unlike the Notes store, where a failed save loses writing
63
+ // and the user has to be told.
64
+ return false;
65
+ }
66
+ }
67
+ export function clearWorkspace(key = WORKSPACE_STORAGE_KEY) {
68
+ try {
69
+ localStorage?.removeItem(key);
70
+ }
71
+ catch {
72
+ /* nothing to do — the goal was for it to be gone */
73
+ }
74
+ }
75
+ /**
76
+ * Restore on setup, then save whenever the desktop changes.
77
+ *
78
+ * Call from a component's script (it registers an `$effect`, so it needs a
79
+ * component context). The load is synchronous and happens immediately, before
80
+ * the first paint.
81
+ *
82
+ * Writes are debounced: dragging a window mutates `pos` on every pointer move,
83
+ * and serialising the whole desktop per frame would be pointless work. The
84
+ * trailing write always lands because the effect's teardown flushes a pending
85
+ * timer.
86
+ */
87
+ export function persistWorkspace(wm, options = {}) {
88
+ const key = options.key ?? WORKSPACE_STORAGE_KEY;
89
+ const wait = options.debounce ?? 400;
90
+ const restored = loadWorkspace(wm, key);
91
+ // Ask for durable storage. Fire-and-forget: a refusal (or an engine without
92
+ // the API) just leaves the default best-effort behaviour.
93
+ void navigator?.storage?.persist?.().catch(() => { });
94
+ $effect(() => {
95
+ // Touch what a snapshot is made of, so the effect re-runs when any of it
96
+ // changes. Reading these is the subscription.
97
+ wm.windows.forEach((w) => {
98
+ void w.pos.x;
99
+ void w.pos.y;
100
+ void w.size?.width;
101
+ void w.size?.height;
102
+ void w.state;
103
+ void w.z;
104
+ void w.title;
105
+ void w.workspaceId;
106
+ void w.props;
107
+ });
108
+ void wm.workspaces.length;
109
+ void wm.activeWorkspaceId;
110
+ const timer = setTimeout(() => {
111
+ // untrack: snapshot() reads the same state this effect depends on, and
112
+ // re-reading it inside would re-subscribe on every write.
113
+ untrack(() => saveWorkspace(wm, key));
114
+ }, wait);
115
+ return () => clearTimeout(timer);
116
+ });
117
+ return { restored };
118
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",