@signal9/era-ui 3.17.0 → 4.0.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.
@@ -1,9 +1,16 @@
1
- import { type WindowManager } from './wm.svelte.js';
1
+ import { type PaneManager } from './pane-manager.svelte.js';
2
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;
3
+ /**
4
+ * Read a saved snapshot and apply it. Returns whether anything was restored.
5
+ *
6
+ * Prefers this tab's own layout (sessionStorage) and falls back to the shared
7
+ * seed (localStorage), so a reload is exact and a brand-new tab still opens on
8
+ * your most recent arrangement.
9
+ */
10
+ export declare function loadWorkspace(pm: PaneManager, key?: string): boolean;
11
+ /** Write the current desktop to both stores. Returns false if neither accepted it. */
12
+ export declare function saveWorkspace(pm: PaneManager, key?: string): boolean;
13
+ /** Forget the layout — this tab's and the shared seed. */
7
14
  export declare function clearWorkspace(key?: string): void;
8
15
  /**
9
16
  * Restore on setup, then save whenever the desktop changes.
@@ -12,12 +19,12 @@ export declare function clearWorkspace(key?: string): void;
12
19
  * component context). The load is synchronous and happens immediately, before
13
20
  * the first paint.
14
21
  *
15
- * Writes are debounced: dragging a window mutates `pos` on every pointer move,
22
+ * Writes are debounced: dragging a pane mutates `pos` on every pointer move,
16
23
  * and serialising the whole desktop per frame would be pointless work. The
17
24
  * trailing write always lands because the effect's teardown flushes a pending
18
25
  * timer.
19
26
  */
