PageSourceSearch

https://pressthink.org/j/rosen-archive/frontend/desktop/DesktopShell.js?v=3.8.36

js pressthink.org collected 2026-10-02 04:26:02 UTC 54,158 bytes, 1,415 lines download raw bytes

1import { useEffect, useMemo, useRef, useState } from 'react';
2import { html } from '../html.js?v=3.8.36';
3import {
4  AlertTriangle,
5  Archive,
6  ArrowDown,
7  ArrowLeft,
8  ArrowRight,
9  ArrowUp,
10  BarChart3,
11  BookOpen,
12  Compass,
13  ExternalLink,
14  FileQuestion,
15  FileText,
16  FolderOpen,
17  Info,
18  Minus,
19  Move,
20  Network,
21  RotateCcw,
22  Search,
23  Sparkles,
24  Users,
25  Wrench,
26  X,
27} from 'lucide-react';
28import { resolveSitePath } from '../utils/pathResolver.js?v=3.8.36';
29import DesktopArchivePanel from './DesktopArchivePanel.js?v=3.8.36';
30import DesktopAnalyticsPanel from './DesktopAnalyticsPanel.js?v=3.8.36';
31import DesktopDissertationPanel from './DesktopDissertationPanel.js?v=3.8.36';
32import DesktopEntityPanel from './DesktopEntityPanel.js?v=3.8.36';
33import DesktopStartPanel from './DesktopStartPanel.js?v=3.8.36';
34import {
35  DESKTOP_TOOL_LINKS,
36  getDesktopApp,
37  getReadyDesktopApps,
38} from './desktopRegistry.js?v=3.8.36';
39import {
40  DESKTOP_LAYOUT_STORAGE_KEY,
41  activateDesktopWindow,
42  clampDesktopWindowPosition,
43  closeDesktopWindow,
44  emptyDesktopLayout,
45  minimizeDesktopWindow,
46  moveDesktopWindow,
47  nextVisibleDesktopWindow,
48  parseDesktopLayout,
49  serializeDesktopLayout,
50} from './desktopWindowState.js?v=3.8.36';
51
52const ICONS = {
53  archive: Archive,
54  folders: FolderOpen,
55  start: Compass,
56  entities: Network,
57  dissertation: BookOpen,
58  analytics: BarChart3,
59  tools: Wrench,
60  about: Info,
61  report: AlertTriangle,
62  readme: FileText,
63  findings: Sparkles,
64  method: Search,
65  participate: Users,
66};
67
68const iconFor = (key, className = 'desktop-icon-svg') => {
69  const Icon = ICONS[key] || FileQuestion;
70  return html`
71    <span className=${`desktop-icon-layer ${className}`} aria-hidden="true">
72      <${Icon} className="desktop-icon-glyph" aria-hidden="true" />
73    </span>
74  `;
75};
76
77const launchModeLabel = (app) => {
78  if (app.launch.kind === 'shell') return 'Desktop window';
79  if (app.launch.kind === 'action') return 'Opens dialog';
80  if (app.launch.kind === 'path') return 'Standalone page';
81  return 'Standard view';
82};
83
84const COMPACT_DESKTOP_QUERY = '(max-width: 700px), (max-width: 900px) and (max-height: 520px)';
85const DRAG_THRESHOLD = 5;
86const MOVE_STEP = 32;
87const MOVE_FINE_STEP = 8;
88const WINDOW_EDGE_MARGIN = 8;
89const MINIMUM_VISIBLE_WINDOW_HEIGHT = 340;
90
91const DesktopShell = ({
92  activeAppId = null,
93  autoFocusWindow = true,
94  onSelectApp,
95  onNavigate,
96  onOpenBugReport,
97  onExit,
98  onOpenAppsChange,
99  archiveView,
100  analyticsView,
101  dissertationView,
102  entityView,
103  startView,
104}) => {
105  const readyApps = useMemo(() => getReadyDesktopApps(), []);
106  const shortcutApps = useMemo(
107    () => readyApps.filter((app) => app.surfaces.includes('desktop')),
108    [readyApps],
109  );
110  const startApps = useMemo(
111    () => readyApps.filter((app) => app.surfaces.includes('start')),
112    [readyApps],
113  );
114  const shellApps = useMemo(
115    () => readyApps.filter((app) => app.launch.kind === 'shell'),
116    [readyApps],
117  );
118  const shellAppIds = useMemo(() => shellApps.map((app) => app.id), [shellApps]);
119  const activeApp = activeAppId ? getDesktopApp(activeAppId) : null;
120  const shellApp = activeApp?.availability === 'ready' && activeApp.launch.kind === 'shell'
121    ? activeApp
122    : null;
123  const hasUnknownApp = Boolean(activeAppId && !shellApp);
124
125  const [shortcutFocusIndex, setShortcutFocusIndex] = useState(0);
126  const [startOpen, setStartOpen] = useState(false);
127  const [menuFocusIndex, setMenuFocusIndex] = useState(0);
128  const [statusMessage, setStatusMessage] = useState('');
129  const [resetFocusRequest, setResetFocusRequest] = useState(0);
130  const [dragFocusRequest, setDragFocusRequest] = useState(0);
131  const [moveControlsAppId, setMoveControlsAppId] = useState(null);
132  const [layout, setLayout] = useState(() => {
133    try {
134      return parseDesktopLayout(localStorage.getItem(DESKTOP_LAYOUT_STORAGE_KEY), shellAppIds);
135    } catch {
136      return emptyDesktopLayout();
137    }
138  });
139  const activeShellWindowVisible = Boolean(
140    shellApp
141    && layout.windows.some((entry) => entry.id === shellApp.id && !entry.minimized),
142  );
143
144  const desktopTitleRef = useRef(null);
145  const shortcutPanelRef = useRef(null);
146  const windowStackRef = useRef(null);
147  const windowTitleRefs = useRef({});
148  const windowFrameRefs = useRef({});
149  const moveButtonRefs = useRef({});
150  const movePanelRefs = useRef({});
151  const shortcutRefs = useRef([]);
152  const taskButtonRefs = useRef({});
153  const startButtonRef = useRef(null);
154  const startMenuRef = useRef(null);
155  const menuItemRefs = useRef([]);
156  const lastShellAppRef = useRef(null);
157  const reportedOpenAppsRef = useRef('');
158  const pointerWindowControlRef = useRef(null);
159  const requestedActiveAppRef = useRef(null);
160  const pendingDragFocusRef = useRef(null);
161  const dragSessionRef = useRef(null);
162  const dragFrameRef = useRef(null);
163  const resetFocusPendingRef = useRef(false);
164  const previousMinimizedByIdRef = useRef(new Map(
165    layout.windows.map((entry) => [entry.id, entry.minimized]),
166  ));
167  const layoutRef = useRef(layout);
168  const activeAppIdRef = useRef(activeAppId);
169
170  layoutRef.current = layout;
171  activeAppIdRef.current = activeAppId;
172
173  useEffect(() => {
174    const stylesheetId = 'archive-desktop-styles';
175    if (document.getElementById(stylesheetId)) return undefined;
176
177    const link = document.createElement('link');
178    link.id = stylesheetId;
179    link.rel = 'stylesheet';
180    link.href = resolveSitePath('frontend/desktop/desktop.css?v=3.8.36');
181    link.addEventListener('error', () => {
182      setStatusMessage('Desktop styling could not load. All destinations remain available.');
183    }, { once: true });
184    document.head.appendChild(link);
185
186    return undefined;
187  }, []);
188
189  useEffect(() => {
190    // Registration happens after the first document load. Asking the active
191    // root worker to warm this optional module graph means even a first-ever
192    // direct desktop visit is available on the next offline load, while a
193    // standard archive visitor never downloads desktop-only assets.
194    if ('serviceWorker' in navigator) {
195      navigator.serviceWorker.ready
196        .then((registration) => registration.active?.postMessage({ action: 'cacheDesktop' }))
197        .catch(() => {});
198    }
199    return undefined;
200  }, []);
201
202  useEffect(() => {
203    if (!shellApp) return;
204    setLayout((current) => activateDesktopWindow(current, shellApp.id, shellAppIds));
205  }, [shellApp, shellAppIds]);
206
207  useEffect(() => {
208    if (requestedActiveAppRef.current === activeAppId) {
209      requestedActiveAppRef.current = null;
210    }
211  }, [activeAppId]);
212
213  useEffect(() => {
214    try {
215      if (layout.windows.length === 0) {
216        localStorage.removeItem(DESKTOP_LAYOUT_STORAGE_KEY);
217        return;
218      }
219      localStorage.setItem(
220        DESKTOP_LAYOUT_STORAGE_KEY,
221        serializeDesktopLayout(layout, shellAppIds),
222      );
223    } catch {
224      // Private browsing and storage policies may reject persistence. Window
225      // behavior remains fully usable for the current session.
226    }
227  }, [layout, shellAppIds]);
228
229  useEffect(() => {
230    const visibleAppIds = layout.windows
231      .filter((entry) => !entry.minimized)
232      .map((entry) => entry.id);
233    const signature = visibleAppIds.join('|');
234    if (reportedOpenAppsRef.current === signature) return;
235    reportedOpenAppsRef.current = signature;
236    onOpenAppsChange?.(visibleAppIds);
237  }, [layout.windows, onOpenAppsChange]);
238
239  useEffect(() => {
240    let focusTarget = null;
241    let revealFocusTarget = false;
242    const compactDesktop = window.matchMedia?.(COMPACT_DESKTOP_QUERY).matches === true;
243    if (dragSessionRef.current) return undefined;
244    if (resetFocusPendingRef.current) {
245      if (shellApp || layout.windows.length > 0) return undefined;
246      resetFocusPendingRef.current = false;
247      const resetFrame = requestAnimationFrame(() => {
248        startButtonRef.current?.focus({ preventScroll: true });
249      });
250      return () => cancelAnimationFrame(resetFrame);
251    }
252    if (shellApp) {
253      lastShellAppRef.current = shellApp.id;
254      if (!autoFocusWindow) return undefined;
255      const activeWindow = document.querySelector(`[data-window-id="${shellApp.id}"]`);
256      if (activeWindow?.contains(document.activeElement)) return undefined;
257      focusTarget = windowTitleRefs.current[shellApp.id];
258    } else if (activeAppId) {
259      setStatusMessage('That desktop item is unavailable. Showing the desktop home.');
260      focusTarget = windowTitleRefs.current.home;
261      revealFocusTarget = compactDesktop;
262    } else if (lastShellAppRef.current && layout.windows.length === 0) {
263      const index = shortcutApps.findIndex((app) => app.id === lastShellAppRef.current);
264      if (index >= 0) {
265        setShortcutFocusIndex(index);
266        focusTarget = shortcutRefs.current[index];
267      }
268      lastShellAppRef.current = null;
269    } else {
270      focusTarget = compactDesktop
271        ? desktopTitleRef.current
272        : windowTitleRefs.current.home || desktopTitleRef.current;
273    }
274
275    const frame = requestAnimationFrame(() => {
276      if (shellApp) {
277        const activeWindow = document.querySelector(`[data-window-id="${shellApp.id}"]`);
278        // A foreground overlay can restore its exact opener after this effect
279        // has scheduled chrome focus. Re-check at paint time so the window
280        // title cannot overwrite that more precise focus destination.
281        if (activeWindow?.contains(document.activeElement)) return;
282      }
283      focusTarget?.focus({ preventScroll: true });
284      if (revealFocusTarget) {
285        focusTarget?.scrollIntoView({ block: 'start', inline: 'nearest' });
286      }
287    });
288    return () => cancelAnimationFrame(frame);
289  }, [
290    activeAppId,
291    activeShellWindowVisible,
292    autoFocusWindow,
293    shellApp,
294    shortcutApps,
295    layout.windows.length,
296    dragFocusRequest,
297    resetFocusRequest,
298  ]);
299
300  useEffect(() => {
301    if (!shellApp) return undefined;
302    const frame = requestAnimationFrame(() => {
303      taskButtonRefs.current[shellApp.id]?.scrollIntoView({
304        block: 'nearest',
305        inline: 'nearest',
306      });
307    });
308    return () => cancelAnimationFrame(frame);
309  }, [shellApp, layout.windows]);
310
311  useEffect(() => {
312    if (!window.matchMedia) return undefined;
313    const compactQuery = window.matchMedia(COMPACT_DESKTOP_QUERY);
314    const preserveVisibleFocus = (event) => {
315      if (!event.matches) return;
316      cancelWindowDrag();
317      if (moveControlsAppId) {
318        setMoveControlsAppId(null);
319        requestAnimationFrame(() => {
320          windowTitleRefs.current[shellApp?.id || 'home']?.focus({ preventScroll: true });
321        });
322      }
323      const hiddenMoveFocus = document.activeElement?.closest?.(
324        '.desktop-window-move, .desktop-window-move-panel',
325      );
326      if (
327        shellApp
328        && (shortcutPanelRef.current?.contains(document.activeElement) || hiddenMoveFocus)
329      ) {
330        requestAnimationFrame(() => {
331          windowTitleRefs.current[shellApp.id]?.focus({ preventScroll: true });
332        });
333      } else if (!shellApp && document.activeElement === windowTitleRefs.current.home) {
334        requestAnimationFrame(() => {
335          if (activeAppId) {
336            windowTitleRefs.current.home?.scrollIntoView({ block: 'start', inline: 'nearest' });
337          } else {
338            desktopTitleRef.current?.focus({ preventScroll: true });
339          }
340        });
341      }
342    };
343    compactQuery.addEventListener('change', preserveVisibleFocus);
344    return () => compactQuery.removeEventListener('change', preserveVisibleFocus);
345  }, [activeAppId, moveControlsAppId, shellApp]);
346
347  useEffect(() => {
348    if (!startOpen) return undefined;
349    setMenuFocusIndex(0);
350    const frame = requestAnimationFrame(() =>
350 menuItemRefs.current[0]?.focus());
351
352    const closeFromOutside = (event) => {
353      if (
354        !startMenuRef.current?.contains(event.target)
355        && !startButtonRef.current?.contains(event.target)
356      ) {
357        setStartOpen(false);
358      }
359    };
360    document.addEventListener('pointerdown', closeFromOutside);
361    return () => {
362      cancelAnimationFrame(frame);
363      document.removeEventListener('pointerdown', closeFromOutside);
364    };
365  }, [startOpen]);
366
367  useEffect(() => {
368    if (!moveControlsAppId) return undefined;
369    const frame = requestAnimationFrame(() => {
370      movePanelRefs.current[moveControlsAppId]
371        ?.querySelector('button')
372        ?.focus({ preventScroll: true });
373    });
374    return () => cancelAnimationFrame(frame);
375  }, [moveControlsAppId]);
376
377  useEffect(() => {
378    const appId = pendingDragFocusRef.current;
379    if (!appId || activeAppId !== appId || dragSessionRef.current) return undefined;
380    const frame = requestAnimationFrame(() => {
381      windowTitleRefs.current[appId]?.focus({ preventScroll: true });
382      pendingDragFocusRef.current = null;
383    });
384    return () => cancelAnimationFrame(frame);
385  }, [activeAppId, dragFocusRequest]);
386
387  const isCompactDesktop = () => (
388    window.matchMedia?.(COMPACT_DESKTOP_QUERY).matches === true
389  );
390
391  const positionForWindow = (appId, sourceLayout = layoutRef.current) => (
392    sourceLayout.windows.find((entry) => entry.id === appId)?.position || { x: 0, y: 0 }
393  );
394
395  const workAreaBounds = () => {
396    const visualViewport = window.visualViewport;
397    const left = visualViewport?.offsetLeft || 0;
398    const top = visualViewport?.offsetTop || 0;
399    const right = left + (visualViewport?.width || window.innerWidth);
400    const viewportBottom = top + (visualViewport?.height || window.innerHeight);
401    const taskbarTop = document.querySelector('.desktop-taskbar')
402      ?.getBoundingClientRect().top;
403    return {
404      left,
405      right,
406      top,
407      bottom: Number.isFinite(taskbarTop) ? Math.min(viewportBottom, taskbarTop) : viewportBottom,
408    };
409  };
410
411  const clampWindowPosition = (
412    appId,
413    position,
414    currentPosition = positionForWindow(appId),
415    capturedGeometry = null,
416  ) => {
417    const frame = windowFrameRefs.current[appId];
418    const titlebar = frame?.querySelector('.desktop-window-titlebar');
419    if (!frame || !titlebar) return currentPosition;
420    return clampDesktopWindowPosition(position, {
421      currentPosition,
422      frameRect: capturedGeometry?.frameRect || frame.getBoundingClientRect(),
423      titlebarRect: capturedGeometry?.titlebarRect || titlebar.getBoundingClientRect(),
424      workArea: capturedGeometry?.workArea || workAreaBounds(),
425      margin: WINDOW_EDGE_MARGIN,
426      minimumVisibleHeight: MINIMUM_VISIBLE_WINDOW_HEIGHT,
427    });
428  };
429
430  const applyWindowPosition = (appId, position) => {
431    const frame = windowFrameRefs.current[appId];
432    if (!frame) return;
433    frame.style.setProperty('--desktop-window-position-x', `${position.x}px`);
434    frame.style.setProperty('--desktop-window-position-y', `${position.y}px`);
435  };
436
437  const scheduleWindowPositionPreview = (appId, position) => {
438    if (dragFrameRef.current !== null) cancelAnimationFrame(dragFrameRef.current);
439    dragFrameRef.current = requestAnimationFrame(() => {
440      dragFrameRef.current = null;
441      applyWindowPosition(appId, position);
442    });
443  };
444
445  const releaseDragCapture = (session, event) => {
446    try {
447      if (session.captureTarget.hasPointerCapture(event.pointerId)) {
448        session.captureTarget.releasePointerCapture(event.pointerId);
449      }
450    } catch {
451      // A canceled or disconnected pointer may already have lost capture.
452    }
453  };
454
455  const cancelWindowDrag = (restorePosition = true) => {
456    const session = dragSessionRef.current;
457    if (!session) return;
458    dragSessionRef.current = null;
459    if (dragFrameRef.current !== null) {
460      cancelAnimationFrame(dragFrameRef.current);
461      dragFrameRef.current = null;
462    }
463    session.frame.classList.remove('is-dragging');
464    if (restorePosition) applyWindowPosition(session.app.id, session.startPosition);
465    try {
466      if (session.captureTarget.hasPointerCapture(session.pointerId)) {
467        session.captureTarget.releasePointerCapture(session.pointerId);
468      }
469    } catch {
470      // Reflow and unmount can disconnect the original capture target.
471    }
472  };
473
474  const activateWindow = (appId, { focusIfActive = true } = {}) => {
475    if (
476      requestedActiveAppRef.current === appId
477      && activeAppIdRef.current !== appId
478    ) return;
479    setLayout((current) => activateDesktopWindow(current, appId, shellAppIds));
480    if (activeAppIdRef.current === appId) {
481      if (focusIfActive && !dragSessionRef.current) {
482        windowTitleRefs.current[appId]?.focus({ preventScroll: true });
483      }
484    } else {
485      requestedActiveAppRef.current = appId;
486      onSelectApp?.(appId);
487    }
488  };
489
490  const beginWindowDrag = (event, app) => {
491    if (
492      isCompactDesktop()
493      || event.isPrimary === false
494      || (event.pointerType === 'mouse' && event.button !== 0)
495      || event.target.closest('.desktop-window-controls')
496    ) return;
497
498    const frame = windowFrameRefs.current[app.id];
499    if (!frame) return;
500    const startPosition = positionForWindow(app.id);
501    const titlebarRect = event.currentTarget.getBoundingClientRect();
502    setStartOpen(false);
503    try {
504      event.currentTarget.setPointerCapture(event.pointerId);
505    } catch {
506      return;
507    }
508    dragSessionRef.current = {
509      app,
510      pointerId: event.pointerId,
511      captureTarget: event.currentTarget,
512      frame,
513      startClientX: event.clientX,
514      startClientY: event.clientY,
515      startPosition,
516      position: startPosition,
517      moved: false,
518      frameRect: frame.getBoundingClientRect(),
519      titlebarRect,
520      workArea: workAreaBounds(),
521    };
522    activateWindow(app.id, { focusIfActive: false });
523    event.stopPropagation();
524  };
525
526  const moveWindowDrag = (event) => {
527    const session = dragSessionRef.current;
528    if (!session || session.pointerId !== event.pointerId) return;
529    const deltaX = event.clientX - session.startClientX;
530    const deltaY = event.clientY - session.startClientY;
531    if (!session.moved && Math.hypot(deltaX, deltaY) < DRAG_THRESHOLD) return;
532    event.preventDefault();
533    if (!session.moved) {
534      session.moved = true;
535      session.frame.classList.add('is-dragging');
536    }
537    session.position = clampWindowPosition(
538      session.app.id,
539      {
540        x: session.startPosition.x + deltaX,
541        y: session.startPosition.y + deltaY,
542      },
543      session.startPosition,
544      session,
545    );
546    scheduleWindowPositionPreview(session.app.id, session.position);
547  };
548
549  const finishWindowDrag = (event, canceled = false) => {
550    const session = dragSessionRef.current;
551    if (!session || session.pointerId !== event.pointerId) return;
552    dragSessionRef.current = null;
553    if (dragFrameRef.current !== null) {
554      cancelAnimationFrame(dragFrameRef.current);
555      dragFrameRef.current = null;
556    }
557    session.frame.classList.remove('is-dragging');
558    releaseDragCapture(session, event);
559
560    if (canceled) {
561      applyWindowPosition(session.app.id, session.startPosition);
562      setStatusMessage(`Canceled moving ${session.app.label}.`);
563    } else if (session.moved) {
564      applyWindowPosition(session.app.id, session.position);
565      setLayout((current) => moveDesktopWindow(
566        current,
567        session.app.id,
568        session.position,
569        shellAppIds,
570      ));
571      setStatusMessage(`Moved ${session.app.label} window.`);
572    } else {
573      setStatusMessage(`Activated ${session.app.label}.`);
574    }
575
576    pendingDragFocusRef.current = session.app.id;
577    setDragFocusRequest((request) => request + 1);
578    requestAnimationFrame(() => {
579      requestAnimationFrame(() => {
580        windowTitleRefs.current[session.app.id]?.focus({ preventScroll: true });
581      });
582    });
583  };
584
585  const closeMoveControls = (appId, returnFocus = true) => {
586    setMoveControlsAppId((current) => (current === appId ? null : current));
587    if (returnFocus) {
588      requestAnimationFrame(() => {
589        moveButtonRefs.current[appId]?.focus({ preventScroll: true });
590      });
591    }
592  };
593
594  const openMoveControls = (app) => {
595    if (isCompactDesktop()) return;
596    activateWindow(app.id, { focusIfActive: false });
597    setMoveControlsAppId(app.id);
598    setStatusMessage(`Move controls opened for ${app.label}.`);
599  };
600
601  const commitWindowPosition = (app, nextPosition, message) => {
602    const currentPosition = positionForWindow(app.id);
603    const clamped = clampWindowPosition(app.id, nextPosition, currentPosition);
604    applyWindowPosition(app.id, clamped);
605    setLayout((current) => moveDesktopWindow(current, app.id, clamped, shellAppIds));
606    setStatusMessage(message);
607  };
608
609  const moveWindowBy = (app, deltaX, deltaY, direction) => {
610    const currentPosition = positionForWindow(app.id);
611    commitWindowPosition(app, {
612      x: currentPosition.x + deltaX,
613      y: currentPosition.y + deltaY,
614    }, `${app.label} moved ${direction}.`);
615  };
616
617  const handleMoveControlsKeyDown = (event, app) => {
618    if (event.key === 'Escape') {
619      event.preventDefault();
620      closeMoveControls(app.id);
621      return;
622    }
623    const step = event.shiftKey ? MOVE_FINE_STEP : MOVE_STEP;
624    const direction = {
625      ArrowLeft: [-step, 0, 'left'],
626      ArrowUp: [0, -step, 'up'],
627      ArrowDown: [0, step, 'down'],
628      ArrowRight: [step, 0, 'right'],
629    }[event.key];
630    if (!direction) return;
631    event.preventDefault();
632    moveWindowBy(app, ...direction);
633  };
634
635  const reclampVisibleWindows = () => {
636    if (isCompactDesktop()) return;
637    setLayout((current) => {
638      let next = current;
639      for (const entry of current.windows) {
640        if (entry.minimized || !windowFrameRefs.current[entry.id]) continue;
641        const clamped = clampWindowPosition(entry.id, entry.position, entry.position);
642        if (clamped.x === entry.position.x && clamped.y === entry.position.y) continue;
643        applyWindowPosition(entry.id, clamped);
644        next = moveDesktopWindow(next, entry.id, clamped, shellAppIds);
645      }
646      return next;
647    });
648  };
649
650  const minimizedStateSignature = layout.windows
651    .map((entry) => `${entry.id}:${entry.minimized ? 1 : 0}`)
652    .join('|');
653  useEffect(() => {
654    const previous = previousMinimizedByIdRef.current;
655    const restoredWindow = layout.windows.some((entry) => (
656      entry.minimized === false && previous.get(entry.id) === true
657    ));
658    previousMinimizedByIdRef.current = new Map(
659      layout.windows.map((entry) => [entry.id, entry.minimized]),
660    );
661    if (!restoredWindow || isCompactDesktop()) return undefined;
662
663    let reclampFrame = null;
664    const mountedFrame = requestAnimationFrame(() => {
665      reclampFrame = requestAnimationFrame(reclampVisibleWindows);
666    });
667    return () => {
668      cancelAnimationFrame(mountedFrame);
669      if (reclampFrame !== null) cancelAnimationFrame(reclampFrame);
670    };
671  }, [minimizedStateSignature]);
672
673  useEffect(() => {
674    const cancelFromKeyboard = (event) => {
675      if (event.key !== 'Escape' || !dragSessionRef.current) return;
676      event.preventDefault();
677      const app = dragSessionRef.current.app;
678      cancelWindowDrag();
679      setStatusMessage(`Canceled moving ${app.label}.`);
680      requestAnimationFrame(() => {
681        windowTitleRefs.current[app.id]?.focus({ preventScroll: true });
682      });
683    };
684    document.addEventListener('keydown', cancelFromKeyboard);
685    return () => document.removeEventListener('keydown', cancelFromKeyboard);
686  }, []);
687
688  useEffect(() => {
689    let frame = null;
690    const handleViewportChange = () => {
691      cancelWindowDrag();
692      if (frame !== null) cancelAnimationFrame(frame);
693      frame = requestAnimationFrame(() => {
694        frame = null;
695        if (isCompactDesktop()) {
696          setMoveControlsAppId(null);
697          return;
698        }
699        reclampVisibleWindows();
700      });
701    };
702
703    const observer = typeof ResizeObserver === 'function'
704      ? new ResizeObserver(handleViewportChange)
705      : null;
706    if (windowStackRef.current) observer?.observe(windowStackRef.current);
707    window.addEventListener('resize', handleViewportChange);
708    window.addEventListener('orientationchange', handleViewportChange);
709    window.visualViewport?.addEventListener('resize', handleViewportChange);
710    handleViewportChange();
711    return () => {
712      if (frame !== null) cancelAnimationFrame(frame);
713      observer?.disconnect();
714      window.removeEventListener('resize', handleViewportChange);
715      window.removeEventListener('orientationchange', handleViewportChange);
716      window.visualViewport?.removeEventListener('resize', handleViewportChange);
717    };
718  }, [layout.windows.map((entry) => entry.id).join('|')]);
719
720  const openApp = (app, source = 'desktop') => {
721    const index = shortcutApps.findIndex((candidate) => candidate.id === app.id);
722    if (index >= 0) setShortcutFocusIndex(index);
723    setStartOpen(false);
724
725    if (app.launch.kind === 'shell') {
726      setStatusMessage(`Opening ${app.label}.`);
727      activateWindow(app.launch.destination);
728      return;
729    }
730    if (app.launch.kind === 'action') {
731      setStatusMessage('Opening the archive problem report.');
732      // A Start menu item disappears in the same render that mounts the
733      // canonical report dialog. Move focus to the durable Start button first
734      // so the shared modal can capture and later restore a connected trigger.
735      if (source === 'start-menu') {
736        startButtonRef.current?.focus({ preventScroll: true });
737      }
738      onOpenBugReport?.();
739      return;
740    }
741    if (app.launch.kind === 'path') {
742      setStatusMessage(`${app.label} opens as a standalone archive page.`);
743      window.location.assign(resolveSitePath(app.launch.destination));
744      return;
745    }
746
747    setStatusMessage(`${app.label} opens in the standard archive view.`);
748    onNavigate?.(app.launch.destination);
749  };
750
751  const moveShortcutFocus = (nextIndex) => {
752    const clamped = Math.max(0, Math.min(shortcutApps.length - 1, nextIndex));
753    setShortcutFocusIndex(clamped);
754    shortcutRefs.current[clamped]?.focus();
755  };
756
757  const handleShortcutKeyDown = (event, app, index) => {
758    const columns = 2;
759    const lastIndex = shortcutApps.length - 1;
760    const rowStart = Math.floor(index / columns) * columns;
761    const rowEnd = Math.min(rowStart + columns - 1, lastIndex);
762    let nextIndex = index;
763
764    if (event.key === 'Enter' || event.key === ' ') {
765      event.preventDefault();
766      openApp(app);
767      return;
768    }
769    if (event.key === 'ArrowLeft') nextIndex = Math.max(rowStart, index - 1);
770    else if (event.key === 'ArrowRight') nextIndex = Math.min(rowEnd, index + 1);
771    else if (event.key === 'ArrowUp') nextIndex = index - columns >= 0 ? index - columns : index;
772    else if (event.key === 'ArrowDown') nextIndex = index + columns <= lastIndex ? index + columns : index;
773    else if (event.key === 'Home') nextIndex = 0;
774    else if (event.key === 'End') nextIndex = lastIndex;
775    else return;
776
777    event.preventDefault();
778    moveShortcutFocus(nextIndex);
779  };
780
781  const menuEntryCount = startApps.length + 2;
782  const handleMenuKeyDown = (event, index) => {
783    let nextIndex = index;
784    if (event.key === 'ArrowDown') nextIndex = (index + 1) % menuEntryCount;
785    else if (event.key === 'ArrowUp') nextIndex = (index - 1 + menuEntryCount) % menuEntryCount;
786    else if (event.key === 'Home') nextIndex = 0;
787    else if (event.key === 'End') nextIndex = menuEntryCount - 1;
788    else if (event.key === 'Escape') {
789      event.preventDefault();
790      setStartOpen(false);
791      startButtonRef.current?.focus();
792      return;
793    } else if (event.key === 'Tab') {
794      setStartOpen(false);
795      return;
796    } else {
797      return;
798    }
799
800    event.preventDefault();
801    setMenuFocusIndex(nextIndex);
802    menuItemRefs.current[nextIndex]?.focus();
803  };
804
805  const closeWindow = (app) => {
806    if (dragSessionRef.current?.app.id === app.id) cancelWindowDrag();
807    if (moveControlsAppId === app.id) closeMoveControls(app.id, false);
808    const nextLayout = closeDesktopWindow(layout, app.id, shellAppIds);
809    setLayout(nextLayout);
810    setStatusMessage(`Closed ${app.label}.`);
811    if (activeAppId === app.id) {
812      onSelectApp?.(nextVisibleDesktopWindow(nextLayout) || null);
813    } else {
814      requestAnimationFrame(() => {
815        windowTitleRefs.current[shellApp?.id || 'home']?.focus({ preventScroll: true });
816      });
817    }
818  };
819
820  const minimizeWindow = (app) => {
821    if (dragSessionRef.current?.app.id === app.id) cancelWindowDrag();
822    if (moveControlsAppId === app.id) closeMoveControls(app.id, false);
823    const nextLayout = minimizeDesktopWindow(layout, app.id, shellAppIds);
824    setLayout(nextLayout);
825    setStatusMessage(`Minimized ${app.label}. Use its taskbar button to restore it.`);
826    if (activeAppId === app.id) {
827      onSelectApp?.(nextVisibleDesktopWindow(nextLayout, app.id) || null);
828    } else {
829      requestAnimationFrame(() => {
830        windowTitleRefs.current[shellApp?.id || 'home']?.focus({ preventScroll: true });
831      });
832    }
833  };
834
835  const resetLayout = () => {
836    cancelWindowDrag();
837    setMoveControlsAppId(null);
838    resetFocusPendingRef.current = true;
839    lastShellAppRef.current = null;
840    setResetFocusRequest((request) => request + 1);
841    setLayout(emptyDesktopLayout());
842    setStartOpen(false);
843    setStatusMessage('Desktop layout reset. All app windows were closed.');
844    try {
845      localStorage.removeItem(DESKTOP_LAYOUT_STORAGE_KEY);
846    } catch {
847      // The in-memory reset still succeeds when storage is unavailable.
848    }
849    onSelectApp?.(null);
850  };
851
852  const renderWelcome = () => html`
853    <div className="desktop-document">
854      ${hasUnknownApp && html`
855        <div className="desktop-notice archive-notice archive-notice--warning" role="note">
856          <${AlertTriangle} className="desktop-inline-icon" aria-hidden="true" />
857          <span>That desktop item is unavailable. Showing the desktop home.</span>
858        </div>
859      `}
860      <p className="desktop-eyebrow">A second front door</p>
861      <h3>Welcome to Rosen 98</h3>
862      <p className="desktop-lede">
863        This optional view arranges the same Rosen Archive as shortcuts, folders, and windows. It is a map of the collection, not a separate copy of it.
864      </p>
865      <div className="desktop-principles">
866        <article>
867          <span className="desktop-principle-number">01</span>
868          <h4>Choose a path</h4>
869          <p>Open a shortcut once, or use Start for the complete destination list.</p>
870        </article>
871        <article>
872          <span className="desktop-principle-number">02</span>
873          <h4>Keep canonical links</h4>
874          <p>Archive records and research areas reuse maintained content and preserve canonical links.</p>
875        </article>
876        <article>
877          <span className="desktop-principle-number">03</span>
878          <h4>Leave at any time</h4>
879          <p>The taskbar always provides a direct route back to the standard archive.</p>
880        </article>
881      </div>
882    </div>
883  `;
884
885  const renderReadme = () => html`
886    <div className="desktop-document">
887      <p className="desktop-eyebrow">Read me</p>
888      <h3>A familiar shape for a large archive</h3>
889      <p className="desktop-lede">
890        Personal-computing and early-web history overlap with many of the archive's questions about publishing, publics, platforms, and control. This shell uses that history to make routes visible and the collection feel containable.
891      </p>
892
893      <dl className="desktop-definition-list">
894        <div><dt>Shortcut</dt><dd>A maintained way into the collection. One click is enough.</dd></div>
895        <div><dt>Desktop window</dt><dd>A live archive surface that can remain open while you compare another path.</dd></div>
896        <div><dt>Standard view</dt><dd>The existing canonical archive route, with its full current behavior.</dd></div>
897        <div><dt>Start</dt><dd>The complete list of destinations and the dependable way out.</dd></div>
898      </dl>
899
900      <section className="desktop-layout-help" aria-labelledby="desktop-layout-help-title">
901        <h4 id="desktop-layout-help-title">Window memory</h4>
902        <p>
903          On wider screens, drag a title bar or choose its Move button. Move offers click, tap, and keyboard arrow controls. Open, minimized, and moved app windows are saved on this device. Active state stays in the URL, so Back and Forward move between the windows you activated; position changes do not add history entries.
904        </p>
905        <button type="button" className="desktop-standard-link archive-action archive-action--secondary" onClick=${resetLayout}>
906          <${RotateCcw} aria-hidden="true" />
907          Reset desktop layout
908        </button>
909      </section>
910
911    </div>
912  `;
913
914  const renderTools = () => html`
915    <div className="desktop-document">
916      <p className="desktop-eyebrow">Maintained tools</p>
917      <h3>Reading and research tools</h3>
918      <p className="desktop-lede">
919        These pages are maintained alongside the archive. Each opens as its own full page and keeps a route back to the collection.
920      </p>
921      <div className="desktop-tool-grid">
922        ${DESKTOP_TOOL_LINKS.map((tool) => html`
923          <a
924            key=${tool.id}
925            className="desktop-tool-link"
926            href=${resolveSitePath(tool.href)}
927            aria-label=${`${tool.label}. Opens a standalone archive page.`}
928          >
929            <span className="desktop-tool-icon">${iconFor(tool.icon)}</span>
930            <span className="desktop-tool-copy">
931              <strong>${tool.label}</strong>
932              <small>${tool.description}</small>
933              <span className="desktop-tool-mode">
934                Open page <${ExternalLink} className="desktop-external-icon" aria-hidden="true" />
935                ${tool.status === 'beta' ? html`<em>Beta</em>` : ''}
936                ${tool.status === 'experimental' ? html`<em>Experimental</em>` : ''}
937              </span>
938            </span>
939          </a>
940        `)}
941      </div>
942    </div>
943  `;
944
945  const renderDataStatus = (view, loadingMessage, errorTitle) => {
946    if (view.error) {
947      return html`
948        <div className="desktop-data-status archive-notice archive-notice--danger is-error" role="alert">
949          <${AlertTriangle} aria-hidden="true" />
950          <div>
951            <strong>${errorTitle}</strong>
952            <p>${view.error}</p>
953            <button type="button" className="archive-action archive-action--danger" onClick=${() => window.location.reload()}>
954              <${RotateCcw} aria-hidden="true" />
955              Reload page
956            </button>
957          </div>
958        </div>
959      `;
960    }
961
962    if (view.loading) {
963      return html`<p className="desktop-data-status archive-notice" role="status">${loadingMessage}</p>`;
964    }
965
966    return null;
967  };
968
969  const renderArchive = (appId) => {
970    const windowViewMode = appId === 'folders' ? 'folder' : 'grid';
971    return html`
972    <${DesktopArchivePanel}
973      viewMode=${windowViewMode}
974      loading=${archiveView.loading}
975      error=${archiveView.error}
976      filters=${archiveView.filters}
977      setFilters=${archiveView.setFilters}
978      facets=${archiveView.facets}
979      autocompleteIndex=${archiveView.autocompleteIndex}
980      sortBy=${archiveView.sortBy}
981      setSortBy=${archiveView.setSortBy}
982      filteredRecords=${archiveView.filteredRecords}
983      paginatedRecords=${archiveView.paginatedRecords}
984      folderGroups=${archiveView.folderGroups}
985      currentPage=${archiveView.currentPage}
986      totalPages=${archiveView.totalPages}
987      activeFilterCount=${archiveView.activeFilterCount}
988      onSetViewMode=${archiveView.onSetViewMode}
989      onSelectRecord=${archiveView.onSelectRecord}
990      onOpenFolder=${archiveView.onOpenFolder}
991      onPageChange=${archiveView.onPageChange}
992      onClearFilters=${archiveView.onClearFilters}
993      onOpenStandard=${() => archiveView.onOpenStandard(windowViewMode)}
994    />
995  `;
996  };
997
998  const renderEntities = () => html`
999    <${DesktopEntityPanel}
1000      records=${entityView.records}
1001      queryActive=${entityView.queryActive}
1002      onClearQuery=${entityView.onClearQuery}
1003      dataStatus=${renderDataStatus(
1004        entityView,
1005        'Loading connected archive records…',
1006        'Connected records are unavailable.',
1007      )}
1008      onSelectRecord=${entityView.onSelectRecord}
1009      selectedEntityId=${entityView.selectedEntityId}
1010      onSelectEntity=${entityView.onSelectEntity}
1011      autoFocusSelection=${entityView.autoFocusSelection}
1012      onOpenStandard=${entityView.onOpenStandard}
1013    />
1014  `;
1015
1016  const renderDissertation = () => html`
1017    <${DesktopDissertationPanel}
1018      onOpenStandard=${dissertationView.onOpenStandard}
1019    />
1020  `;
1021
1022  const renderAnalytics = () => html`
1023    <${DesktopAnalyticsPanel}
1024      onRecordResults=${analyticsView.onRecordResults}
1025      onOpenStandard=${analyticsView.onOpenStandard}
1026    />
1027  `;
1028
1029  const renderGuidedPath = (appId) => html`
1030    <${DesktopStartPanel}
1031      mode=${appId}
1032      records=${startView.records}
1033      dataStatus=${renderDataStatus(
1034        startView,
1035        'Loading the guide’s archive records…',
1036        'Guide records are unavailable.',
1037      )}
1038      onNavigate=${startView.onNavigate}
1039      onSelectRecord=${startView.onSelectRecord}
1040      onOpenBugReport=${startView.onOpenBugReport}
1041      onOpenStandard=${startView.onOpenStandard}
1042    />
1043  `;
1044
1045  const renderWindowContent = (app) => {
1046    if (!app) return renderWelcome();
1047    if (app.id === 'archive' || app.id === 'folders') return renderArchive(app.id);
1048    if (app.id === 'entities') return renderEntities();
1049    if (app.id === 'dissertation') return renderDissertation();
1050    if (app.id === 'analytics') return renderAnalytics();
1051    if (app.id === 'start' || app.id === 'findings') return renderGuidedPath(app.id);
1052    if (app.id === 'readme') return renderReadme();
1053    if (app.id === 'tools') return renderTools();
1054    return renderWelcome();
1055  };
1056
1057  const windowStatusText = (app, isActive) => {
1058    const stackStatus = isActive ? 'Active' : 'Background';
1059    let context = 'One archive, two ways in';
1060
1061    if (app?.id === 'archive' || app?.id === 'folders') {
1062      if (archiveView.loading) context = 'Loading canonical archive records';
1063      else if (archiveView.error) context = 'Canonical archive records unavailable';
1064      else {
1065        const filterLabel = archiveView.activeFilterCount === 1 ? 'filter' : 'filters';
1066        context = `${archiveView.filteredRecords.length} records; ${archiveView.activeFilterCount} active ${filterLabel}; page ${archiveView.currentPage} of ${archiveView.totalPages}`;
1067      }
1068    } else if (app?.id === 'entities') {
1069      context = entityView.selectedEntityId
1070        ? 'Selected entity context stays open'
1071        : 'Canonical entity index; connected records open in place';
1072    } else if (app?.id === 'analytics') {
1073      context = 'Aggregate index ready; larger queries load on request';
1074    } else if (app?.id === 'dissertation') {
1075      context = 'One dissertation, many routes through it';
1076    } else if (app?.id === 'start' || app?.id === 'findings') {
1077      context = 'Guide records come from the canonical archive';
1078    } else if (app?.id === 'tools') {
1079      context = `${DESKTOP_TOOL_LINKS.length} maintained archive tools`;
1080    } else if (app?.id === 'readme') {
1081      context = 'Layout memory: this device only';
1082    }
1083
1084    return `${stackStatus}; ${context}`;
1085  };
1086
1087  const renderWindowFrame = (app) => {
1088    const appId = app?.id || 'home';
1089    const windowTitle = app?.label || 'Welcome to Rosen 98';
1090    const windowEntry = app
1091      ? layout.windows.find((entry) => entry.id === app.id)
1092      : null;
1093    const isActive = app ? shellApp?.id === app.id : !shellApp;
1094    const isWideWindow = [
1095      'archive',
1096      'folders',
1097      'entities',
1098      'dissertation',
1099      'analytics',
1100      'start',
1101      'findings',
1102    ].includes(app?.id);
1103    const titleId = `desktop-window-title-${appId}`;
1104    const movePanelId = `desktop-window-move-panel-${appId}`;
1105    const moveInstructionsId = `desktop-window-move-instructions-${appId}`;
1106    const style = app ? {
1107      '--desktop-window-position-x': `${windowEntry?.position.x || 0}px`,
1108      '--desktop-window-position-y': `${windowEntry?.position.y || 0}px`,
1109      zIndex: layout.zOrder.indexOf(app.id) + 3,
1110    } : {
1111      // Restored windows remain available in the taskbar on a desktop-home or
1112      // unknown-app URL, but home is the active surface in both cases. Keep it
1113      // above the saved stack so its visible content agrees with URL and focus
1114      // state; an activated app sends home back behind the stack.
1115      zIndex: shellApp ? 1 : layout.zOrder.length + 3,
1116    };
1117
1118    return html`
1119      <section
1120        key=${appId}
1121        ref=${(element) => { windowFrameRefs.current[appId] = element; }}
1122        className=${`desktop-window ${isWideWindow ? 'desktop-window-wide' : ''} ${isActive ? 'is-active' : 'is-inactive'} ${app ? '' : 'desktop-home-window'}`}
1123        style=${style}
1124        role="region"
1125        aria-labelledby=${titleId}
1126        data-window-id=${appId}
1127        onPointerDown=${(event) => {
1128          if (!app || isActive) return;
1129          if (event.target.closest('.desktop-window-controls')) {
1130            pointerWindowControlRef.current = app.id;
1131            requestAnimationFrame(() => {
1132              if (pointerWindowControlRef.current === app.id) {
1133                pointerWindowControlRef.current = null;
1134              }
1135            });
1136            return;
1137          }
1138          activateWindow(app.id);
1139        }}
1140        onFocusCapture=${(event) => {
1141          const pointerControlFocus = pointerWindowControlRef.current === app?.id
1142            && event.target.closest('.desktop-window-controls');
1143          const dragFocus = dragSessionRef.current?.app.id === app?.id;
1144          if (app && !isActive && !pointerControlFocus && !dragFocus) activateWindow(app.id);
1145        }}
1146      >
1147        <div
1148          className=${`desktop-window-titlebar ${app ? 'is-movable' : ''}`}
1149          onPointerDown=${app ? (event) => beginWindowDrag(event, app) : undefined}
1150          onPointerMove=${app ? moveWindowDrag : undefined}
1151          onPointerUp=${app ? (event) => finishWindowDrag(event) : undefined}
1152          onPointerCancel=${app ? (event) => finishWindowDrag(event, true) : undefined}
1153          onLostPointerCapture=${app ? (event) => finishWindowDrag(event, true) : undefined}
1154        >
1155          <div className="desktop-window-title-copy">
1156            <span aria-hidden="true">${iconFor(app?.icon || 'archive', 'desktop-titlebar-icon')}</span>
1157            <h2
1158              id=${titleId}
1159              ref=${(element) => { windowTitleRefs.current[appId] = element; }}
1160              tabIndex="-1"
1161            >${windowTitle}</h2>
1162            ${isActive && html`<span className="desktop-active-window-label">Active</span>`}
1163          </div>
1164          ${app && html`
1165            <div className="desktop-window-controls">
1166              <button
1167                ref=${(element) => { moveButtonRefs.current[app.id] = element; }}
1168                type="button"
1169                className="desktop-window-move"
1170                aria-label=${`Move ${windowTitle}`}
1171                aria-expanded=${moveControlsAppId === app.id}
1172                onClick=${() => {
1173                  if (moveControlsAppId === app.id) closeMoveControls(app.id);
1174                  else openMoveControls(app);
1175                }}
1176              >
1177                <${Move} aria-hidden="true" />
1178              </button>
1179              <button
1180                type="button"
1181                className="desktop-window-minimize"
1182                onClick=${() => minimizeWindow(app)}
1183                aria-label=${`Minimize ${windowTitle}`}
1184              >
1185                <${Minus} aria-hidden="true" />
1186              </button>
1187              <button
1188                type="button"
1189                className="desktop-window-close"
1190                onClick=${() => closeWindow(app)}
1191                aria-label=${`Close ${windowTitle}`}
1192              >
1193                <${X} aria-hidden="true" />
1194              </button>
1195            </div>
1196          `}
1197        </div>
1198        ${app && moveControlsAppId === app.id && html`
1199          <div
1200            id=${movePanelId}
1201            ref=${(element) => { movePanelRefs.current[app.id] = element; }}
1202            className="desktop-window-move-panel"
1203            role="group"
1204            aria-label=${`Move ${windowTitle} window`}
1205            aria-describedby=${moveInstructionsId}
1206            onKeyDown=${(event) => handleMoveControlsKeyDown(event, app)}
1207          >
1208            <p id=${moveInstructionsId}>Use the arrow buttons or arrow keys. Hold Shift for smaller keyboard steps.</p>
1209            <div className="desktop-window-move-arrows">
1210              <button
1211                type="button"
1212                aria-label=${`Move ${windowTitle} left`}
1213                onClick=${() => moveWindowBy(app, -MOVE_STEP, 0, 'left')}
1214              ><${ArrowLeft} aria-hidden="true" /></button>
1215              <button
1216                type="button"
1217                aria-label=${`Move ${windowTitle} up`}
1218                onClick=${() => moveWindowBy(app, 0, -MOVE_STEP, 'up')}
1219              ><${ArrowUp} aria-hidden="true" /></button>
1220              <button
1221                type="button"
1222                aria-label=${`Move ${windowTitle} down`}
1223                onClick=${() => moveWindowBy(app, 0, MOVE_STEP, 'down')}
1224              ><${ArrowDown} aria-hidden="true" /></button>
1225              <button
1226                type="button"
1227                aria-label=${`Move ${windowTitle} right`}
1228                onClick=${() => moveWindowBy(app, MOVE_STEP, 0, 'right')}
1229              ><${ArrowRight} aria-hidden="true" /></button>
1230            </div>
1231            <button
1232              type="button"
1233              className="desktop-window-center"
1234              aria-label=${`Center ${windowTitle}`}
1235              onClick=${() => commitWindowPosition(app, { x: 0, y: 0 }, `${app.label} centered.`)}
1236            >Center</button>
1237            <button
1238              type="button"
1239              className="desktop-window-move-done"
1240              aria-label=${`Done moving ${windowTitle}`}
1241              onClick=${() => closeMoveControls(app.id)}
1242            >Done</button>
1243          </div>
1244        `}
1245        <div className="desktop-window-body">${renderWindowContent(app)}</div>
1246        <div className="desktop-window-status">
1247          ${windowStatusText(app, isActive)}
1248        </div>
1249      </section>
1250    `;
1251  };
1252
1253  const openWindows = layout.windows
1254    .filter((entry) => !entry.minimized)
1255    .map((entry) =>
1255 getDesktopApp(entry.id))
1256    .filter((app) => app?.availability === 'ready' && app.launch.kind === 'shell');
1257  const taskWindows = layout.windows
1258    .map((entry) => ({ ...entry, app: getDesktopApp(entry.id) }))
1259    .filter((entry) => entry.app?.availability === 'ready' && entry.app.launch.kind === 'shell');
1260
1261  return html`
1262    <main id="main-content" className=${`archive-desktop archive-density--compact ${shellApp ? 'desktop-app-active' : ''}`}>
1263      ${shellApp && html`<h1 className="desktop-mobile-page-title">Archive desktop</h1>`}
1264      <div className="desktop-wallpaper-mark" aria-hidden="true">JR</div>
1265      <div className="desktop-workspace">
1266        <section ref=${shortcutPanelRef} className="desktop-shortcut-panel" aria-labelledby="desktop-title">
1267          <header className="desktop-brand">
1268            <p>Jay Rosen's Internet Archive</p>
1269            <h1 id="desktop-title" ref=${desktopTitleRef} tabIndex="-1">Rosen 98</h1>
1270            <span>Optional exploration view</span>
1271          </header>
1272
1273          <div className="desktop-shortcut-grid" role="list" aria-label="Archive desktop shortcuts">
1274            ${shortcutApps.map((app, index) => html`
1275              <div role="listitem" key=${app.id}>
1276                <button
1277                  ref=${(element) => { shortcutRefs.current[index] = element; }}
1278                  type="button"
1279                  className=${`desktop-shortcut ${shellApp?.id === app.id ? 'is-active' : ''}`}
1280                  tabIndex=${index === shortcutFocusIndex ? 0 : -1}
1281                  aria-current=${shellApp?.id === app.id ? 'page' : undefined}
1282                  aria-label=${`${app.label}. ${app.description} ${launchModeLabel(app)}.`}
1283                  onFocus=${() => setShortcutFocusIndex(index)}
1284                  onClick=${() => openApp(app)}
1285                  onKeyDown=${(event) => handleShortcutKeyDown(event, app, index)}
1286                >
1287                  <span className="desktop-shortcut-icon">${iconFor(app.icon)}</span>
1288                  <span className="desktop-shortcut-label">${app.label}</span>
1289                  <span className="desktop-shortcut-mode">${launchModeLabel(app)}</span>
1290                </button>
1291              </div>
1292            `)}
1293          </div>
1294        </section>
1295
1296        <div ref=${windowStackRef} className="desktop-window-stack">
1297          ${renderWindowFrame(null)}
1298          ${openWindows.map((app) => renderWindowFrame(app))}
1299        </div>
1300      </div>
1301
1302      <p className="desktop-live-status" aria-live="polite" aria-atomic="true">${statusMessage}</p>
1303
1304      ${startOpen && html`
1305        <div
1306          id="archive-desktop-start-menu"
1307          ref=${startMenuRef}
1308          className="desktop-start-menu"
1309          role="menu"
1310          aria-label="Archive desktop Start menu"
1311        >
1312          <div className="desktop-start-rail" aria-hidden="true"><span>Rosen 98</span></div>
1313          <div className="desktop-start-content">
1314            <div className="desktop-start-heading" role="presentation">
1315              <strong>Rosen 98</strong>
1316              <small>Choose a destination</small>
1317            </div>
1318            ${startApps.map((app, index) => html`
1319              <button
1320                key=${app.id}
1321                ref=${(element) => { menuItemRefs.current[index] = element; }}
1322                type="button"
1323                className="desktop-menu-item"
1324                role="menuitem"
1325                tabIndex=${index === menuFocusIndex ? 0 : -1}
1326                onFocus=${() => setMenuFocusIndex(index)}
1327                onClick=${() => openApp(app, 'start-menu')}
1328                onKeyDown=${(event) => handleMenuKeyDown(event, index)}
1329              >
1330                <span className="desktop-menu-icon">${iconFor(app.icon)}</span>
1331                <span><strong>${app.label}</strong><small>${launchModeLabel(app)}</small></span>
1332              </button>
1333            `)}
1334            <div className="desktop-menu-divider" role="separator"></div>
1335            <button
1336              ref=${(element) => { menuItemRefs.current[startApps.length] = element; }}
1337              type="button"
1338              className="desktop-menu-item"
1339              role="menuitem"
1340              tabIndex=${menuFocusIndex === startApps.length ? 0 : -1}
1341              onFocus=${() => setMenuFocusIndex(startApps.length)}
1342              onClick=${resetLayout}
1343              onKeyDown=${(event) => handleMenuKeyDown(event, startApps.length)}
1344            >
1345              <span className="desktop-menu-icon"><${RotateCcw} aria-hidden="true" /></span>
1346              <span><strong>Reset desktop layout</strong><small>Close windows and clear saved state</small></span>
1347            </button>
1348            <button
1349              ref=${(element) => { menuItemRefs.current[startApps.length + 1] = element; }}
1350              type="button"
1351              className="desktop-menu-item desktop-menu-exit"
1352              role="menuitem"
1353              tabIndex=${menuFocusIndex === startApps.length + 1 ? 0 : -1}
1354              onFocus=${() => setMenuFocusIndex(startApps.length + 1)}
1355              onClick=${onExit}
1356              onKeyDown=${(event) => handleMenuKeyDown(event, startApps.length + 1)}
1357            >
1358              <span className="desktop-menu-icon"><${ArrowLeft} aria-hidden="true" /></span>
1359              <span><strong>Standard archive</strong><small>Leave the desktop view</small></span>
1360            </button>
1361          </div>
1362        </div>
1363      `}
1364
1365      <nav className="desktop-taskbar" aria-label="Archive desktop taskbar">
1366        <button
1367          ref=${startButtonRef}
1368          type="button"
1369          className=${`desktop-start-button ${startOpen ? 'is-pressed' : ''}`}
1370          aria-haspopup="menu"
1371          aria-expanded=${startOpen}
1372          onClick=${() => setStartOpen((open) => !open)}
1373        >
1374          <span className="desktop-start-mark" aria-hidden="true">R98</span>
1375          <strong>Start</strong>
1376        </button>
1377
1378        <button type="button" className="desktop-task-button desktop-standard-button" onClick=${onExit}>
1379          <${ArrowLeft} aria-hidden="true" />
1380          <span>Standard archive</span>
1381        </button>
1382
1383        ${taskWindows.length > 0 && html`
1384          <div className="desktop-task-window-list" role="list" aria-label="Open desktop windows" tabIndex="0">
1385            ${taskWindows.map(({ app, minimized }) => html`
1386              <div role="listitem" key=${app.id}>
1387                <button
1388                  ref=${(element) => { taskButtonRefs.current[app.id] = element; }}
1389                  type="button"
1390                  className=${`desktop-task-button ${shellApp?.id === app.id ? 'is-active' : ''} ${minimized ? 'is-minimized' : ''}`}
1391                  aria-current=${shellApp?.id === app.id ? 'page' : undefined}
1392                  aria-label=${`${minimized ? 'Restore' : 'Activate'} ${app.label}${minimized ? ', minimized' : ''}`}
1393                  onClick=${() => {
1394                    setStatusMessage(`${minimized ? 'Restored' : 'Activated'} ${app.label}.`);
1395                    activateWindow(app.id);
1396                  }}
1397                >
1398                  ${iconFor(app.icon, 'desktop-task-icon')}
1399                  <span>${app.label}${minimized ? html`<small>Minimized</small>` : ''}</span>
1400                </button>
1401              </div>
1402            `)}
1403          </div>
1404        `}
1405
1406        <div className="desktop-task-status" aria-hidden="true">
1407          <span>Rosen 98</span>
1408          <small>desktop</small>
1409        </div>
1410      </nav>
1411    </main>
1412  `;
1413};
1414
1415export default DesktopShell;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.