@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.
- package/dist/generated-docs/llms-full.txt +1 -1
- package/dist/generated-docs/manifest.json +1 -1
- package/dist/generated-docs/pane.md +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -0
- package/dist/os/desktop.svelte +25 -25
- package/dist/os/desktop.svelte.d.ts +3 -3
- package/dist/os/index.d.ts +3 -3
- package/dist/os/index.js +5 -5
- package/dist/os/layers.d.ts +4 -4
- package/dist/os/layers.js +4 -4
- package/dist/os/notification-center.svelte +4 -4
- package/dist/os/{wm.svelte.d.ts → pane-manager.svelte.d.ts} +50 -50
- package/dist/os/{wm.svelte.js → pane-manager.svelte.js} +49 -49
- package/dist/os/{window.svelte → pane.svelte} +35 -34
- package/dist/os/pane.svelte.d.ts +8 -0
- package/dist/os/taskbar.svelte +38 -38
- package/dist/os/taskbar.svelte.d.ts +6 -6
- package/dist/os/workspace-storage.svelte.d.ts +14 -7
- package/dist/os/workspace-storage.svelte.js +118 -62
- package/dist/ui/pane/pane.svelte +2 -2
- package/dist/ui/video-player/video-player.svelte +1 -1
- package/package.json +1 -1
- package/dist/os/window.svelte.d.ts +0 -7
|
@@ -14,7 +14,7 @@ export const SNAPSHOT_VERSION = 1;
|
|
|
14
14
|
* trusting `as`, and refuses an unknown `version` outright — silently importing
|
|
15
15
|
* a layout you only half understand is worse than declining it.
|
|
16
16
|
*
|
|
17
|
-
* Individual
|
|
17
|
+
* Individual panes are filtered, not fatal: one malformed entry should not
|
|
18
18
|
* cost the user the rest of the layout. applySnapshot() drops unknown appIds the
|
|
19
19
|
* same way, for the same reason.
|
|
20
20
|
*/
|
|
@@ -33,7 +33,7 @@ export function readSnapshot(json) {
|
|
|
33
33
|
return null;
|
|
34
34
|
if (typeof o.activeWorkspaceId !== 'string')
|
|
35
35
|
return null;
|
|
36
|
-
if (!Array.isArray(o.workspaces) || !Array.isArray(o.
|
|
36
|
+
if (!Array.isArray(o.workspaces) || !Array.isArray(o.panes))
|
|
37
37
|
return null;
|
|
38
38
|
const isPoint = (v) => typeof v === 'object' &&
|
|
39
39
|
v !== null &&
|
|
@@ -56,7 +56,7 @@ export function readSnapshot(json) {
|
|
|
56
56
|
if (workspaces.length === 0)
|
|
57
57
|
return null;
|
|
58
58
|
const states = ['normal', 'minimized', 'maximized'];
|
|
59
|
-
const
|
|
59
|
+
const panes = o.panes.filter((w) => {
|
|
60
60
|
if (typeof w !== 'object' || w === null)
|
|
61
61
|
return false;
|
|
62
62
|
const x = w;
|
|
@@ -72,20 +72,20 @@ export function readSnapshot(json) {
|
|
|
72
72
|
savedAt: typeof o.savedAt === 'number' ? o.savedAt : 0,
|
|
73
73
|
activeWorkspaceId: o.activeWorkspaceId,
|
|
74
74
|
workspaces,
|
|
75
|
-
|
|
75
|
+
panes
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
const DEFAULT_SIZE = { width: 480, height: 320 };
|
|
79
79
|
/**
|
|
80
|
-
* The source of truth for the desktop: which workspaces and
|
|
80
|
+
* The source of truth for the desktop: which workspaces and panes exist,
|
|
81
81
|
* their geometry, and their stacking / focus order. Everything visual
|
|
82
|
-
* (Desktop,
|
|
82
|
+
* (Desktop, Pane, Taskbar, Launcher, …) is a pure view of this. Runes-based,
|
|
83
83
|
* shared via context.
|
|
84
84
|
*/
|
|
85
|
-
export class
|
|
86
|
-
/** Every live
|
|
85
|
+
export class PaneManager {
|
|
86
|
+
/** Every live pane, across all workspaces. Order is spawn order; paint
|
|
87
87
|
* order is by `z`; visibility is by workspace. */
|
|
88
|
-
|
|
88
|
+
panes = $state([]);
|
|
89
89
|
/** Registered app templates, keyed by id. */
|
|
90
90
|
apps = $state.raw({});
|
|
91
91
|
/** The virtual desktops. Always at least one. */
|
|
@@ -98,7 +98,7 @@ export class WindowManager {
|
|
|
98
98
|
* The work area = bounds − insets; maximize, snap and drag containment
|
|
99
99
|
* honour it. */
|
|
100
100
|
insets = $state({ top: 0, right: 0, bottom: 0, left: 0 });
|
|
101
|
-
/** Drag containment: a
|
|
101
|
+
/** Drag containment: a pane's bar may never leave the top of the work
|
|
102
102
|
* area, and at least this many px of it must stay reachable on every
|
|
103
103
|
* other side. */
|
|
104
104
|
dragKeep = 25;
|
|
@@ -106,7 +106,7 @@ export class WindowManager {
|
|
|
106
106
|
#seq = 0;
|
|
107
107
|
#wsSeq = 0;
|
|
108
108
|
/** The usable desktop: bounds minus reserved chrome. Everything that places
|
|
109
|
-
* a
|
|
109
|
+
* a pane against "the screen" (maximize, snap, containment) uses this,
|
|
110
110
|
* never raw bounds — a strip hidden behind chrome is as lost as one
|
|
111
111
|
* off-screen. */
|
|
112
112
|
workArea = $derived.by(() => {
|
|
@@ -118,14 +118,14 @@ export class WindowManager {
|
|
|
118
118
|
height: Math.max(0, this.bounds.height - top - bottom)
|
|
119
119
|
};
|
|
120
120
|
});
|
|
121
|
-
/** The active workspace's
|
|
121
|
+
/** The active workspace's panes — what the Desktop paints and the
|
|
122
122
|
* Taskbar lists. */
|
|
123
|
-
|
|
124
|
-
/** The focused
|
|
123
|
+
activePanes = $derived.by(() => this.panes.filter((w) => w.workspaceId === this.activeWorkspaceId));
|
|
124
|
+
/** The focused pane = the top-most non-minimized one on the active
|
|
125
125
|
* workspace. */
|
|
126
126
|
focusedId = $derived.by(() => {
|
|
127
127
|
let top = null;
|
|
128
|
-
for (const w of this.
|
|
128
|
+
for (const w of this.activePanes) {
|
|
129
129
|
if (w.state === 'minimized')
|
|
130
130
|
continue;
|
|
131
131
|
if (!top || w.z > top.z)
|
|
@@ -141,20 +141,20 @@ export class WindowManager {
|
|
|
141
141
|
this.apps = { ...this.apps, ...Object.fromEntries(apps.map((a) => [a.id, a])) };
|
|
142
142
|
}
|
|
143
143
|
#find(id) {
|
|
144
|
-
return this.
|
|
144
|
+
return this.panes.find((w) => w.id === id);
|
|
145
145
|
}
|
|
146
|
-
/* ----------------
|
|
147
|
-
/** Spawn a
|
|
146
|
+
/* ---------------- panes ---------------- */
|
|
147
|
+
/** Spawn a pane for an app (or focus the existing one if it's a
|
|
148
148
|
* singleton — from any workspace; it is brought to the active one). */
|
|
149
149
|
open(appId, overrides) {
|
|
150
150
|
const app = this.apps[appId];
|
|
151
151
|
if (!app)
|
|
152
152
|
return null;
|
|
153
153
|
if (app.singleton) {
|
|
154
|
-
const existing = this.
|
|
154
|
+
const existing = this.panes.find((w) => w.appId === appId);
|
|
155
155
|
if (existing) {
|
|
156
156
|
// Reveal, don't relocate: restore() → focus() switches to the
|
|
157
|
-
// workspace the
|
|
157
|
+
// workspace the pane already lives on, the way every virtual
|
|
158
158
|
// desktop behaves. An explicit override still moves it.
|
|
159
159
|
if (overrides?.workspaceId)
|
|
160
160
|
existing.workspaceId = overrides.workspaceId;
|
|
@@ -165,7 +165,7 @@ export class WindowManager {
|
|
|
165
165
|
const id = `${appId}#${++this.#seq}`;
|
|
166
166
|
// Cascade from the work area's origin so stacked spawns don't land
|
|
167
167
|
// exactly on top of each other (or under top chrome).
|
|
168
|
-
const n = this.
|
|
168
|
+
const n = this.panes.length % 8;
|
|
169
169
|
const { x, y } = this.workArea;
|
|
170
170
|
const win = {
|
|
171
171
|
id,
|
|
@@ -185,13 +185,13 @@ export class WindowManager {
|
|
|
185
185
|
actions: app.actions,
|
|
186
186
|
props: app.props
|
|
187
187
|
};
|
|
188
|
-
this.
|
|
188
|
+
this.panes.push(win);
|
|
189
189
|
return id;
|
|
190
190
|
}
|
|
191
191
|
close(id) {
|
|
192
|
-
this.
|
|
192
|
+
this.panes = this.panes.filter((w) => w.id !== id);
|
|
193
193
|
}
|
|
194
|
-
/** Make a
|
|
194
|
+
/** Make a pane the frontmost VISIBLE one: switch to its workspace,
|
|
195
195
|
* un-minimize it, and bring it to the front. Focus means reveal at every
|
|
196
196
|
* level — never a silent off-screen z bump. */
|
|
197
197
|
focus(id) {
|
|
@@ -222,7 +222,7 @@ export class WindowManager {
|
|
|
222
222
|
else
|
|
223
223
|
this.focus(id);
|
|
224
224
|
}
|
|
225
|
-
/** Fill the work area (middle-click on the
|
|
225
|
+
/** Fill the work area (middle-click on the pane's ×). Keeps the first
|
|
226
226
|
* pre-maximize/pre-snap frame so restore returns to the true origin. */
|
|
227
227
|
maximize(id) {
|
|
228
228
|
const w = this.#find(id);
|
|
@@ -258,8 +258,8 @@ export class WindowManager {
|
|
|
258
258
|
w.state = 'normal';
|
|
259
259
|
this.focus(id);
|
|
260
260
|
}
|
|
261
|
-
/** Re-fit
|
|
262
|
-
*
|
|
261
|
+
/** Re-fit panes after the desktop or its chrome resizes: maximized
|
|
262
|
+
* panes re-fill the work area; normal panes re-clamp so the bar stays
|
|
263
263
|
* below the top and at least dragKeep px stays reachable on every other
|
|
264
264
|
* side — the same fence the drag containment enforces, applied at rest.
|
|
265
265
|
* The Desktop calls this whenever the work area changes; chrome itself
|
|
@@ -268,7 +268,7 @@ export class WindowManager {
|
|
|
268
268
|
const { x, y, width, height } = this.workArea;
|
|
269
269
|
if (!width || !height)
|
|
270
270
|
return;
|
|
271
|
-
for (const w of this.
|
|
271
|
+
for (const w of this.panes) {
|
|
272
272
|
if (w.state === 'maximized') {
|
|
273
273
|
w.pos = { x, y };
|
|
274
274
|
w.size = { width, height };
|
|
@@ -288,7 +288,7 @@ export class WindowManager {
|
|
|
288
288
|
/**
|
|
289
289
|
* The whole desktop as one JSON-safe document.
|
|
290
290
|
*
|
|
291
|
-
* Reads every workspace and every
|
|
291
|
+
* Reads every workspace and every pane across ALL of them, not just the
|
|
292
292
|
* active one — a snapshot that only captured what was on screen would quietly
|
|
293
293
|
* drop the other workspaces the moment it was restored.
|
|
294
294
|
*/
|
|
@@ -298,7 +298,7 @@ export class WindowManager {
|
|
|
298
298
|
savedAt: Date.now(),
|
|
299
299
|
activeWorkspaceId: this.activeWorkspaceId,
|
|
300
300
|
workspaces: this.workspaces.map((w) => ({ ...w })),
|
|
301
|
-
|
|
301
|
+
panes: this.panes.map((w) => {
|
|
302
302
|
const app = this.apps[w.appId];
|
|
303
303
|
const snap = {
|
|
304
304
|
appId: w.appId,
|
|
@@ -312,7 +312,7 @@ export class WindowManager {
|
|
|
312
312
|
};
|
|
313
313
|
// Only record a title the user changed. Storing the app's own default
|
|
314
314
|
// would pin it, so renaming the app in code would not reach any
|
|
315
|
-
//
|
|
315
|
+
// pane restored from an older snapshot.
|
|
316
316
|
if (app && w.title !== app.title)
|
|
317
317
|
snap.title = w.title;
|
|
318
318
|
if (w.props !== undefined)
|
|
@@ -325,15 +325,15 @@ export class WindowManager {
|
|
|
325
325
|
* Replace the desktop with a snapshot.
|
|
326
326
|
*
|
|
327
327
|
* Named applySnapshot, not restore: `restore(id)` already means "un-maximize
|
|
328
|
-
* this
|
|
328
|
+
* this pane", and one verb cannot mean both.
|
|
329
329
|
*
|
|
330
330
|
* Windows whose `appId` is no longer registered are SKIPPED rather than
|
|
331
331
|
* failing the whole restore: an app can be removed between export and import,
|
|
332
|
-
* and losing one
|
|
332
|
+
* and losing one pane beats losing the layout. Same for a pane pointing at
|
|
333
333
|
* a workspace the snapshot does not contain.
|
|
334
334
|
*/
|
|
335
335
|
applySnapshot(snapshot) {
|
|
336
|
-
this.
|
|
336
|
+
this.panes = [];
|
|
337
337
|
this.workspaces = snapshot.workspaces.map((w) => ({ ...w }));
|
|
338
338
|
if (this.workspaces.length === 0)
|
|
339
339
|
this.workspaces = [{ id: 'w1', name: '1' }];
|
|
@@ -344,11 +344,11 @@ export class WindowManager {
|
|
|
344
344
|
? snapshot.activeWorkspaceId
|
|
345
345
|
: this.workspaces[0].id;
|
|
346
346
|
let z = 0;
|
|
347
|
-
for (const s of snapshot.
|
|
347
|
+
for (const s of snapshot.panes) {
|
|
348
348
|
const app = this.apps[s.appId];
|
|
349
349
|
if (!app || !known.includes(s.workspaceId))
|
|
350
350
|
continue;
|
|
351
|
-
this.
|
|
351
|
+
this.panes.push({
|
|
352
352
|
id: `${s.appId}#${++this.#seq}`,
|
|
353
353
|
appId: s.appId,
|
|
354
354
|
workspaceId: s.workspaceId,
|
|
@@ -368,7 +368,7 @@ export class WindowManager {
|
|
|
368
368
|
});
|
|
369
369
|
}
|
|
370
370
|
this.#z = z;
|
|
371
|
-
this.focusedId = this.
|
|
371
|
+
this.focusedId = this.activePanes.at(-1)?.id ?? null;
|
|
372
372
|
}
|
|
373
373
|
/** Half / quadrant tiling against the work area. */
|
|
374
374
|
snap(id, zone) {
|
|
@@ -411,7 +411,7 @@ export class WindowManager {
|
|
|
411
411
|
if (this.workspaces.some((ws) => ws.id === id))
|
|
412
412
|
this.activeWorkspaceId = id;
|
|
413
413
|
}
|
|
414
|
-
/** Remove a workspace. Its
|
|
414
|
+
/** Remove a workspace. Its panes migrate to the fallback (the active
|
|
415
415
|
* workspace, or the first remaining one) — closing a desktop never
|
|
416
416
|
* silently closes the work on it. The last workspace can't be removed. */
|
|
417
417
|
removeWorkspace(id) {
|
|
@@ -420,12 +420,12 @@ export class WindowManager {
|
|
|
420
420
|
this.workspaces = this.workspaces.filter((ws) => ws.id !== id);
|
|
421
421
|
if (this.activeWorkspaceId === id)
|
|
422
422
|
this.activeWorkspaceId = this.workspaces[0].id;
|
|
423
|
-
for (const w of this.
|
|
423
|
+
for (const w of this.panes) {
|
|
424
424
|
if (w.workspaceId === id)
|
|
425
425
|
w.workspaceId = this.activeWorkspaceId;
|
|
426
426
|
}
|
|
427
427
|
}
|
|
428
|
-
/** Send a
|
|
428
|
+
/** Send a pane to another workspace (it stays where it was on screen). */
|
|
429
429
|
moveToWorkspace(windowId, workspaceId) {
|
|
430
430
|
const w = this.#find(windowId);
|
|
431
431
|
if (!w || !this.workspaces.some((ws) => ws.id === workspaceId))
|
|
@@ -433,14 +433,14 @@ export class WindowManager {
|
|
|
433
433
|
w.workspaceId = workspaceId;
|
|
434
434
|
}
|
|
435
435
|
}
|
|
436
|
-
const
|
|
437
|
-
export function
|
|
438
|
-
setContext(
|
|
439
|
-
return
|
|
436
|
+
const PM_KEY = Symbol('era-pm');
|
|
437
|
+
export function setPaneManagerContext(pm) {
|
|
438
|
+
setContext(PM_KEY, pm);
|
|
439
|
+
return pm;
|
|
440
440
|
}
|
|
441
|
-
export function
|
|
442
|
-
const
|
|
443
|
-
if (!
|
|
444
|
-
throw new Error('
|
|
445
|
-
return
|
|
441
|
+
export function getPaneManager() {
|
|
442
|
+
const pm = getContext(PM_KEY);
|
|
443
|
+
if (!pm)
|
|
444
|
+
throw new Error('getPaneManager() must be used inside a <Desktop>.');
|
|
445
|
+
return pm;
|
|
446
446
|
}
|
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
import X from '@lucide/svelte/icons/x';
|
|
7
7
|
import type { Plugin } from '@neodrag/svelte';
|
|
8
8
|
import { cn } from '../utils/index.js';
|
|
9
|
-
import {
|
|
9
|
+
import { getPaneManager, type AppPane } from './pane-manager.svelte.js';
|
|
10
10
|
|
|
11
|
-
let {
|
|
11
|
+
let { pane }: { pane: AppPane } = $props();
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
const focused = $derived(
|
|
13
|
+
const pm = getPaneManager();
|
|
14
|
+
const focused = $derived(pm.focusedId === pane.id);
|
|
15
15
|
|
|
16
|
-
// Shell containment: a
|
|
16
|
+
// Shell containment: a pane can never become unrecoverable. The bar may
|
|
17
17
|
// not leave the TOP of the work area at all (y ≥ workArea.y), and on every
|
|
18
|
-
// other side at least
|
|
18
|
+
// other side at least pm.dragKeep px of the pane must stay inside the
|
|
19
19
|
// work area (which excludes chrome — a strip hidden behind the taskbar
|
|
20
20
|
// would be just as lost). Clamped mid-drag the way neodrag's own bounds
|
|
21
21
|
// plugin does it: offset + proposed delta → clamp → re-propose the delta,
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
const shellClamp: Plugin = {
|
|
27
27
|
name: 'era:shell-clamp',
|
|
28
28
|
drag(ctx) {
|
|
29
|
-
const area =
|
|
29
|
+
const area = pm.workArea;
|
|
30
30
|
if (!area.width || !area.height) return;
|
|
31
|
-
const keep =
|
|
31
|
+
const keep = pm.dragKeep;
|
|
32
32
|
const width = (ctx.rootNode as HTMLElement).offsetWidth;
|
|
33
33
|
const px = ctx.offset.x + (ctx.proposed.x ?? 0);
|
|
34
34
|
const py = ctx.offset.y + (ctx.proposed.y ?? 0);
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
ctx.propose(cx - ctx.offset.x, cy - ctx.offset.y);
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
|
-
const
|
|
40
|
+
const panePlugins = [shellClamp];
|
|
41
41
|
|
|
42
|
-
// The
|
|
42
|
+
// The pane's single control: left-click CLOSES, right-click MINIMIZES (the
|
|
43
43
|
// taskbar restores it), middle-click toggles MAXIMIZE — no dedicated buttons.
|
|
44
44
|
// While a non-primary button is held the glyph previews its action (minus /
|
|
45
45
|
// square); a left press keeps the ×, so holding never implies another action
|
|
@@ -53,43 +53,44 @@
|
|
|
53
53
|
// clobber that on every reorder. A directive-style single-property write is safe.
|
|
54
54
|
let ref = $state<HTMLDivElement | null>(null);
|
|
55
55
|
$effect(() => {
|
|
56
|
-
if (ref) ref.style.zIndex = String(
|
|
56
|
+
if (ref) ref.style.zIndex = String(pane.z);
|
|
57
57
|
});
|
|
58
58
|
</script>
|
|
59
59
|
|
|
60
60
|
<Pane.Root
|
|
61
61
|
bind:ref
|
|
62
|
-
data-
|
|
63
|
-
bind:position={
|
|
64
|
-
bind:size={
|
|
65
|
-
resizable={
|
|
66
|
-
plugins={
|
|
67
|
-
onpointerdowncapture={() =>
|
|
62
|
+
data-pane
|
|
63
|
+
bind:position={pane.pos}
|
|
64
|
+
bind:size={pane.size}
|
|
65
|
+
resizable={pane.resizable}
|
|
66
|
+
plugins={panePlugins}
|
|
67
|
+
onpointerdowncapture={() => pm.focus(pane.id)}
|
|
68
68
|
class={cn(
|
|
69
69
|
'transition-[box-shadow,opacity]',
|
|
70
|
-
// Focused
|
|
70
|
+
// Focused pane reads forward; the rest recede (dimmer, flatter).
|
|
71
71
|
focused ? 'outline -outline-offset-1 outline-primary/70' : 'opacity-95 shadow-(--era-shadow)',
|
|
72
|
-
|
|
72
|
+
pane.state === 'minimized' && 'hidden'
|
|
73
73
|
)}
|
|
74
74
|
>
|
|
75
75
|
<Pane.Handle class="gap-(--era-gap)">
|
|
76
|
-
{#if
|
|
77
|
-
{@const Icon =
|
|
76
|
+
{#if pane.icon}
|
|
77
|
+
{@const Icon = pane.icon}
|
|
78
78
|
<Icon class="size-(--era-h-xs) shrink-0 text-muted" />
|
|
79
79
|
{/if}
|
|
80
|
-
<span class={cn('truncate text-body', focused ? 'text-bright' : 'text-muted')}
|
|
80
|
+
<span class={cn('truncate text-body', focused ? 'text-bright' : 'text-muted')}
|
|
81
|
+
>{pane.title}</span
|
|
81
82
|
>
|
|
82
83
|
<!-- Close / minimize / maximize. Deliberately NOT data-pane-control (same as
|
|
83
|
-
the raw Pane.Close): a left press-and-HOLD drags the
|
|
84
|
+
the raw Pane.Close): a left press-and-HOLD drags the pane — neodrag only
|
|
84
85
|
drags the primary button and suppresses the trailing click after a real
|
|
85
86
|
drag, so a clean click still closes, and right/middle arming is a
|
|
86
87
|
different button entirely. -->
|
|
87
|
-
<!--
|
|
88
|
+
<!-- Pane actions sit LEFT of the close button, on the same --era-gap
|
|
88
89
|
rhythm as the rest of the handle. Like the close button they carry no
|
|
89
90
|
data-pane-control, so a press-and-hold anywhere on the bar still drags
|
|
90
|
-
the
|
|
91
|
+
the pane and only a clean click activates them. -->
|
|
91
92
|
<div class="ml-auto flex shrink-0 items-center gap-(--era-gap)">
|
|
92
|
-
{@render
|
|
93
|
+
{@render pane.actions?.()}
|
|
93
94
|
<!-- The macOS traffic lights, one button: red = close at rest, and the
|
|
94
95
|
held preview recolours to the action it will commit — yellow for
|
|
95
96
|
minimize, green for maximize — so the tone and glyph agree on what
|
|
@@ -116,18 +117,18 @@
|
|
|
116
117
|
armed === 'maximize' ? '[&_svg]:size-(--era-icon-xs)' : '[&_svg]:size-(--era-icon-xxs)'
|
|
117
118
|
)}
|
|
118
119
|
aria-label="Close (right-click minimizes, middle-click maximizes)"
|
|
119
|
-
onclick={() =>
|
|
120
|
+
onclick={() => pm.close(pane.id)}
|
|
120
121
|
oncontextmenu={(e: MouseEvent) => e.preventDefault()}
|
|
121
122
|
onpointerdown={(e: PointerEvent) => {
|
|
122
123
|
// Cancelling the middle pointerdown suppresses its compatibility
|
|
123
124
|
// mouse events, so the browser's autoscroll never engages.
|
|
124
125
|
if (e.button === 1) e.preventDefault();
|
|
125
126
|
armed =
|
|
126
|
-
e.button === 2 &&
|
|
127
|
+
e.button === 2 && pane.minimizable ? 'minimize' : e.button === 1 ? 'maximize' : null;
|
|
127
128
|
}}
|
|
128
129
|
onpointerup={(e: PointerEvent) => {
|
|
129
|
-
if (e.button === 2 && armed === 'minimize')
|
|
130
|
-
else if (e.button === 1 && armed === 'maximize')
|
|
130
|
+
if (e.button === 2 && armed === 'minimize') pm.minimize(pane.id);
|
|
131
|
+
else if (e.button === 1 && armed === 'maximize') pm.toggleMaximize(pane.id);
|
|
131
132
|
armed = null;
|
|
132
133
|
}}
|
|
133
134
|
onpointerleave={() => (armed = null)}
|
|
@@ -144,10 +145,10 @@
|
|
|
144
145
|
</div>
|
|
145
146
|
</Pane.Handle>
|
|
146
147
|
|
|
147
|
-
<Pane.Content class={
|
|
148
|
-
{#if
|
|
149
|
-
{@const Body =
|
|
150
|
-
<Body
|
|
148
|
+
<Pane.Content class={pane.bodyClass}>
|
|
149
|
+
{#if pane.component}
|
|
150
|
+
{@const Body = pane.component}
|
|
151
|
+
<Body {pane} {pm} />
|
|
151
152
|
{/if}
|
|
152
153
|
</Pane.Content>
|
|
153
154
|
</Pane.Root>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as Pane from '../ui/pane';
|
|
2
|
+
import { type AppPane } from './pane-manager.svelte.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
pane: AppPane;
|
|
5
|
+
};
|
|
6
|
+
declare const Pane: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type Pane = ReturnType<typeof Pane>;
|
|
8
|
+
export default Pane;
|
package/dist/os/taskbar.svelte
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import Bell from '@lucide/svelte/icons/bell';
|
|
10
10
|
import Terminal from '@lucide/svelte/icons/terminal';
|
|
11
11
|
import { cn } from '../utils/index.js';
|
|
12
|
-
import {
|
|
12
|
+
import { getPaneManager } from './pane-manager.svelte.js';
|
|
13
13
|
import { getNotifications } from './notifications.svelte.js';
|
|
14
14
|
import { LAYER } from './layers.js';
|
|
15
15
|
|
|
@@ -36,16 +36,16 @@
|
|
|
36
36
|
position?: 'top' | 'bottom';
|
|
37
37
|
/** Float the bar on the desktop with a uniform --era-gap gutter (fully
|
|
38
38
|
* rounded, borderless — a Bar tile) instead of running flush edge to
|
|
39
|
-
* edge. The registered inset covers gutter + bar + gutter, so
|
|
39
|
+
* edge. The registered inset covers gutter + bar + gutter, so panes
|
|
40
40
|
* keep the same even gap to the bar that the bar keeps to the shell. */
|
|
41
41
|
floating?: boolean;
|
|
42
42
|
/** Pinned launchers — click spawns that app even when it isn't running. */
|
|
43
43
|
pinned?: { appId: string; title: string; icon?: Component<IconProps> | null }[];
|
|
44
|
-
/** Show the open/minimized
|
|
45
|
-
* stay reachable through the command bar's `
|
|
44
|
+
/** Show the open/minimized pane tabs (default true). Hidden, panes
|
|
45
|
+
* stay reachable through the command bar's `panes` view. */
|
|
46
46
|
showWindows?: boolean;
|
|
47
|
-
/** Include the built-in `
|
|
48
|
-
* true) — the palette view that focuses/restores an open
|
|
47
|
+
/** Include the built-in `panes` command in the command bar (default
|
|
48
|
+
* true) — the palette view that focuses/restores an open pane. Distinct
|
|
49
49
|
* from `showWindows`, which gates the tab strip and deliberately keeps
|
|
50
50
|
* this view as the tabs' recovery path. Set false alongside
|
|
51
51
|
* `showWorkspaces={false}` for a single-purpose shell whose palette shows
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
showWindowsCommand?: boolean;
|
|
54
54
|
/** Show the multi-workspace UI (default true). Hidden, both the numbered
|
|
55
55
|
* switcher and the command bar's `workspace` view go away — a
|
|
56
|
-
* single-workspace shell. `
|
|
56
|
+
* single-workspace shell. `pm.addWorkspace()` still works for consumers
|
|
57
57
|
* who drive workspaces themselves. */
|
|
58
58
|
showWorkspaces?: boolean;
|
|
59
59
|
/** Extra root commands appended to the built-in ones. */
|
|
@@ -78,11 +78,11 @@
|
|
|
78
78
|
end?: Snippet;
|
|
79
79
|
} = $props();
|
|
80
80
|
|
|
81
|
-
const
|
|
81
|
+
const pm = getPaneManager();
|
|
82
82
|
const notif = getNotifications();
|
|
83
83
|
|
|
84
84
|
// Register our reserved chrome on whichever edge we dock to: maximize stops
|
|
85
|
-
// at the bar, and drag containment keeps
|
|
85
|
+
// at the bar, and drag containment keeps pane bars reachable beside it
|
|
86
86
|
// (not hidden behind). Floating bars reserve their gutter on BOTH sides —
|
|
87
87
|
// measured from the rendered offset, so it tracks the live --era-gap value
|
|
88
88
|
// across density changes (barHeight moves with it, re-running this).
|
|
@@ -99,43 +99,43 @@
|
|
|
99
99
|
: (host?.clientHeight ?? 0) - ref.offsetTop - ref.offsetHeight;
|
|
100
100
|
inset = barHeight + Math.max(0, gutter) * 2;
|
|
101
101
|
}
|
|
102
|
-
|
|
102
|
+
pm.insets[edge] = inset;
|
|
103
103
|
return () => {
|
|
104
|
-
|
|
104
|
+
pm.insets[edge] = 0;
|
|
105
105
|
};
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
// The command bar owns everything the old spotlight overlay did, and more:
|
|
109
|
-
// every registered app launches from the root view, `
|
|
110
|
-
// the open
|
|
109
|
+
// every registered app launches from the root view, `panes` drills into
|
|
110
|
+
// the open panes (focus/restore — the recovery path when the pane tabs
|
|
111
111
|
// are hidden), and `workspace` drills into numbered workspaces + `new`.
|
|
112
112
|
// Consumer `commands` append after the built-ins (merged in a thin outer
|
|
113
|
-
// derived, so a consumer-commands change doesn't rebuild the
|
|
113
|
+
// derived, so a consumer-commands change doesn't rebuild the pm closures
|
|
114
114
|
// and vice-versa).
|
|
115
115
|
const builtinItems = $derived.by((): CommandBarItem[] => {
|
|
116
|
-
const items: CommandBarItem[] = Object.values(
|
|
116
|
+
const items: CommandBarItem[] = Object.values(pm.apps).map((app) => ({
|
|
117
117
|
value: `app:${app.id}`,
|
|
118
118
|
label: app.title,
|
|
119
119
|
icon: app.icon ?? undefined,
|
|
120
120
|
keywords: [app.id],
|
|
121
121
|
sublabel: 'launch',
|
|
122
|
-
onSelect: () => void
|
|
122
|
+
onSelect: () => void pm.open(app.id)
|
|
123
123
|
}));
|
|
124
124
|
// Each built-in view is assembled only when shown, so a hidden view costs
|
|
125
|
-
// nothing and takes no reactive dependency on its
|
|
125
|
+
// nothing and takes no reactive dependency on its pm slice.
|
|
126
126
|
if (showWindowsCommand) {
|
|
127
127
|
items.push({
|
|
128
|
-
value: '
|
|
129
|
-
label: '
|
|
128
|
+
value: 'panes',
|
|
129
|
+
label: 'panes',
|
|
130
130
|
icon: AppWindowIcon,
|
|
131
|
-
sublabel: 'focus an open
|
|
132
|
-
placeholder: 'which
|
|
133
|
-
items:
|
|
131
|
+
sublabel: 'focus an open pane',
|
|
132
|
+
placeholder: 'which pane?',
|
|
133
|
+
items: pm.panes.map((w) => ({
|
|
134
134
|
value: `win:${w.id}`,
|
|
135
135
|
label: w.title,
|
|
136
136
|
icon: w.icon ?? undefined,
|
|
137
137
|
sublabel: w.state === 'minimized' ? 'minimized' : undefined,
|
|
138
|
-
onSelect: () =>
|
|
138
|
+
onSelect: () => pm.focus(w.id)
|
|
139
139
|
}))
|
|
140
140
|
});
|
|
141
141
|
}
|
|
@@ -147,17 +147,17 @@
|
|
|
147
147
|
sublabel: 'switch / create',
|
|
148
148
|
placeholder: 'which workspace?',
|
|
149
149
|
items: [
|
|
150
|
-
...
|
|
150
|
+
...pm.workspaces.map((ws, i) => ({
|
|
151
151
|
value: `ws:${ws.id}`,
|
|
152
152
|
label: `${i + 1} ${ws.name}`,
|
|
153
153
|
keywords: [String(i + 1)],
|
|
154
|
-
onSelect: () =>
|
|
154
|
+
onSelect: () => pm.switchWorkspace(ws.id)
|
|
155
155
|
})),
|
|
156
156
|
{
|
|
157
157
|
value: 'ws:new',
|
|
158
158
|
label: 'new',
|
|
159
159
|
sublabel: 'create a workspace',
|
|
160
|
-
onSelect: () => void
|
|
160
|
+
onSelect: () => void pm.addWorkspace()
|
|
161
161
|
}
|
|
162
162
|
]
|
|
163
163
|
});
|
|
@@ -212,27 +212,27 @@
|
|
|
212
212
|
{#if pinned.length}
|
|
213
213
|
<div class="flex shrink-0 items-center gap-(--era-gap)">
|
|
214
214
|
{#each pinned as p (p.appId)}
|
|
215
|
-
<Button icon size="default" aria-label={p.title} onclick={() =>
|
|
215
|
+
<Button icon size="default" aria-label={p.title} onclick={() => pm.open(p.appId)}>
|
|
216
216
|
{#if p.icon}{@const Icon = p.icon}<Icon class="size-(--era-h-xs)" />{/if}
|
|
217
217
|
</Button>
|
|
218
218
|
{/each}
|
|
219
219
|
</div>
|
|
220
220
|
{/if}
|
|
221
221
|
|
|
222
|
-
<!-- Running
|
|
223
|
-
the focused
|
|
222
|
+
<!-- Running panes on the active workspace: real Buttons — `active` latches
|
|
223
|
+
the focused pane's pressed face (surface-aware for free), click
|
|
224
224
|
focuses / minimises. Optional: hide them and use the command bar's
|
|
225
|
-
`
|
|
225
|
+
`panes` view instead. -->
|
|
226
226
|
{#if showWindows}
|
|
227
227
|
<div
|
|
228
228
|
class="scrollbar-none flex max-w-[40%] min-w-0 shrink-0 items-center gap-(--era-gap) overflow-x-auto"
|
|
229
229
|
>
|
|
230
|
-
{#each
|
|
230
|
+
{#each pm.activePanes as win (win.id)}
|
|
231
231
|
<Button
|
|
232
|
-
active={
|
|
233
|
-
onclick={() =>
|
|
232
|
+
active={pm.focusedId === win.id && win.state !== 'minimized'}
|
|
233
|
+
onclick={() => pm.toggleMinimize(win.id)}
|
|
234
234
|
onauxclick={(e: MouseEvent) => {
|
|
235
|
-
if (e.button === 1)
|
|
235
|
+
if (e.button === 1) pm.close(win.id);
|
|
236
236
|
}}
|
|
237
237
|
onpointerdown={(e: PointerEvent) => {
|
|
238
238
|
// Cancel middle pointerdown so autoscroll never engages.
|
|
@@ -250,16 +250,16 @@
|
|
|
250
250
|
<!-- Numbered workspace switcher, term-style on the right — appears only
|
|
251
251
|
once there's something to switch between (and only when the bar exposes
|
|
252
252
|
workspaces at all). -->
|
|
253
|
-
{#if showWorkspaces &&
|
|
253
|
+
{#if showWorkspaces && pm.workspaces.length > 1}
|
|
254
254
|
<div class="flex shrink-0 items-center gap-(--era-gap)">
|
|
255
|
-
{#each
|
|
255
|
+
{#each pm.workspaces as ws, i (ws.id)}
|
|
256
256
|
<Button
|
|
257
257
|
icon
|
|
258
258
|
size="default"
|
|
259
|
-
active={
|
|
259
|
+
active={pm.activeWorkspaceId === ws.id}
|
|
260
260
|
title={ws.name}
|
|
261
261
|
aria-label="Switch to workspace {i + 1} ({ws.name})"
|
|
262
|
-
onclick={() =>
|
|
262
|
+
onclick={() => pm.switchWorkspace(ws.id)}
|
|
263
263
|
>
|
|
264
264
|
{i + 1}
|
|
265
265
|
</Button>
|
|
@@ -8,7 +8,7 @@ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
|
|
|
8
8
|
position?: 'top' | 'bottom';
|
|
9
9
|
/** Float the bar on the desktop with a uniform --era-gap gutter (fully
|
|
10
10
|
* rounded, borderless — a Bar tile) instead of running flush edge to
|
|
11
|
-
* edge. The registered inset covers gutter + bar + gutter, so
|
|
11
|
+
* edge. The registered inset covers gutter + bar + gutter, so panes
|
|
12
12
|
* keep the same even gap to the bar that the bar keeps to the shell. */
|
|
13
13
|
floating?: boolean;
|
|
14
14
|
/** Pinned launchers — click spawns that app even when it isn't running. */
|
|
@@ -17,11 +17,11 @@ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
|
|
|
17
17
|
title: string;
|
|
18
18
|
icon?: Component<IconProps> | null;
|
|
19
19
|
}[];
|
|
20
|
-
/** Show the open/minimized
|
|
21
|
-
* stay reachable through the command bar's `
|
|
20
|
+
/** Show the open/minimized pane tabs (default true). Hidden, panes
|
|
21
|
+
* stay reachable through the command bar's `panes` view. */
|
|
22
22
|
showWindows?: boolean;
|
|
23
|
-
/** Include the built-in `
|
|
24
|
-
* true) — the palette view that focuses/restores an open
|
|
23
|
+
/** Include the built-in `panes` command in the command bar (default
|
|
24
|
+
* true) — the palette view that focuses/restores an open pane. Distinct
|
|
25
25
|
* from `showWindows`, which gates the tab strip and deliberately keeps
|
|
26
26
|
* this view as the tabs' recovery path. Set false alongside
|
|
27
27
|
* `showWorkspaces={false}` for a single-purpose shell whose palette shows
|
|
@@ -29,7 +29,7 @@ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
|
|
|
29
29
|
showWindowsCommand?: boolean;
|
|
30
30
|
/** Show the multi-workspace UI (default true). Hidden, both the numbered
|
|
31
31
|
* switcher and the command bar's `workspace` view go away — a
|
|
32
|
-
* single-workspace shell. `
|
|
32
|
+
* single-workspace shell. `pm.addWorkspace()` still works for consumers
|
|
33
33
|
* who drive workspaces themselves. */
|
|
34
34
|
showWorkspaces?: boolean;
|
|
35
35
|
/** Extra root commands appended to the built-in ones. */
|