20
- export declare function persistWorkspace(wm: WindowManager, options?: {
27
+ export declare function persistWorkspace(pm: PaneManager, options?: {
21
28
  key?: string;
22
29
  debounce?: number;
23
30
  }): {
@@ -1,75 +1,131 @@
1
1
  /**
2
2
  * Persisting the desktop across reloads.
3
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:
4
+ * WHERE IT IS STORED, AND WHY IT IS TWO PLACES
5
+ * --------------------------------------------
6
+ * A pane layout is VIEW state, not document state the same kind of thing as
7
+ * scroll position. Nobody expects scrolling one tab to scroll another, and the
8
+ * same holds here: two tabs of the same app are two workspaces, not two views of
9
+ * one workspace. That observation decides the whole design.
8
10
  *
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.
11
+ * So the live layout lives in sessionStorage, which is scoped to a single tab and
12
+ * survives reload. localStorage keeps a copy purely as the SEED for a tab that
13
+ * has no layout of its own yet.
18
14
  *
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.
15
+ * sessionStorage this tab's layout. Authoritative while the tab is open,
16
+ * isolated from every other tab, gone when it closes.
17
+ * localStorage the most recently saved layout, read only when a tab starts
18
+ * with an empty sessionStorage — i.e. a brand-new tab, so a
19
+ * fresh one opens on your last arrangement instead of nothing.
22
20
  *
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.
21
+ * Two tabs therefore cannot clobber each other. They still race on the seed, but
22
+ * a lost race there only changes what a FUTURE new tab starts from never what
23
+ * any open tab is showing.
24
+ *
25
+ * The alternatives, and why not:
26
+ *
27
+ * `storage` event / BroadcastChannel — mirror every tab to one shared layout.
28
+ * Both work, and both are wrong HERE: dragging a pane in one tab would move
29
+ * it in the other. That is the correct design for a synced setting (theme,
30
+ * auth), not for a workspace.
31
+ * Web Locks leader election — `navigator.locks.request()` held forever elects
32
+ * one writer, which is how real single-writer coordination is done
33
+ * (RxDB, Yjs providers). It fixes the clobber by making the other tabs
34
+ * read-only, which is worse than isolation for symmetric tabs: whichever you
35
+ * opened second silently stops remembering anything.
36
+ * CRDT / merge — vast machinery for a value no one is collaborating on.
37
+ *
38
+ * WHY localStorage AND NOT SOMETHING NEWER
39
+ * ----------------------------------------
40
+ * The payload is a few kB of JSON that must be read BEFORE the first paint, which
41
+ * rules out every modern alternative, because they are all async: IndexedDB,
42
+ * OPFS, and Storage Buckets alike would paint an empty desktop and pop the
43
+ * panes in a frame later. Synchronous access is the feature here, not the
44
+ * compromise. Their real strengths do not apply either — OPFS is for large or
45
+ * binary files with read/write handles, and Storage Buckets exists for per-bucket
46
+ * eviction policy across several competing stores (and is Chromium-only). One
47
+ * ~300-byte layout is neither.
48
+ *
49
+ * The genuinely modern piece IS adopted: navigator.storage.persist() asks the
50
+ * browser to exempt this origin from eviction under storage pressure. Requested
51
+ * once, non-blocking, and a refusal is harmless.
27
52
  */
28
53
  import { untrack } from 'svelte';
29
- import { readSnapshot } from './wm.svelte.js';
54
+ import { readSnapshot } from './pane-manager.svelte.js';
30
55
  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;
56
+ /** Both stores, newest-first: this tab's own layout, then the new-tab seed. */
57
+ function stores() {
58
+ const out = [];
36
59
  try {
37
- raw = localStorage.getItem(key);
60
+ if (typeof sessionStorage !== 'undefined')
61
+ out.push(sessionStorage);
38
62
  }
39
63
  catch {
40
- return false; // private mode, or storage disabled
64
+ /* storage disabled */
41
65
  }
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
66
  try {
57
- localStorage.setItem(key, JSON.stringify(wm.snapshot()));
58
- return true;
67
+ if (typeof localStorage !== 'undefined')
68
+ out.push(localStorage);
59
69
  }
60
70
  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;
71
+ /* private mode */
65
72
  }
73
+ return out;
66
74
  }
67
- export function clearWorkspace(key = WORKSPACE_STORAGE_KEY) {
68
- try {
69
- localStorage?.removeItem(key);
75
+ /**
76
+ * Read a saved snapshot and apply it. Returns whether anything was restored.
77
+ *
78
+ * Prefers this tab's own layout (sessionStorage) and falls back to the shared
79
+ * seed (localStorage), so a reload is exact and a brand-new tab still opens on
80
+ * your most recent arrangement.
81
+ */
82
+ export function loadWorkspace(pm, key = WORKSPACE_STORAGE_KEY) {
83
+ for (const store of stores()) {
84
+ let raw = null;
85
+ try {
86
+ raw = store.getItem(key);
87
+ }
88
+ catch {
89
+ continue;
90
+ }
91
+ if (!raw)
92
+ continue;
93
+ // readSnapshot is the gate: this string is untrusted (hand-edited, stale, or
94
+ // written by a newer version), so it is validated rather than cast.
95
+ const snapshot = readSnapshot(raw);
96
+ if (!snapshot)
97
+ continue;
98
+ pm.applySnapshot(snapshot);
99
+ return true;
70
100
  }
71
- catch {
72
- /* nothing to do — the goal was for it to be gone */
101
+ return false;
102
+ }
103
+ /** Write the current desktop to both stores. Returns false if neither accepted it. */
104
+ export function saveWorkspace(pm, key = WORKSPACE_STORAGE_KEY) {
105
+ const json = JSON.stringify(pm.snapshot());
106
+ let ok = false;
107
+ for (const store of stores()) {
108
+ try {
109
+ store.setItem(key, json);
110
+ ok = true;
111
+ }
112
+ catch {
113
+ // Quota exceeded or private mode. A layout is convenience state, so this
114
+ // stays silent — unlike the Notes store, where a failed save loses
115
+ // writing and the user has to be told.
116
+ }
117
+ }
118
+ return ok;
119
+ }
120
+ /** Forget the layout — this tab's and the shared seed. */
121
+ export function clearWorkspace(key = WORKSPACE_STORAGE_KEY) {
122
+ for (const store of stores()) {
123
+ try {
124
+ store.removeItem(key);
125
+ }
126
+ catch {
127
+ /* nothing to do — the goal was for it to be gone */
128
+ }
73
129
  }
74
130
  }
75
131
  /**
@@ -79,22 +135,22 @@ export function clearWorkspace(key = WORKSPACE_STORAGE_KEY) {
79
135
  * component context). The load is synchronous and happens immediately, before
80
136
  * the first paint.
81
137
  *
82
- * Writes are debounced: dragging a window mutates `pos` on every pointer move,
138
+ * Writes are debounced: dragging a pane mutates `pos` on every pointer move,
83
139
  * and serialising the whole desktop per frame would be pointless work. The
84
140
  * trailing write always lands because the effect's teardown flushes a pending
85
141
  * timer.
86
142
  */
87
- export function persistWorkspace(wm, options = {}) {
143
+ export function persistWorkspace(pm, options = {}) {
88
144
  const key = options.key ?? WORKSPACE_STORAGE_KEY;
89
145
  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.
146
+ const restored = loadWorkspace(pm, key);
147
+ // Fire-and-forget: a refusal (or an engine without the API) just leaves the
148
+ // default best-effort behaviour.
93
149
  void navigator?.storage?.persist?.().catch(() => { });
94
150
  $effect(() => {
95
151
  // Touch what a snapshot is made of, so the effect re-runs when any of it
96
152
  // changes. Reading these is the subscription.
97
- wm.windows.forEach((w) => {
153
+ pm.panes.forEach((w) => {
98
154
  void w.pos.x;
99
155
  void w.pos.y;
100
156
  void w.size?.width;
@@ -105,12 +161,12 @@ export function persistWorkspace(wm, options = {}) {
105
161
  void w.workspaceId;
106
162
  void w.props;
107
163
  });
108
- void wm.workspaces.length;
109
- void wm.activeWorkspaceId;
164
+ void pm.workspaces.length;
165
+ void pm.activeWorkspaceId;
110
166
  const timer = setTimeout(() => {
111
167
  // untrack: snapshot() reads the same state this effect depends on, and
112
168
  // re-reading it inside would re-subscribe on every write.
113
- untrack(() => saveWorkspace(wm, key));
169
+ untrack(() => saveWorkspace(pm, key));
114
170
  }, wait);
115
171
  return () => clearTimeout(timer);
116
172
  });
@@ -7,7 +7,7 @@
7
7
  import Handle from './pane-handle.svelte';
8
8
  import Content from './pane-content.svelte';
9
9
  import Close from './pane-close.svelte';
10
- import AppWindow from '@lucide/svelte/icons/app-window';
10
+ import AppPane from '@lucide/svelte/icons/app-window';
11
11
 
12
12
  let {
13
13
  ref = $bindable(null),
@@ -15,7 +15,7 @@
15
15
  size = $bindable(null),
16
16
  resizable = false,
17
17
  title,
18
- icon = AppWindow,
18
+ icon = AppPane,
19
19
  disabled = false,
20
20
  plugins = [],
21
21
  onDragStart,
@@ -791,7 +791,7 @@
791
791
 
792
792
  const playLabel = $derived(ended ? 'Replay' : paused ? 'Play' : 'Pause');
793
793
  const muteLabel = $derived(mutedState || volumeState === 0 ? 'Unmute' : 'Mute');
794
- // Prefer seekable.end when smaller than duration (HLS live windows); fall
794
+ // Prefer seekable.end when smaller than duration (HLS live panes); fall
795
795
  // back to duration for VOD where the engine may report seekable === duration.
796
796
  const timeMax = $derived(
797
797
  seekableEnd > 0 && seekableEnd < (duration || Infinity)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "3.17.0",
3
+ "version": "4.0.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",
@@ -1,7 +0,0 @@
1
- import { type AppWindow } from './wm.svelte.js';
2
- type $$ComponentProps = {
3
- window: AppWindow;
4
- };
5
- declare const Window: import("svelte").Component<$$ComponentProps, {}, "">;
6
- type Window = ReturnType<typeof Window>;
7
- export default Window;