@signal9/era-ui 3.16.0 → 3.17.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.
|
@@ -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}
|
package/dist/os/index.d.ts
CHANGED
|
@@ -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
|
|
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,32 @@
|
|
|
1
|
+
import { type WindowManager } from './wm.svelte.js';
|
|
2
|
+
export declare const WORKSPACE_STORAGE_KEY = "era-ui:workspace";
|
|
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(wm: WindowManager, key?: string): boolean;
|
|
11
|
+
/** Write the current desktop to both stores. Returns false if neither accepted it. */
|
|
12
|
+
export declare function saveWorkspace(wm: WindowManager, key?: string): boolean;
|
|
13
|
+
/** Forget the layout — this tab's and the shared seed. */
|
|
14
|
+
export declare function clearWorkspace(key?: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Restore on setup, then save whenever the desktop changes.
|
|
17
|
+
*
|
|
18
|
+
* Call from a component's script (it registers an `$effect`, so it needs a
|
|
19
|
+
* component context). The load is synchronous and happens immediately, before
|
|
20
|
+
* the first paint.
|
|
21
|
+
*
|
|
22
|
+
* Writes are debounced: dragging a window mutates `pos` on every pointer move,
|
|
23
|
+
* and serialising the whole desktop per frame would be pointless work. The
|
|
24
|
+
* trailing write always lands because the effect's teardown flushes a pending
|
|
25
|
+
* timer.
|
|
26
|
+
*/
|
|
27
|
+
export declare function persistWorkspace(wm: WindowManager, options?: {
|
|
28
|
+
key?: string;
|
|
29
|
+
debounce?: number;
|
|
30
|
+
}): {
|
|
31
|
+
restored: boolean;
|
|
32
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisting the desktop across reloads.
|
|
3
|
+
*
|
|
4
|
+
* WHERE IT IS STORED, AND WHY IT IS TWO PLACES
|
|
5
|
+
* --------------------------------------------
|
|
6
|
+
* A window 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.
|
|
10
|
+
*
|
|
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.
|
|
14
|
+
*
|
|
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.
|
|
20
|
+
*
|
|
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 window 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
|
+
* windows 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.
|
|
52
|
+
*/
|
|
53
|
+
import { untrack } from 'svelte';
|
|
54
|
+
import { readSnapshot } from './wm.svelte.js';
|
|
55
|
+
export const WORKSPACE_STORAGE_KEY = 'era-ui:workspace';
|
|
56
|
+
/** Both stores, newest-first: this tab's own layout, then the new-tab seed. */
|
|
57
|
+
function stores() {
|
|
58
|
+
const out = [];
|
|
59
|
+
try {
|
|
60
|
+
if (typeof sessionStorage !== 'undefined')
|
|
61
|
+
out.push(sessionStorage);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* storage disabled */
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
if (typeof localStorage !== 'undefined')
|
|
68
|
+
out.push(localStorage);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* private mode */
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
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(wm, 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
|
+
wm.applySnapshot(snapshot);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
/** Write the current desktop to both stores. Returns false if neither accepted it. */
|
|
104
|
+
export function saveWorkspace(wm, key = WORKSPACE_STORAGE_KEY) {
|
|
105
|
+
const json = JSON.stringify(wm.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
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Restore on setup, then save whenever the desktop changes.
|
|
133
|
+
*
|
|
134
|
+
* Call from a component's script (it registers an `$effect`, so it needs a
|
|
135
|
+
* component context). The load is synchronous and happens immediately, before
|
|
136
|
+
* the first paint.
|
|
137
|
+
*
|
|
138
|
+
* Writes are debounced: dragging a window mutates `pos` on every pointer move,
|
|
139
|
+
* and serialising the whole desktop per frame would be pointless work. The
|
|
140
|
+
* trailing write always lands because the effect's teardown flushes a pending
|
|
141
|
+
* timer.
|
|
142
|
+
*/
|
|
143
|
+
export function persistWorkspace(wm, options = {}) {
|
|
144
|
+
const key = options.key ?? WORKSPACE_STORAGE_KEY;
|
|
145
|
+
const wait = options.debounce ?? 400;
|
|
146
|
+
const restored = loadWorkspace(wm, key);
|
|
147
|
+
// Fire-and-forget: a refusal (or an engine without the API) just leaves the
|
|
148
|
+
// default best-effort behaviour.
|
|
149
|
+
void navigator?.storage?.persist?.().catch(() => { });
|
|
150
|
+
$effect(() => {
|
|
151
|
+
// Touch what a snapshot is made of, so the effect re-runs when any of it
|
|
152
|
+
// changes. Reading these is the subscription.
|
|
153
|
+
wm.windows.forEach((w) => {
|
|
154
|
+
void w.pos.x;
|
|
155
|
+
void w.pos.y;
|
|
156
|
+
void w.size?.width;
|
|
157
|
+
void w.size?.height;
|
|
158
|
+
void w.state;
|
|
159
|
+
void w.z;
|
|
160
|
+
void w.title;
|
|
161
|
+
void w.workspaceId;
|
|
162
|
+
void w.props;
|
|
163
|
+
});
|
|
164
|
+
void wm.workspaces.length;
|
|
165
|
+
void wm.activeWorkspaceId;
|
|
166
|
+
const timer = setTimeout(() => {
|
|
167
|
+
// untrack: snapshot() reads the same state this effect depends on, and
|
|
168
|
+
// re-reading it inside would re-subscribe on every write.
|
|
169
|
+
untrack(() => saveWorkspace(wm, key));
|
|
170
|
+
}, wait);
|
|
171
|
+
return () => clearTimeout(timer);
|
|
172
|
+
});
|
|
173
|
+
return { restored };
|
|
174
|
+
}
|