anentrypoint-design 0.0.385 → 0.0.387
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/247420.css +33 -0
- package/dist/247420.js +53 -51
- package/package.json +1 -1
- package/src/components/agent-chat/controls.js +143 -0
- package/src/components/agent-chat/empty-state.js +63 -0
- package/src/components/agent-chat/message-rows.js +141 -0
- package/src/components/agent-chat/surface.js +206 -0
- package/src/components/agent-chat/thread-behaviour.js +50 -0
- package/src/components/agent-chat.js +6 -546
- package/src/components/chat/composer-affordances.js +109 -0
- package/src/components/chat/composer.js +256 -0
- package/src/components/chat/threads.js +105 -0
- package/src/components/chat-message-parts/agent-nodes.js +103 -0
- package/src/components/chat-message-parts/inline.js +106 -0
- package/src/components/chat-message-parts/prose-nodes.js +133 -0
- package/src/components/chat-message-parts/renderers.js +89 -0
- package/src/components/chat-message-parts.js +12 -408
- package/src/components/chat-minimap/minimap.js +167 -0
- package/src/components/chat-minimap/paint.js +73 -0
- package/src/components/chat-minimap/preview.js +41 -0
- package/src/components/chat-minimap.js +7 -263
- package/src/components/chat.js +20 -628
- package/src/components/community/chrome.js +63 -0
- package/src/components/community/navigation.js +167 -0
- package/src/components/community/presence.js +106 -0
- package/src/components/community/shell.js +17 -0
- package/src/components/community/views.js +131 -0
- package/src/components/community.js +18 -447
- package/src/components/files/chrome.js +140 -0
- package/src/components/files/entries.js +156 -0
- package/src/components/files/grid-controls.js +63 -0
- package/src/components/files/grid.js +177 -0
- package/src/components/files/types.js +69 -0
- package/src/components/files-modals/dialogs.js +118 -0
- package/src/components/files-modals/modal-shell.js +153 -0
- package/src/components/files-modals/preview-bodies.js +129 -0
- package/src/components/files-modals/preview-containers.js +96 -0
- package/src/components/files-modals.js +14 -475
- package/src/components/files.js +15 -564
- package/src/components/freddie/pages-config.js +3 -94
- package/src/components/freddie/pages-models.js +97 -0
- package/src/components/freddie.js +2 -1
- package/src/components/interaction-primitives/mobile.js +25 -0
- package/src/components/interaction-primitives/pointer.js +214 -0
- package/src/components/interaction-primitives/reorderable.js +47 -0
- package/src/components/interaction-primitives/shortcuts.js +119 -0
- package/src/components/interaction-primitives.js +16 -388
- package/src/components/sessions/conversation-list.js +230 -0
- package/src/components/sessions/dashboard.js +202 -0
- package/src/components/sessions/detail-bits.js +49 -0
- package/src/components/sessions/format.js +42 -0
- package/src/components/sessions/session-card.js +92 -0
- package/src/components/sessions.js +16 -579
- package/src/components/voice/audio-cue.js +39 -0
- package/src/components/voice/capture.js +90 -0
- package/src/components/voice/playback.js +75 -0
- package/src/components/voice/settings-modal.js +104 -0
- package/src/components/voice.js +16 -293
- package/src/css/app-shell/base.css +23 -0
- package/src/css/app-shell/states-interactions.css +10 -0
- package/src/kits/os/freddie/chat-protocol.js +32 -0
- package/src/kits/os/freddie/chat-transport.js +178 -0
- package/src/kits/os/freddie/pages-chat.js +10 -180
- package/src/kits/os/shell-chrome.js +163 -0
- package/src/kits/os/shell-geometry.js +59 -0
- package/src/kits/os/shell.js +178 -335
- package/src/page-html/client-script.js +151 -0
- package/src/page-html/head-tags.js +59 -0
- package/src/page-html/markdown.js +68 -0
- package/src/page-html/page-styles.js +44 -0
- package/src/page-html.js +14 -291
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Window spawn/placement geometry for the desktop shell: how big a new window
|
|
2
|
+
// should be for the current desktop area, where it cascades to, and how open
|
|
3
|
+
// windows are pulled back into range when the viewport shrinks.
|
|
4
|
+
|
|
5
|
+
// Small-viewport threshold: below this, a floating window (with drag/resize
|
|
6
|
+
// chrome meant for a pointer+large-canvas paradigm) is awkward — spawn apps
|
|
7
|
+
// maximized instead of as a small floating rect. Matches the coarse-pointer
|
|
8
|
+
// tablet/phone class, not just narrow desktop windows.
|
|
9
|
+
export const SMALL_VIEWPORT_W = 768;
|
|
10
|
+
|
|
11
|
+
// Scale a fixed-px default spawn size against the actual desktop area so a
|
|
12
|
+
// 4K/ultrawide viewport doesn't cage every window at the same handful of
|
|
13
|
+
// pixels: the requested size is nudged toward a fraction of the available
|
|
14
|
+
// area, clamped between the app's own default (floor) and a generous
|
|
15
|
+
// multiple of it (ceiling) so windows still cascade/overlap sensibly instead
|
|
16
|
+
// of each spawning full-bleed.
|
|
17
|
+
export function scaleSpawnSize(sz, vw, vh) {
|
|
18
|
+
const targetW = Math.round(vw * 0.42);
|
|
19
|
+
const targetH = Math.round(vh * 0.52);
|
|
20
|
+
const w = Math.max(sz.w, Math.min(targetW, sz.w * 2, vw));
|
|
21
|
+
const h = Math.max(sz.h, Math.min(targetH, sz.h * 2, vh));
|
|
22
|
+
return { w, h };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Resolve the full spawn rect for a new window. Clamp spawn bounds to the
|
|
26
|
+
// desktop area so cascaded windows never open with their chrome (titlebar/
|
|
27
|
+
// resize grip) off-screen. Below the tablet breakpoint a floating window is
|
|
28
|
+
// awkward (no room to drag/resize around it) — spawn maximized instead of a
|
|
29
|
+
// small floating rect. Above it, scale the default size toward the available
|
|
30
|
+
// desktop area so 4K/ultrawide viewports don't cage every window at the same
|
|
31
|
+
// fixed handful of pixels.
|
|
32
|
+
export function computeSpawnRect(sz, openCount) {
|
|
33
|
+
const host = document.querySelector('.wm-root');
|
|
34
|
+
const vw = host ? host.clientWidth : window.innerWidth;
|
|
35
|
+
const vh = host ? host.clientHeight : window.innerHeight;
|
|
36
|
+
const small = vw < SMALL_VIEWPORT_W;
|
|
37
|
+
const scaled = small ? sz : scaleSpawnSize(sz, vw, vh);
|
|
38
|
+
const w = Math.min(scaled.w, vw);
|
|
39
|
+
const h = Math.min(scaled.h, vh);
|
|
40
|
+
const x = Math.max(0, Math.min(100 + (openCount * 28) % 240, vw - w));
|
|
41
|
+
const y = Math.max(0, Math.min(80 + (openCount * 22) % 180, vh - h));
|
|
42
|
+
return { w, h, x, y, maximized: small };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Keep open windows reachable when the viewport shrinks (rotation, browser
|
|
46
|
+
// resize): pull any window whose titlebar left the desktop back in range.
|
|
47
|
+
export function reflowWindows(wm) {
|
|
48
|
+
for (const w of wm.list()) {
|
|
49
|
+
const el = document.querySelector('.wm-win[data-id="' + w.id + '"]');
|
|
50
|
+
if (!el) continue;
|
|
51
|
+
const p = el.offsetParent;
|
|
52
|
+
if (!p) continue;
|
|
53
|
+
const nx = Math.min(el.offsetLeft, Math.max(0, p.clientWidth - 60));
|
|
54
|
+
const ny = Math.min(el.offsetTop, Math.max(0, p.clientHeight - 36));
|
|
55
|
+
if (nx === el.offsetLeft && ny === el.offsetTop) continue;
|
|
56
|
+
if (typeof wm.setBounds === 'function') wm.setBounds(w.id, { x: nx, y: ny });
|
|
57
|
+
else if (w.handle && typeof w.handle.setBounds === 'function') w.handle.setBounds({ x: nx, y: ny });
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/kits/os/shell.js
CHANGED
|
@@ -1,335 +1,178 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
// only trigger today) so the user's keyboard position isn't lost.
|
|
180
|
-
let drawerReturnFocus = null;
|
|
181
|
-
function openDrawer() {
|
|
182
|
-
drawerReturnFocus = document.activeElement;
|
|
183
|
-
drawer.classList.add('open');
|
|
184
|
-
drawer.setAttribute('aria-hidden', 'false');
|
|
185
|
-
drawerClose.focus();
|
|
186
|
-
}
|
|
187
|
-
function closeDrawer() {
|
|
188
|
-
drawer.classList.remove('open');
|
|
189
|
-
drawer.setAttribute('aria-hidden', 'true');
|
|
190
|
-
if (drawerReturnFocus && typeof drawerReturnFocus.focus === 'function') drawerReturnFocus.focus();
|
|
191
|
-
drawerReturnFocus = null;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
appsBtn.addEventListener('click', e => { e.stopPropagation(); appsMenu.classList.contains('open') ? closeMenu() : openMenu(); });
|
|
195
|
-
homeBtn.addEventListener('click', e => { e.stopPropagation(); drawer.classList.contains('open') ? closeDrawer() : openDrawer(); });
|
|
196
|
-
drawerClose.addEventListener('click', closeDrawer);
|
|
197
|
-
drawer.addEventListener('click', e => { if (e.target === drawer) closeDrawer(); });
|
|
198
|
-
document.addEventListener('click', e => {
|
|
199
|
-
if (!appsMenu.contains(e.target) && !appsBtn.contains(e.target)) closeMenu();
|
|
200
|
-
});
|
|
201
|
-
document.addEventListener('keydown', e => {
|
|
202
|
-
if (e.key === 'Escape') { closeMenu(); closeDrawer(); }
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
function tickClock() { clock.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }
|
|
206
|
-
tickClock();
|
|
207
|
-
const clockTimer = setInterval(tickClock, 30000);
|
|
208
|
-
|
|
209
|
-
let activeContext = null;
|
|
210
|
-
let activeInstanceId = null;
|
|
211
|
-
function setContext(ctx) { activeContext = ctx; }
|
|
212
|
-
|
|
213
|
-
function applyInstanceFilter() {
|
|
214
|
-
if (!activeInstanceId) return;
|
|
215
|
-
for (const wEl of document.querySelectorAll('.wm-win[data-instance-id]')) {
|
|
216
|
-
const wInst = wEl.dataset.instanceId;
|
|
217
|
-
wEl.classList.toggle('wm-inst-hidden', wInst !== activeInstanceId);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
let taskbarSig = null;
|
|
222
|
-
function refreshTaskbar() {
|
|
223
|
-
const items = [];
|
|
224
|
-
for (const w of wm.list()) {
|
|
225
|
-
const wEl = document.querySelector('.wm-win[data-id="' + w.id + '"]');
|
|
226
|
-
const wInst = wEl && wEl.dataset.instanceId;
|
|
227
|
-
if (activeInstanceId && wInst && wInst !== activeInstanceId) continue;
|
|
228
|
-
items.push(w);
|
|
229
|
-
}
|
|
230
|
-
// Diff-aware rebuild: the 500ms poll must not reset the taskbar's
|
|
231
|
-
// horizontal scroll (mobile scroll-snap) or button focus every tick.
|
|
232
|
-
const sig = items.map(w => w.id + '' + w.title + '' + (w.focused ? 1 : 0)).join('');
|
|
233
|
-
if (sig === taskbarSig) return;
|
|
234
|
-
taskbarSig = sig;
|
|
235
|
-
const sl = taskbar.scrollLeft;
|
|
236
|
-
taskbar.innerHTML = '';
|
|
237
|
-
if (!items.length) {
|
|
238
|
-
const empty = document.createElement('span');
|
|
239
|
-
empty.className = 'os-task-empty';
|
|
240
|
-
empty.textContent = 'no windows';
|
|
241
|
-
taskbar.appendChild(empty);
|
|
242
|
-
}
|
|
243
|
-
for (const w of items) {
|
|
244
|
-
const t = document.createElement('button');
|
|
245
|
-
t.className = 'os-task' + (w.focused ? ' focused' : '');
|
|
246
|
-
t.type = 'button';
|
|
247
|
-
t.textContent = w.title;
|
|
248
|
-
t.dataset.winId = w.id;
|
|
249
|
-
// aria-current announces which window is the active one; a
|
|
250
|
-
// sighted user reads this from the .focused visual state alone.
|
|
251
|
-
if (w.focused) t.setAttribute('aria-current', 'true');
|
|
252
|
-
t.addEventListener('click', () => wm.focus(w.id));
|
|
253
|
-
taskbar.appendChild(t);
|
|
254
|
-
}
|
|
255
|
-
taskbar.scrollLeft = sl;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function setActiveInstance(id) {
|
|
259
|
-
activeInstanceId = id;
|
|
260
|
-
for (const btn of instSwitch.querySelectorAll('.os-btn')) {
|
|
261
|
-
btn.classList.toggle('active', btn.dataset.instanceId === id);
|
|
262
|
-
}
|
|
263
|
-
applyInstanceFilter();
|
|
264
|
-
refreshTaskbar();
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function openApp(appId) {
|
|
268
|
-
const app = (typeof registry.get === 'function') ? registry.get(appId) : registry[appId];
|
|
269
|
-
if (!app) throw new Error('unknown app: ' + appId);
|
|
270
|
-
const ctx = { ...(activeContext || {}), registry, openApp, wm };
|
|
271
|
-
const result = app.factory(ctx);
|
|
272
|
-
const finish = (r) => {
|
|
273
|
-
const sz = app.defaultSize || { w: 520, h: 360 };
|
|
274
|
-
// Clamp spawn bounds to the desktop area so cascaded windows never
|
|
275
|
-
// open with their chrome (titlebar/resize grip) off-screen.
|
|
276
|
-
const host = document.querySelector('.wm-root');
|
|
277
|
-
const vw = host ? host.clientWidth : window.innerWidth;
|
|
278
|
-
const vh = host ? host.clientHeight : window.innerHeight;
|
|
279
|
-
// Below the tablet breakpoint a floating window is awkward (no
|
|
280
|
-
// room to drag/resize around it) — spawn maximized instead of a
|
|
281
|
-
// small floating rect. Above it, scale the default size toward
|
|
282
|
-
// the available desktop area so 4K/ultrawide viewports don't
|
|
283
|
-
// cage every window at the same fixed handful of pixels.
|
|
284
|
-
const small = vw < SMALL_VIEWPORT_W;
|
|
285
|
-
const scaled = small ? sz : scaleSpawnSize(sz, vw, vh);
|
|
286
|
-
const w = Math.min(scaled.w, vw);
|
|
287
|
-
const h = Math.min(scaled.h, vh);
|
|
288
|
-
const x = Math.max(0, Math.min(100 + (wm.count * 28) % 240, vw - w));
|
|
289
|
-
const y = Math.max(0, Math.min(80 + (wm.count * 22) % 180, vh - h));
|
|
290
|
-
const titlePrefix = (activeContext && activeContext.titlePrefix) ? activeContext.titlePrefix + ' · ' : '';
|
|
291
|
-
const win = wm.open({ title: titlePrefix + app.name, body: r.node, kind: appId, width: w, height: h, x, y, maximized: small });
|
|
292
|
-
win._app = { id: appId, dispose: r.dispose };
|
|
293
|
-
if (activeInstanceId && win.el) {
|
|
294
|
-
win.el.dataset.instanceId = activeInstanceId;
|
|
295
|
-
win.instanceId = activeInstanceId;
|
|
296
|
-
}
|
|
297
|
-
win.appId = appId;
|
|
298
|
-
refreshTaskbar();
|
|
299
|
-
return win;
|
|
300
|
-
};
|
|
301
|
-
return (result && typeof result.then === 'function') ? result.then(finish) : finish(result);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (newInstBtn) newInstBtn.addEventListener('click', () => onNewInstance && onNewInstance({ instSwitch, setContext, openApp }));
|
|
305
|
-
|
|
306
|
-
const taskTimer = setInterval(refreshTaskbar, 500);
|
|
307
|
-
|
|
308
|
-
// Keep open windows reachable when the viewport shrinks (rotation, browser
|
|
309
|
-
// resize): pull any window whose titlebar left the desktop back in range.
|
|
310
|
-
const onViewportResize = () => {
|
|
311
|
-
for (const w of wm.list()) {
|
|
312
|
-
const el = document.querySelector('.wm-win[data-id="' + w.id + '"]');
|
|
313
|
-
if (!el) continue;
|
|
314
|
-
const p = el.offsetParent;
|
|
315
|
-
if (!p) continue;
|
|
316
|
-
const nx = Math.min(el.offsetLeft, Math.max(0, p.clientWidth - 60));
|
|
317
|
-
const ny = Math.min(el.offsetTop, Math.max(0, p.clientHeight - 36));
|
|
318
|
-
if (nx === el.offsetLeft && ny === el.offsetTop) continue;
|
|
319
|
-
if (typeof wm.setBounds === 'function') wm.setBounds(w.id, { x: nx, y: ny });
|
|
320
|
-
else if (w.handle && typeof w.handle.setBounds === 'function') w.handle.setBounds({ x: nx, y: ny });
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
window.addEventListener('resize', onViewportResize);
|
|
324
|
-
|
|
325
|
-
const api = {
|
|
326
|
-
wm, registry, openApp, setContext, refreshTaskbar, setActiveInstance,
|
|
327
|
-
openDrawer, closeDrawer, openMenu, closeMenu,
|
|
328
|
-
get activeInstanceId() { return activeInstanceId; },
|
|
329
|
-
elements: { osRoot, menubar, taskbar, appsMenu, sideRail, drawer, instSwitch, homeBtn, appsBtn },
|
|
330
|
-
dispose() { clearInterval(clockTimer); clearInterval(taskTimer); window.removeEventListener('resize', onViewportResize); osRoot.remove(); sideRail.remove(); drawer.remove(); },
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
if (autoBoot && typeof autoBoot === 'string') openApp(autoBoot);
|
|
334
|
-
return api;
|
|
335
|
-
}
|
|
1
|
+
// createDesktopShell — the desktop OS shell: assembles the menubar / apps
|
|
2
|
+
// menu / side rail / drawer / taskbar chrome (built by ./shell-chrome.js),
|
|
3
|
+
// owns the shared mutable state those surfaces read (active context, active
|
|
4
|
+
// instance, taskbar signature, drawer focus return), and drives window
|
|
5
|
+
// spawning through ./shell-geometry.js.
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ensureCss, buildMenubar, buildAppsMenu, buildSideRail, buildDrawer,
|
|
9
|
+
buildTaskbar, buildAppEntries,
|
|
10
|
+
} from './shell-chrome.js';
|
|
11
|
+
import { computeSpawnRect, reflowWindows } from './shell-geometry.js';
|
|
12
|
+
|
|
13
|
+
export function createDesktopShell({ root = document.body, wm, registry, brand = 'desktop', themeUrl, onNewInstance, autoBoot = false } = {}) {
|
|
14
|
+
if (!wm) throw new Error('createDesktopShell: wm is required');
|
|
15
|
+
if (!registry) throw new Error('createDesktopShell: registry is required');
|
|
16
|
+
ensureCss(themeUrl);
|
|
17
|
+
|
|
18
|
+
const osRoot = document.createElement('div');
|
|
19
|
+
osRoot.className = 'os-root';
|
|
20
|
+
root.appendChild(osRoot);
|
|
21
|
+
|
|
22
|
+
const { menubar, homeBtn, appsBtn, newInstBtn, instSwitch, clock } =
|
|
23
|
+
buildMenubar({ brand, withNewInstance: !!onNewInstance });
|
|
24
|
+
const appsMenu = buildAppsMenu();
|
|
25
|
+
const sideRail = buildSideRail();
|
|
26
|
+
const { drawer, drawerClose, drawerGrid } = buildDrawer();
|
|
27
|
+
const taskbar = buildTaskbar();
|
|
28
|
+
|
|
29
|
+
const apps = typeof registry.list === 'function' ? registry.list() : [...registry.values()];
|
|
30
|
+
|
|
31
|
+
for (const app of apps) {
|
|
32
|
+
const { menuBtn, railBtn, tile } = buildAppEntries(app, {
|
|
33
|
+
onMenuClick: () => { closeMenu(); openApp(app.id); },
|
|
34
|
+
onRailClick: () => openApp(app.id),
|
|
35
|
+
onTileClick: () => { closeDrawer(); openApp(app.id); },
|
|
36
|
+
});
|
|
37
|
+
appsMenu.appendChild(menuBtn);
|
|
38
|
+
sideRail.appendChild(railBtn);
|
|
39
|
+
drawerGrid.appendChild(tile);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
osRoot.append(menubar, appsMenu, taskbar);
|
|
43
|
+
document.body.append(sideRail, drawer);
|
|
44
|
+
|
|
45
|
+
function openMenu() { appsMenu.classList.add('open'); appsBtn.setAttribute('aria-expanded', 'true'); }
|
|
46
|
+
function closeMenu() { appsMenu.classList.remove('open'); appsBtn.setAttribute('aria-expanded', 'false'); }
|
|
47
|
+
// Focus management: opening the drawer moves keyboard focus onto its
|
|
48
|
+
// close button (the first reachable control inside the now-visible
|
|
49
|
+
// dialog) so Tab starts inside it, not lost on a now-hidden ancestor;
|
|
50
|
+
// closing restores focus to whichever element opened it (homeBtn is the
|
|
51
|
+
// only trigger today) so the user's keyboard position isn't lost.
|
|
52
|
+
let drawerReturnFocus = null;
|
|
53
|
+
function openDrawer() {
|
|
54
|
+
drawerReturnFocus = document.activeElement;
|
|
55
|
+
drawer.classList.add('open');
|
|
56
|
+
drawer.setAttribute('aria-hidden', 'false');
|
|
57
|
+
drawerClose.focus();
|
|
58
|
+
}
|
|
59
|
+
function closeDrawer() {
|
|
60
|
+
drawer.classList.remove('open');
|
|
61
|
+
drawer.setAttribute('aria-hidden', 'true');
|
|
62
|
+
if (drawerReturnFocus && typeof drawerReturnFocus.focus === 'function') drawerReturnFocus.focus();
|
|
63
|
+
drawerReturnFocus = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
appsBtn.addEventListener('click', e => { e.stopPropagation(); appsMenu.classList.contains('open') ? closeMenu() : openMenu(); });
|
|
67
|
+
homeBtn.addEventListener('click', e => { e.stopPropagation(); drawer.classList.contains('open') ? closeDrawer() : openDrawer(); });
|
|
68
|
+
drawerClose.addEventListener('click', closeDrawer);
|
|
69
|
+
drawer.addEventListener('click', e => { if (e.target === drawer) closeDrawer(); });
|
|
70
|
+
document.addEventListener('click', e => {
|
|
71
|
+
if (!appsMenu.contains(e.target) && !appsBtn.contains(e.target)) closeMenu();
|
|
72
|
+
});
|
|
73
|
+
document.addEventListener('keydown', e => {
|
|
74
|
+
if (e.key === 'Escape') { closeMenu(); closeDrawer(); }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function tickClock() { clock.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }
|
|
78
|
+
tickClock();
|
|
79
|
+
const clockTimer = setInterval(tickClock, 30000);
|
|
80
|
+
|
|
81
|
+
let activeContext = null;
|
|
82
|
+
let activeInstanceId = null;
|
|
83
|
+
function setContext(ctx) { activeContext = ctx; }
|
|
84
|
+
|
|
85
|
+
function applyInstanceFilter() {
|
|
86
|
+
if (!activeInstanceId) return;
|
|
87
|
+
for (const wEl of document.querySelectorAll('.wm-win[data-instance-id]')) {
|
|
88
|
+
const wInst = wEl.dataset.instanceId;
|
|
89
|
+
wEl.classList.toggle('wm-inst-hidden', wInst !== activeInstanceId);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let taskbarSig = null;
|
|
94
|
+
function refreshTaskbar() {
|
|
95
|
+
const items = [];
|
|
96
|
+
for (const w of wm.list()) {
|
|
97
|
+
const wEl = document.querySelector('.wm-win[data-id="' + w.id + '"]');
|
|
98
|
+
const wInst = wEl && wEl.dataset.instanceId;
|
|
99
|
+
if (activeInstanceId && wInst && wInst !== activeInstanceId) continue;
|
|
100
|
+
items.push(w);
|
|
101
|
+
}
|
|
102
|
+
// Diff-aware rebuild: the 500ms poll must not reset the taskbar's
|
|
103
|
+
// horizontal scroll (mobile scroll-snap) or button focus every tick.
|
|
104
|
+
const sig = items.map(w => w.id + ' ' + w.title + ' ' + (w.focused ? 1 : 0)).join('');
|
|
105
|
+
if (sig === taskbarSig) return;
|
|
106
|
+
taskbarSig = sig;
|
|
107
|
+
const sl = taskbar.scrollLeft;
|
|
108
|
+
taskbar.innerHTML = '';
|
|
109
|
+
if (!items.length) {
|
|
110
|
+
const empty = document.createElement('span');
|
|
111
|
+
empty.className = 'os-task-empty';
|
|
112
|
+
empty.textContent = 'no windows';
|
|
113
|
+
taskbar.appendChild(empty);
|
|
114
|
+
}
|
|
115
|
+
for (const w of items) {
|
|
116
|
+
const t = document.createElement('button');
|
|
117
|
+
t.className = 'os-task' + (w.focused ? ' focused' : '');
|
|
118
|
+
t.type = 'button';
|
|
119
|
+
t.textContent = w.title;
|
|
120
|
+
t.dataset.winId = w.id;
|
|
121
|
+
// aria-current announces which window is the active one; a
|
|
122
|
+
// sighted user reads this from the .focused visual state alone.
|
|
123
|
+
if (w.focused) t.setAttribute('aria-current', 'true');
|
|
124
|
+
t.addEventListener('click', () => wm.focus(w.id));
|
|
125
|
+
taskbar.appendChild(t);
|
|
126
|
+
}
|
|
127
|
+
taskbar.scrollLeft = sl;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function setActiveInstance(id) {
|
|
131
|
+
activeInstanceId = id;
|
|
132
|
+
for (const btn of instSwitch.querySelectorAll('.os-btn')) {
|
|
133
|
+
btn.classList.toggle('active', btn.dataset.instanceId === id);
|
|
134
|
+
}
|
|
135
|
+
applyInstanceFilter();
|
|
136
|
+
refreshTaskbar();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function openApp(appId) {
|
|
140
|
+
const app = (typeof registry.get === 'function') ? registry.get(appId) : registry[appId];
|
|
141
|
+
if (!app) throw new Error('unknown app: ' + appId);
|
|
142
|
+
const ctx = { ...(activeContext || {}), registry, openApp, wm };
|
|
143
|
+
const result = app.factory(ctx);
|
|
144
|
+
const finish = (r) => {
|
|
145
|
+
const sz = app.defaultSize || { w: 520, h: 360 };
|
|
146
|
+
const { w, h, x, y, maximized } = computeSpawnRect(sz, wm.count);
|
|
147
|
+
const titlePrefix = (activeContext && activeContext.titlePrefix) ? activeContext.titlePrefix + ' · ' : '';
|
|
148
|
+
const win = wm.open({ title: titlePrefix + app.name, body: r.node, kind: appId, width: w, height: h, x, y, maximized });
|
|
149
|
+
win._app = { id: appId, dispose: r.dispose };
|
|
150
|
+
if (activeInstanceId && win.el) {
|
|
151
|
+
win.el.dataset.instanceId = activeInstanceId;
|
|
152
|
+
win.instanceId = activeInstanceId;
|
|
153
|
+
}
|
|
154
|
+
win.appId = appId;
|
|
155
|
+
refreshTaskbar();
|
|
156
|
+
return win;
|
|
157
|
+
};
|
|
158
|
+
return (result && typeof result.then === 'function') ? result.then(finish) : finish(result);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (newInstBtn) newInstBtn.addEventListener('click', () => onNewInstance && onNewInstance({ instSwitch, setContext, openApp }));
|
|
162
|
+
|
|
163
|
+
const taskTimer = setInterval(refreshTaskbar, 500);
|
|
164
|
+
|
|
165
|
+
const onViewportResize = () => reflowWindows(wm);
|
|
166
|
+
window.addEventListener('resize', onViewportResize);
|
|
167
|
+
|
|
168
|
+
const api = {
|
|
169
|
+
wm, registry, openApp, setContext, refreshTaskbar, setActiveInstance,
|
|
170
|
+
openDrawer, closeDrawer, openMenu, closeMenu,
|
|
171
|
+
get activeInstanceId() { return activeInstanceId; },
|
|
172
|
+
elements: { osRoot, menubar, taskbar, appsMenu, sideRail, drawer, instSwitch, homeBtn, appsBtn },
|
|
173
|
+
dispose() { clearInterval(clockTimer); clearInterval(taskTimer); window.removeEventListener('resize', onViewportResize); osRoot.remove(); sideRail.remove(); drawer.remove(); },
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (autoBoot && typeof autoBoot === 'string') openApp(autoBoot);
|
|
177
|
+
return api;
|
|
178
|
+
}
|