@signal9/era-ui 1.12.2 → 1.13.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/era-ui.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/os/desktop.svelte +58 -0
- package/dist/os/desktop.svelte.d.ts +17 -0
- package/dist/os/index.d.ts +10 -0
- package/dist/os/index.js +14 -0
- package/dist/os/launcher.svelte +83 -0
- package/dist/os/launcher.svelte.d.ts +8 -0
- package/dist/os/layers.d.ts +25 -0
- package/dist/os/layers.js +25 -0
- package/dist/os/notification-center.svelte +85 -0
- package/dist/os/notification-center.svelte.d.ts +6 -0
- package/dist/os/notifications.svelte.d.ts +48 -0
- package/dist/os/notifications.svelte.js +80 -0
- package/dist/os/taskbar.svelte +97 -0
- package/dist/os/taskbar.svelte.d.ts +16 -0
- package/dist/os/toast.svelte +57 -0
- package/dist/os/toast.svelte.d.ts +7 -0
- package/dist/os/toaster.svelte +30 -0
- package/dist/os/toaster.svelte.d.ts +4 -0
- package/dist/os/window.svelte +60 -0
- package/dist/os/window.svelte.d.ts +7 -0
- package/dist/os/wm.svelte.d.ts +99 -0
- package/dist/os/wm.svelte.js +172 -0
- package/dist/ui/pane/pane-root.svelte +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { type Component } from 'svelte';
|
|
2
|
+
import type { IconProps } from '@lucide/svelte';
|
|
3
|
+
export type WindowState = 'normal' | 'minimized' | 'maximized';
|
|
4
|
+
/** Snap targets: the four edges (halves), four corners (quadrants), or full. */
|
|
5
|
+
export type SnapZone = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'maximize';
|
|
6
|
+
/** Props every app body receives — its own window record and the manager, so it
|
|
7
|
+
* can read its state and drive the desktop (close itself, spawn more…). */
|
|
8
|
+
export type AppProps = {
|
|
9
|
+
window: AppWindow;
|
|
10
|
+
wm: WindowManager;
|
|
11
|
+
};
|
|
12
|
+
/** What a window renders as its body. */
|
|
13
|
+
export type AppComponent = Component<AppProps>;
|
|
14
|
+
/** A registered "app": the template every window of that app is spawned from. */
|
|
15
|
+
export interface AppDefinition {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
icon?: Component<IconProps> | null;
|
|
19
|
+
/** Body renderer. Omit for a chromeless/placeholder window. */
|
|
20
|
+
component?: AppComponent;
|
|
21
|
+
defaultSize?: {
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
};
|
|
25
|
+
/** Only one live instance; opening again focuses the existing one. */
|
|
26
|
+
singleton?: boolean;
|
|
27
|
+
/** Show a resize grip (default true). */
|
|
28
|
+
resizable?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** A live window instance in the registry. */
|
|
31
|
+
export interface AppWindow {
|
|
32
|
+
id: string;
|
|
33
|
+
appId: string;
|
|
34
|
+
title: string;
|
|
35
|
+
icon?: Component<IconProps> | null;
|
|
36
|
+
/** neodrag offset — bound straight to Pane.Root's `position`. */
|
|
37
|
+
pos: {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
};
|
|
41
|
+
/** Explicit size once set — bound to Pane.Root's `size`. */
|
|
42
|
+
size: {
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
} | null;
|
|
46
|
+
/** Stacking order within the desktop (higher = nearer the front). */
|
|
47
|
+
z: number;
|
|
48
|
+
state: WindowState;
|
|
49
|
+
/** Geometry snapshot to restore to after un-maximize / un-snap. */
|
|
50
|
+
restore: {
|
|
51
|
+
pos: {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
};
|
|
55
|
+
size: {
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
} | null;
|
|
59
|
+
} | null;
|
|
60
|
+
component?: AppComponent;
|
|
61
|
+
resizable: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The source of truth for the desktop: which windows exist, their geometry, and
|
|
65
|
+
* their stacking / focus order. Everything visual (Desktop, Window, Taskbar,
|
|
66
|
+
* Alt-Tab, …) is a pure view of this. Runes-based, shared via context.
|
|
67
|
+
*/
|
|
68
|
+
export declare class WindowManager {
|
|
69
|
+
#private;
|
|
70
|
+
/** Every live window. Order is spawn order; paint order is by `z`. */
|
|
71
|
+
windows: AppWindow[];
|
|
72
|
+
/** Registered app templates, keyed by id. */
|
|
73
|
+
apps: Record<string, AppDefinition>;
|
|
74
|
+
/** Desktop viewport size — set by the Desktop host; drives maximize / snap. */
|
|
75
|
+
bounds: {
|
|
76
|
+
width: number;
|
|
77
|
+
height: number;
|
|
78
|
+
};
|
|
79
|
+
/** The focused window = the top-most non-minimized one. */
|
|
80
|
+
focusedId: string | null;
|
|
81
|
+
register(app: AppDefinition): void;
|
|
82
|
+
registerMany(apps: AppDefinition[]): void;
|
|
83
|
+
/** Spawn a window for an app (or focus the existing one if it's a singleton). */
|
|
84
|
+
open(appId: string, overrides?: Partial<Pick<AppWindow, 'title' | 'pos' | 'size' | 'resizable'>>): string | null;
|
|
85
|
+
close(id: string): void;
|
|
86
|
+
/** Bring a window to the front (and un-minimize it if needed). */
|
|
87
|
+
focus(id: string): void;
|
|
88
|
+
minimize(id: string): void;
|
|
89
|
+
/** Taskbar behaviour: focus if not focused, else minimize; restore if minimized. */
|
|
90
|
+
toggleMinimize(id: string): void;
|
|
91
|
+
maximize(id: string): void;
|
|
92
|
+
/** Un-maximize / un-snap back to the pre-change geometry (or just un-minimize). */
|
|
93
|
+
restore(id: string): void;
|
|
94
|
+
toggleMaximize(id: string): void;
|
|
95
|
+
/** Half / quadrant tiling against the desktop bounds. */
|
|
96
|
+
snap(id: string, zone: SnapZone): void;
|
|
97
|
+
}
|
|
98
|
+
export declare function setWindowManagerContext(wm: WindowManager): WindowManager;
|
|
99
|
+
export declare function getWindowManager(): WindowManager;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const DEFAULT_SIZE = { width: 480, height: 320 };
|
|
3
|
+
/**
|
|
4
|
+
* The source of truth for the desktop: which windows exist, their geometry, and
|
|
5
|
+
* their stacking / focus order. Everything visual (Desktop, Window, Taskbar,
|
|
6
|
+
* Alt-Tab, …) is a pure view of this. Runes-based, shared via context.
|
|
7
|
+
*/
|
|
8
|
+
export class WindowManager {
|
|
9
|
+
/** Every live window. Order is spawn order; paint order is by `z`. */
|
|
10
|
+
windows = $state([]);
|
|
11
|
+
/** Registered app templates, keyed by id. */
|
|
12
|
+
apps = $state.raw({});
|
|
13
|
+
/** Desktop viewport size — set by the Desktop host; drives maximize / snap. */
|
|
14
|
+
bounds = $state({ width: 0, height: 0 });
|
|
15
|
+
#z = 0;
|
|
16
|
+
#seq = 0;
|
|
17
|
+
/** The focused window = the top-most non-minimized one. */
|
|
18
|
+
focusedId = $derived.by(() => {
|
|
19
|
+
let top = null;
|
|
20
|
+
for (const w of this.windows) {
|
|
21
|
+
if (w.state === 'minimized')
|
|
22
|
+
continue;
|
|
23
|
+
if (!top || w.z > top.z)
|
|
24
|
+
top = w;
|
|
25
|
+
}
|
|
26
|
+
return top?.id ?? null;
|
|
27
|
+
});
|
|
28
|
+
register(app) {
|
|
29
|
+
this.apps = { ...this.apps, [app.id]: app };
|
|
30
|
+
}
|
|
31
|
+
registerMany(apps) {
|
|
32
|
+
this.apps = { ...this.apps, ...Object.fromEntries(apps.map((a) => [a.id, a])) };
|
|
33
|
+
}
|
|
34
|
+
#find(id) {
|
|
35
|
+
return this.windows.find((w) => w.id === id);
|
|
36
|
+
}
|
|
37
|
+
/** Spawn a window for an app (or focus the existing one if it's a singleton). */
|
|
38
|
+
open(appId, overrides) {
|
|
39
|
+
const app = this.apps[appId];
|
|
40
|
+
if (!app)
|
|
41
|
+
return null;
|
|
42
|
+
if (app.singleton) {
|
|
43
|
+
const existing = this.windows.find((w) => w.appId === appId);
|
|
44
|
+
if (existing) {
|
|
45
|
+
this.restore(existing.id);
|
|
46
|
+
return existing.id;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const id = `${appId}#${++this.#seq}`;
|
|
50
|
+
// Cascade so stacked spawns don't land exactly on top of each other.
|
|
51
|
+
const n = this.windows.length % 8;
|
|
52
|
+
const win = {
|
|
53
|
+
id,
|
|
54
|
+
appId,
|
|
55
|
+
title: overrides?.title ?? app.title,
|
|
56
|
+
icon: app.icon,
|
|
57
|
+
pos: overrides?.pos ?? { x: 48 + n * 28, y: 48 + n * 28 },
|
|
58
|
+
size: overrides?.size ?? app.defaultSize ?? { ...DEFAULT_SIZE },
|
|
59
|
+
z: ++this.#z,
|
|
60
|
+
state: 'normal',
|
|
61
|
+
restore: null,
|
|
62
|
+
component: app.component,
|
|
63
|
+
resizable: overrides?.resizable ?? app.resizable ?? true
|
|
64
|
+
};
|
|
65
|
+
this.windows.push(win);
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
close(id) {
|
|
69
|
+
this.windows = this.windows.filter((w) => w.id !== id);
|
|
70
|
+
}
|
|
71
|
+
/** Bring a window to the front (and un-minimize it if needed). */
|
|
72
|
+
focus(id) {
|
|
73
|
+
const w = this.#find(id);
|
|
74
|
+
if (!w)
|
|
75
|
+
return;
|
|
76
|
+
if (w.state === 'minimized')
|
|
77
|
+
w.state = 'normal';
|
|
78
|
+
if (w.z <= this.#z)
|
|
79
|
+
w.z = ++this.#z;
|
|
80
|
+
}
|
|
81
|
+
minimize(id) {
|
|
82
|
+
const w = this.#find(id);
|
|
83
|
+
if (w)
|
|
84
|
+
w.state = 'minimized';
|
|
85
|
+
}
|
|
86
|
+
/** Taskbar behaviour: focus if not focused, else minimize; restore if minimized. */
|
|
87
|
+
toggleMinimize(id) {
|
|
88
|
+
const w = this.#find(id);
|
|
89
|
+
if (!w)
|
|
90
|
+
return;
|
|
91
|
+
if (w.state === 'minimized')
|
|
92
|
+
this.focus(id);
|
|
93
|
+
else if (this.focusedId === id)
|
|
94
|
+
w.state = 'minimized';
|
|
95
|
+
else
|
|
96
|
+
this.focus(id);
|
|
97
|
+
}
|
|
98
|
+
maximize(id) {
|
|
99
|
+
const w = this.#find(id);
|
|
100
|
+
if (!w)
|
|
101
|
+
return;
|
|
102
|
+
if (w.state !== 'maximized') {
|
|
103
|
+
w.restore = { pos: { ...w.pos }, size: w.size ? { ...w.size } : null };
|
|
104
|
+
}
|
|
105
|
+
w.pos = { x: 0, y: 0 };
|
|
106
|
+
w.size = { width: this.bounds.width, height: this.bounds.height };
|
|
107
|
+
w.state = 'maximized';
|
|
108
|
+
this.focus(id);
|
|
109
|
+
}
|
|
110
|
+
/** Un-maximize / un-snap back to the pre-change geometry (or just un-minimize). */
|
|
111
|
+
restore(id) {
|
|
112
|
+
const w = this.#find(id);
|
|
113
|
+
if (!w)
|
|
114
|
+
return;
|
|
115
|
+
if (w.restore) {
|
|
116
|
+
w.pos = w.restore.pos;
|
|
117
|
+
w.size = w.restore.size;
|
|
118
|
+
w.restore = null;
|
|
119
|
+
}
|
|
120
|
+
w.state = 'normal';
|
|
121
|
+
this.focus(id);
|
|
122
|
+
}
|
|
123
|
+
toggleMaximize(id) {
|
|
124
|
+
const w = this.#find(id);
|
|
125
|
+
if (!w)
|
|
126
|
+
return;
|
|
127
|
+
if (w.state === 'maximized')
|
|
128
|
+
this.restore(id);
|
|
129
|
+
else
|
|
130
|
+
this.maximize(id);
|
|
131
|
+
}
|
|
132
|
+
/** Half / quadrant tiling against the desktop bounds. */
|
|
133
|
+
snap(id, zone) {
|
|
134
|
+
if (zone === 'maximize')
|
|
135
|
+
return this.maximize(id);
|
|
136
|
+
const w = this.#find(id);
|
|
137
|
+
if (!w)
|
|
138
|
+
return;
|
|
139
|
+
const { width: W, height: H } = this.bounds;
|
|
140
|
+
const halfW = Math.round(W / 2);
|
|
141
|
+
const halfH = Math.round(H / 2);
|
|
142
|
+
const rects = {
|
|
143
|
+
left: [0, 0, halfW, H],
|
|
144
|
+
right: [halfW, 0, W - halfW, H],
|
|
145
|
+
top: [0, 0, W, halfH],
|
|
146
|
+
bottom: [0, halfH, W, H - halfH],
|
|
147
|
+
'top-left': [0, 0, halfW, halfH],
|
|
148
|
+
'top-right': [halfW, 0, W - halfW, halfH],
|
|
149
|
+
'bottom-left': [0, halfH, halfW, H - halfH],
|
|
150
|
+
'bottom-right': [halfW, halfH, W - halfW, H - halfH]
|
|
151
|
+
};
|
|
152
|
+
const [x, y, width, height] = rects[zone];
|
|
153
|
+
if (w.state !== 'maximized' && !w.restore) {
|
|
154
|
+
w.restore = { pos: { ...w.pos }, size: w.size ? { ...w.size } : null };
|
|
155
|
+
}
|
|
156
|
+
w.pos = { x, y };
|
|
157
|
+
w.size = { width, height };
|
|
158
|
+
w.state = 'normal';
|
|
159
|
+
this.focus(id);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const WM_KEY = Symbol('era-wm');
|
|
163
|
+
export function setWindowManagerContext(wm) {
|
|
164
|
+
setContext(WM_KEY, wm);
|
|
165
|
+
return wm;
|
|
166
|
+
}
|
|
167
|
+
export function getWindowManager() {
|
|
168
|
+
const wm = getContext(WM_KEY);
|
|
169
|
+
if (!wm)
|
|
170
|
+
throw new Error('getWindowManager() must be used inside a <Desktop>.');
|
|
171
|
+
return wm;
|
|
172
|
+
}
|
|
@@ -285,6 +285,11 @@
|
|
|
285
285
|
// would fight the drag (the handle bar already has select-none; the content
|
|
286
286
|
// doesn't). Resize handles keep their own resize cursors (more specific).
|
|
287
287
|
ctrlHeld && !resizing ? 'cursor-grab select-none' : '',
|
|
288
|
+
// ...and tabs stop responding to the pointer: bits-ui activates a tab on
|
|
289
|
+
// click AND focus, so a ctrl-press would switch tabs instead of grabbing.
|
|
290
|
+
// pointer-events-none means no focus/click reaches the tab (no activation)
|
|
291
|
+
// and the press falls through to drag the pane.
|
|
292
|
+
ctrlHeld ? '[&_[role=tab]]:pointer-events-none' : '',
|
|
288
293
|
// Promote to its own compositor layer only while dragging, so moves
|
|
289
294
|
// repaint the layer instead of the page; dropped back to auto at idle.
|
|
290
295
|
// neodrag animates the `translate` CSS property (not `transform`), so the
|