react-os-shell 4.1.5 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -15,8 +15,8 @@ export { ShellAuthProvider, setShellAuthBridge, useShellAuth } from './chunk-ADJ
15
15
  import { EditableGrid } from './chunk-GP4Y3VCB.js';
16
16
  export { EditableGrid } from './chunk-GP4Y3VCB.js';
17
17
  import './chunk-4SQU5YV6.js';
18
- import { APP_VERSION, VERSION } from './chunk-IMKD43E3.js';
19
- export { VERSION } from './chunk-IMKD43E3.js';
18
+ import { APP_VERSION, VERSION } from './chunk-3S5NISAL.js';
19
+ export { VERSION } from './chunk-3S5NISAL.js';
20
20
  import { registerModalEscapeInterceptor, useIsMobile, useWindowManager, PopupMenu, PopupMenuLabel, PopupMenuDivider, PopupMenuItem, WINDOW_REGISTRY, isPageEntry, Modal, useShellPrefs, SIDEBAR_STRIP_W, forgetMaximizedWindowBoxes, ModalActions, useModalActive, client_default, LoadingSpinner, isShellApiClientConfigured, CancelButton, setWindowPosition } from './chunk-URHWXWZE.js';
21
21
  export { CancelButton, ConfirmProvider, CopyButton, DocFavStar, Modal, ModalActions, PopupMenu, PopupMenuDivider, PopupMenuItem, PopupMenuLabel, ShellPrefsProvider, WindowCrashedFallback, WindowErrorBoundary, WindowManagerProvider, WindowTitle, commitExposeHighlight, confirm, confirmDestructive, exitExposeMode, getActiveWindowRoute, getExposeHighlight, getWindowPosition, isEntityEntry, isPageEntry, prompt, registerModalEscapeInterceptor, setExposeHighlight, setShellApiClient, setShellWindowRegistry, setWindowDefaultPosition, setWindowPosition, subscribeExposeHighlight, toggleExposeMode, useLocalStoragePrefs, useModalActive, useShellPrefs, useWidgetSettings, useWindowManager, useWindowMenuItem, useWindowTitle } from './chunk-URHWXWZE.js';
22
22
  import { glassStyle, startMenuCategories, navSections, isSection, GLASS_INPUT_BG, navIcons, isReachable, sectionIcons, visibleChildren } from './chunk-6Q4KYGMH.js';
@@ -506,22 +506,22 @@ function HelpCenter({
506
506
  }
507
507
  )
508
508
  ] }) }),
509
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto py-1", children: loading ? /* @__PURE__ */ jsx("p", { className: "px-3 py-6 text-center text-xs text-gray-400", children: "Loading\u2026" }) : visibleGroups.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-3 py-6 text-center text-xs text-gray-500", children: q ? "No articles match your search." : emptyMessage }) : visibleGroups.map((group) => {
510
- const open = isGroupOpen(group.key);
509
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto py-1", children: loading ? /* @__PURE__ */ jsx("p", { className: "px-3 py-6 text-center text-xs text-gray-400", children: "Loading\u2026" }) : visibleGroups.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-3 py-6 text-center text-xs text-gray-500", children: q ? "No articles match your search." : emptyMessage }) : visibleGroups.map((group2) => {
510
+ const open = isGroupOpen(group2.key);
511
511
  return /* @__PURE__ */ jsxs("div", { className: "py-0.5", children: [
512
512
  /* @__PURE__ */ jsxs(
513
513
  "button",
514
514
  {
515
515
  type: "button",
516
- onClick: () => toggleGroup(group.key),
516
+ onClick: () => toggleGroup(group2.key),
517
517
  className: "w-full flex items-center gap-1.5 px-3 py-1.5 text-left text-[10px] font-semibold uppercase tracking-wide text-gray-400 hover:text-gray-600",
518
518
  children: [
519
519
  /* @__PURE__ */ jsx(Chevron, { open }),
520
- /* @__PURE__ */ jsx("span", { className: "truncate", children: group.label })
520
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: group2.label })
521
521
  ]
522
522
  }
523
523
  ),
524
- open && group.docs.map((doc) => {
524
+ open && group2.docs.map((doc) => {
525
525
  const active = selected?.slug === doc.slug;
526
526
  return /* @__PURE__ */ jsxs(
527
527
  "button",
@@ -537,7 +537,7 @@ function HelpCenter({
537
537
  doc.slug
538
538
  );
539
539
  })
540
- ] }, group.key);
540
+ ] }, group2.key);
541
541
  }) })
542
542
  ] }),
543
543
  /* @__PURE__ */ jsx("main", { className: "flex-1 min-w-0 bg-white rounded-lg shadow overflow-hidden flex flex-col", children: selected ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1183,8 +1183,8 @@ function StatusBadgeProvider({
1183
1183
  }
1184
1184
  function StatusBadge({ status }) {
1185
1185
  const groups = useContext(StatusGroupsContext);
1186
- const group = groups[status] ?? "neutral";
1187
- const color = GROUP_COLORS[group];
1186
+ const group2 = groups[status] ?? "neutral";
1187
+ const color = GROUP_COLORS[group2];
1188
1188
  const label = status.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1189
1189
  return /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${color}`, children: label });
1190
1190
  }
@@ -1370,6 +1370,437 @@ function WidgetManager({ open, onClose }) {
1370
1370
  ] }) });
1371
1371
  }
1372
1372
 
1373
+ // src/shell/perfVerdict.ts
1374
+ var SMOOTH_FPS = 50;
1375
+ var BLOCKED_PCT_CPU = 20;
1376
+ var CPU_BOUND = {
1377
+ kind: "cpu",
1378
+ label: "CPU-bound (JavaScript)",
1379
+ detail: "The main thread is blocked. Rendering settings will not help \u2014 this is script or data work."
1380
+ };
1381
+ function summariseFrames(timestamps) {
1382
+ if (timestamps.length < 2) return { fps: 0, frameMs: 0, worstMs: 0 };
1383
+ let worstMs = 0;
1384
+ for (let i = 1; i < timestamps.length; i++) {
1385
+ const delta = timestamps[i] - timestamps[i - 1];
1386
+ if (delta > worstMs) worstMs = delta;
1387
+ }
1388
+ const spanMs = timestamps[timestamps.length - 1] - timestamps[0];
1389
+ if (spanMs <= 0) return { fps: 0, frameMs: 0, worstMs: 0 };
1390
+ const intervals = timestamps.length - 1;
1391
+ return {
1392
+ fps: intervals / spanMs * 1e3,
1393
+ frameMs: spanMs / intervals,
1394
+ worstMs
1395
+ };
1396
+ }
1397
+ function classify(reading) {
1398
+ const { fps, blockedPct } = reading;
1399
+ const threadBlocked = blockedPct !== null && blockedPct >= BLOCKED_PCT_CPU;
1400
+ if (!Number.isFinite(fps) || fps <= 0) {
1401
+ if (threadBlocked) return CPU_BOUND;
1402
+ return { kind: "unknown", label: "Measuring\u2026", detail: "Collecting frame timings." };
1403
+ }
1404
+ if (fps >= SMOOTH_FPS) {
1405
+ return {
1406
+ kind: "smooth",
1407
+ label: "Smooth",
1408
+ detail: "Frames are landing on time. No rendering bottleneck here."
1409
+ };
1410
+ }
1411
+ if (blockedPct === null) {
1412
+ return {
1413
+ kind: "unknown",
1414
+ label: "Janky \u2014 cause unknown",
1415
+ detail: "This browser reports no long-task timing, so GPU and JS cannot be told apart. Try Chrome or Edge."
1416
+ };
1417
+ }
1418
+ if (threadBlocked) return CPU_BOUND;
1419
+ return {
1420
+ kind: "gpu",
1421
+ label: "GPU-bound (compositing)",
1422
+ detail: "Frames are late while the main thread sits idle. Try Preferences \u2192 Customization \u2192 Reduce transparency."
1423
+ };
1424
+ }
1425
+
1426
+ // src/shell/perfLog.ts
1427
+ var LOG_CAP = 2400;
1428
+ var MIN_GROUP_SAMPLES = 4;
1429
+ function appendRecord(log, record, cap = LOG_CAP) {
1430
+ const next = log.length >= cap ? log.slice(log.length - cap + 1) : log.slice();
1431
+ next.push(record);
1432
+ return next;
1433
+ }
1434
+ function isInteracting(r) {
1435
+ return r.clicks > 0 || r.keys > 0 || r.scrolls > 0 || r.dragMs > 0;
1436
+ }
1437
+ function median(values) {
1438
+ if (!values.length) return 0;
1439
+ const sorted = values.slice().sort((a, b) => a - b);
1440
+ const mid = Math.floor(sorted.length / 2);
1441
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
1442
+ }
1443
+ function group(records) {
1444
+ return { samples: records.length, medianFps: median(records.map((r) => r.fps)) };
1445
+ }
1446
+ function summariseLog(log) {
1447
+ const empty = {
1448
+ samples: 0,
1449
+ durationMs: 0,
1450
+ medianFps: 0,
1451
+ worstFrameMs: 0,
1452
+ verdictShare: { smooth: 0, gpu: 0, cpu: 0, unknown: 0 },
1453
+ interacting: null,
1454
+ idle: null,
1455
+ byWindowCount: [],
1456
+ worstWindows: []
1457
+ };
1458
+ if (!log.length) return empty;
1459
+ const measured = log.filter((r) => r.fps > 0);
1460
+ const verdictShare = { ...empty.verdictShare };
1461
+ for (const r of log) verdictShare[r.verdict] += 1 / log.length;
1462
+ const interacting = measured.filter(isInteracting);
1463
+ const idle = measured.filter((r) => !isInteracting(r));
1464
+ const byWindows = /* @__PURE__ */ new Map();
1465
+ const byKey = /* @__PURE__ */ new Map();
1466
+ for (const r of measured) {
1467
+ const bucket = byWindows.get(r.windows);
1468
+ if (bucket) bucket.push(r);
1469
+ else byWindows.set(r.windows, [r]);
1470
+ if (r.active) {
1471
+ const keyed = byKey.get(r.active);
1472
+ if (keyed) keyed.push(r);
1473
+ else byKey.set(r.active, [r]);
1474
+ }
1475
+ }
1476
+ return {
1477
+ samples: log.length,
1478
+ durationMs: log[log.length - 1].t - log[0].t,
1479
+ medianFps: median(measured.map((r) => r.fps)),
1480
+ worstFrameMs: log.reduce((worst, r) => r.worstMs > worst ? r.worstMs : worst, 0),
1481
+ verdictShare,
1482
+ interacting: interacting.length >= MIN_GROUP_SAMPLES ? group(interacting) : null,
1483
+ idle: idle.length >= MIN_GROUP_SAMPLES ? group(idle) : null,
1484
+ byWindowCount: [...byWindows.entries()].filter(([, rs]) => rs.length >= MIN_GROUP_SAMPLES).map(([windows, rs]) => ({ windows, ...group(rs) })).sort((a, b) => a.windows - b.windows),
1485
+ worstWindows: [...byKey.entries()].filter(([, rs]) => rs.length >= MIN_GROUP_SAMPLES).map(([key, rs]) => ({ key, ...group(rs) })).sort((a, b) => a.medianFps - b.medianFps)
1486
+ };
1487
+ }
1488
+ var CSV_COLUMNS = [
1489
+ "t",
1490
+ "fps",
1491
+ "frameMs",
1492
+ "worstMs",
1493
+ "blockedPct",
1494
+ "heapMB",
1495
+ "verdict",
1496
+ "windows",
1497
+ "active",
1498
+ "clicks",
1499
+ "keys",
1500
+ "scrolls",
1501
+ "dragMs"
1502
+ ];
1503
+ function toCsv(log) {
1504
+ const cell = (value) => {
1505
+ if (value === null || value === void 0) return "";
1506
+ const text = typeof value === "number" ? String(Math.round(value * 10) / 10) : String(value);
1507
+ return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
1508
+ };
1509
+ return [
1510
+ CSV_COLUMNS.join(","),
1511
+ ...log.map((r) => CSV_COLUMNS.map((c) => cell(r[c])).join(","))
1512
+ ].join("\n");
1513
+ }
1514
+ var FLUSH_MS = 500;
1515
+ var WINDOW_MS = 1e3;
1516
+ var HISTORY = 40;
1517
+ var PERSIST_EVERY_MS = 1e4;
1518
+ var LOG_STORAGE_KEY = "react-os-shell:perf-log";
1519
+ var VERDICT_STYLES = {
1520
+ smooth: { dot: "bg-emerald-400", text: "text-emerald-300" },
1521
+ gpu: { dot: "bg-amber-400", text: "text-amber-300" },
1522
+ cpu: { dot: "bg-sky-400", text: "text-sky-300" },
1523
+ unknown: { dot: "bg-gray-400", text: "text-gray-300" }
1524
+ };
1525
+ function readWindows() {
1526
+ if (typeof document === "undefined") return { windows: 0, active: null };
1527
+ const panels = document.querySelectorAll("[data-modal-panel]");
1528
+ let active = null;
1529
+ let topZ = -Infinity;
1530
+ panels.forEach((panel) => {
1531
+ const z = parseInt(window.getComputedStyle(panel).zIndex, 10);
1532
+ const rank = Number.isFinite(z) ? z : 0;
1533
+ if (rank >= topZ) {
1534
+ topZ = rank;
1535
+ active = panel.dataset.windowKey ?? panel.dataset.modalId ?? null;
1536
+ }
1537
+ });
1538
+ return { windows: panels.length, active };
1539
+ }
1540
+ function PerfStats({ onClose, className = "" }) {
1541
+ const framesRef = useRef([]);
1542
+ const blockedMsRef = useRef(0);
1543
+ const activityRef = useRef({ clicks: 0, keys: 0, scrolls: 0, dragMs: 0 });
1544
+ const dragRef = useRef({ down: false, since: 0 });
1545
+ const logRef = useRef([]);
1546
+ const startedAtRef = useRef(0);
1547
+ const persistedAtRef = useRef(0);
1548
+ const [longTaskSupported, setLongTaskSupported] = useState(false);
1549
+ const [reading, setReading] = useState({ fps: 0, frameMs: 0, worstMs: 0, blockedPct: null });
1550
+ const [history, setHistory] = useState([]);
1551
+ const [heap, setHeap] = useState(null);
1552
+ const [logSize, setLogSize] = useState(0);
1553
+ useEffect(() => {
1554
+ let raf = 0;
1555
+ const tick = (now) => {
1556
+ const frames = framesRef.current;
1557
+ frames.push(now);
1558
+ const cutoff = now - WINDOW_MS;
1559
+ while (frames.length && frames[0] < cutoff) frames.shift();
1560
+ raf = requestAnimationFrame(tick);
1561
+ };
1562
+ raf = requestAnimationFrame(tick);
1563
+ return () => cancelAnimationFrame(raf);
1564
+ }, []);
1565
+ useEffect(() => {
1566
+ if (typeof PerformanceObserver === "undefined") return;
1567
+ let observer;
1568
+ try {
1569
+ observer = new PerformanceObserver((list) => {
1570
+ for (const entry of list.getEntries()) blockedMsRef.current += entry.duration;
1571
+ });
1572
+ observer.observe({ entryTypes: ["longtask"] });
1573
+ setLongTaskSupported(true);
1574
+ } catch {
1575
+ return;
1576
+ }
1577
+ return () => observer?.disconnect();
1578
+ }, []);
1579
+ useEffect(() => {
1580
+ const onPointerDown = () => {
1581
+ activityRef.current.clicks++;
1582
+ dragRef.current = { down: true, since: performance.now() };
1583
+ };
1584
+ const onPointerMove = () => {
1585
+ if (!dragRef.current.down) return;
1586
+ const now = performance.now();
1587
+ activityRef.current.dragMs += now - dragRef.current.since;
1588
+ dragRef.current.since = now;
1589
+ };
1590
+ const onPointerUp = () => {
1591
+ dragRef.current.down = false;
1592
+ };
1593
+ const onKeyDown = () => {
1594
+ activityRef.current.keys++;
1595
+ };
1596
+ const onWheel = () => {
1597
+ activityRef.current.scrolls++;
1598
+ };
1599
+ const opts = { capture: true, passive: true };
1600
+ window.addEventListener("pointerdown", onPointerDown, opts);
1601
+ window.addEventListener("pointermove", onPointerMove, opts);
1602
+ window.addEventListener("pointerup", onPointerUp, opts);
1603
+ window.addEventListener("pointercancel", onPointerUp, opts);
1604
+ window.addEventListener("keydown", onKeyDown, opts);
1605
+ window.addEventListener("wheel", onWheel, opts);
1606
+ return () => {
1607
+ window.removeEventListener("pointerdown", onPointerDown, opts);
1608
+ window.removeEventListener("pointermove", onPointerMove, opts);
1609
+ window.removeEventListener("pointerup", onPointerUp, opts);
1610
+ window.removeEventListener("pointercancel", onPointerUp, opts);
1611
+ window.removeEventListener("keydown", onKeyDown, opts);
1612
+ window.removeEventListener("wheel", onWheel, opts);
1613
+ };
1614
+ }, []);
1615
+ const persist = useCallback(() => {
1616
+ try {
1617
+ localStorage.setItem(LOG_STORAGE_KEY, JSON.stringify(logRef.current));
1618
+ } catch {
1619
+ }
1620
+ }, []);
1621
+ useEffect(() => {
1622
+ const onHide = () => persist();
1623
+ window.addEventListener("pagehide", onHide);
1624
+ document.addEventListener("visibilitychange", onHide);
1625
+ return () => {
1626
+ window.removeEventListener("pagehide", onHide);
1627
+ document.removeEventListener("visibilitychange", onHide);
1628
+ persist();
1629
+ };
1630
+ }, [persist]);
1631
+ useEffect(() => {
1632
+ const id = setInterval(() => {
1633
+ const now = performance.now();
1634
+ if (!startedAtRef.current) startedAtRef.current = now;
1635
+ const frames = summariseFrames(framesRef.current);
1636
+ const blockedPct = longTaskSupported ? Math.min(100, blockedMsRef.current / FLUSH_MS * 100) : null;
1637
+ blockedMsRef.current = 0;
1638
+ const memory = performance.memory;
1639
+ const heapMB = memory ? memory.usedJSHeapSize / 1048576 : null;
1640
+ const nextReading = { ...frames, blockedPct };
1641
+ const activity = activityRef.current;
1642
+ const { windows, active } = readWindows();
1643
+ logRef.current = appendRecord(logRef.current, {
1644
+ t: Math.round(now - startedAtRef.current),
1645
+ ...frames,
1646
+ blockedPct,
1647
+ heapMB,
1648
+ verdict: classify(nextReading).kind,
1649
+ windows,
1650
+ active,
1651
+ clicks: activity.clicks,
1652
+ keys: activity.keys,
1653
+ scrolls: activity.scrolls,
1654
+ dragMs: Math.round(activity.dragMs)
1655
+ });
1656
+ activity.clicks = 0;
1657
+ activity.keys = 0;
1658
+ activity.scrolls = 0;
1659
+ activity.dragMs = 0;
1660
+ if (!persistedAtRef.current || now - persistedAtRef.current >= PERSIST_EVERY_MS) {
1661
+ persistedAtRef.current = now;
1662
+ persist();
1663
+ }
1664
+ setReading(nextReading);
1665
+ setHistory((prev) => [...prev, frames.fps].slice(-HISTORY));
1666
+ setHeap(memory ? { usedMB: memory.usedJSHeapSize / 1048576, limitMB: memory.jsHeapSizeLimit / 1048576 } : null);
1667
+ setLogSize(logRef.current.length);
1668
+ }, FLUSH_MS);
1669
+ return () => clearInterval(id);
1670
+ }, [longTaskSupported, persist]);
1671
+ const download = useCallback((body, type, extension) => {
1672
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1673
+ const url = URL.createObjectURL(new Blob([body], { type }));
1674
+ const link = document.createElement("a");
1675
+ link.href = url;
1676
+ link.download = `perf-log-${stamp}.${extension}`;
1677
+ link.click();
1678
+ URL.revokeObjectURL(url);
1679
+ }, []);
1680
+ const exportJson = useCallback(() => {
1681
+ download(
1682
+ JSON.stringify(
1683
+ {
1684
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1685
+ userAgent: navigator.userAgent,
1686
+ screen: { width: window.screen.width, height: window.screen.height, dpr: window.devicePixelRatio },
1687
+ reducedTransparency: document.documentElement.classList.contains("rosh-reduce-transparency"),
1688
+ summary: summariseLog(logRef.current),
1689
+ records: logRef.current
1690
+ },
1691
+ null,
1692
+ 2
1693
+ ),
1694
+ "application/json",
1695
+ "json"
1696
+ );
1697
+ }, [download]);
1698
+ const exportCsv = useCallback(() => download(toCsv(logRef.current), "text/csv", "csv"), [download]);
1699
+ const verdict = classify(reading);
1700
+ const styles = VERDICT_STYLES[verdict.kind];
1701
+ const peak = Math.max(60, ...history);
1702
+ const loggedSeconds = Math.round(logSize * FLUSH_MS / 1e3);
1703
+ return /* @__PURE__ */ jsxs(
1704
+ "div",
1705
+ {
1706
+ className: `w-56 rounded-lg border border-white/15 bg-[#11111b]/95 px-3 py-2 font-mono text-[10px] text-white/80 shadow-lg select-none ${className}`,
1707
+ role: "status",
1708
+ "aria-live": "off",
1709
+ "aria-label": "Performance statistics",
1710
+ children: [
1711
+ /* @__PURE__ */ jsxs("div", { className: "mb-1.5 flex items-center justify-between", children: [
1712
+ /* @__PURE__ */ jsx("span", { className: "text-[9px] uppercase tracking-wider text-white/40", children: "Performance" }),
1713
+ onClose && /* @__PURE__ */ jsx(
1714
+ "button",
1715
+ {
1716
+ onClick: onClose,
1717
+ className: "-mr-1 px-1 leading-none text-white/40 transition-colors hover:text-white/90",
1718
+ "aria-label": "Hide performance statistics",
1719
+ children: "\xD7"
1720
+ }
1721
+ )
1722
+ ] }),
1723
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between", children: [
1724
+ /* @__PURE__ */ jsxs("span", { className: `text-base font-semibold tabular-nums ${styles.text}`, children: [
1725
+ reading.fps ? reading.fps.toFixed(0) : "\u2014",
1726
+ /* @__PURE__ */ jsx("span", { className: "ml-1 text-[10px] font-normal text-white/40", children: "fps" })
1727
+ ] }),
1728
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums text-white/50", children: reading.frameMs ? `${reading.frameMs.toFixed(1)} ms` : "\u2014" })
1729
+ ] }),
1730
+ /* @__PURE__ */ jsx("div", { className: "mt-1.5 flex h-5 items-end gap-px", "aria-hidden": "true", children: Array.from({ length: HISTORY }, (_, i) => {
1731
+ const value = history[history.length - HISTORY + i];
1732
+ return /* @__PURE__ */ jsx(
1733
+ "div",
1734
+ {
1735
+ className: `flex-1 rounded-sm ${value === void 0 ? "bg-white/5" : styles.dot}`,
1736
+ style: { height: value === void 0 ? "2px" : `${Math.max(6, value / peak * 100)}%` }
1737
+ },
1738
+ i
1739
+ );
1740
+ }) }),
1741
+ /* @__PURE__ */ jsxs("dl", { className: "mt-2 space-y-0.5", children: [
1742
+ /* @__PURE__ */ jsx(Row, { label: "worst frame", value: reading.worstMs ? `${reading.worstMs.toFixed(0)} ms` : "\u2014" }),
1743
+ /* @__PURE__ */ jsx(
1744
+ Row,
1745
+ {
1746
+ label: "main thread",
1747
+ value: reading.blockedPct === null ? "n/a" : `${reading.blockedPct.toFixed(0)}% blocked`
1748
+ }
1749
+ ),
1750
+ /* @__PURE__ */ jsx(
1751
+ Row,
1752
+ {
1753
+ label: "JS heap",
1754
+ value: heap ? `${heap.usedMB.toFixed(0)} / ${heap.limitMB.toFixed(0)} MB` : "n/a"
1755
+ }
1756
+ )
1757
+ ] }),
1758
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 border-t border-white/10 pt-1.5", children: [
1759
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
1760
+ /* @__PURE__ */ jsx("span", { className: `h-1.5 w-1.5 shrink-0 rounded-full ${styles.dot}`, "aria-hidden": "true" }),
1761
+ /* @__PURE__ */ jsx("span", { className: `font-semibold ${styles.text}`, children: verdict.label })
1762
+ ] }),
1763
+ /* @__PURE__ */ jsx("p", { className: "mt-0.5 leading-snug text-white/45", children: verdict.detail })
1764
+ ] }),
1765
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center justify-between border-t border-white/10 pt-1.5", children: [
1766
+ /* @__PURE__ */ jsxs("span", { className: "text-white/35", children: [
1767
+ "log ",
1768
+ logSize,
1769
+ " \xB7 ",
1770
+ loggedSeconds < 60 ? `${loggedSeconds}s` : `${Math.round(loggedSeconds / 60)}m`
1771
+ ] }),
1772
+ /* @__PURE__ */ jsxs("span", { className: "flex gap-1", children: [
1773
+ /* @__PURE__ */ jsx(
1774
+ "button",
1775
+ {
1776
+ onClick: exportJson,
1777
+ disabled: !logSize,
1778
+ className: "rounded border border-white/15 px-1.5 py-0.5 text-white/60 transition-colors hover:border-white/30 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
1779
+ children: "JSON"
1780
+ }
1781
+ ),
1782
+ /* @__PURE__ */ jsx(
1783
+ "button",
1784
+ {
1785
+ onClick: exportCsv,
1786
+ disabled: !logSize,
1787
+ className: "rounded border border-white/15 px-1.5 py-0.5 text-white/60 transition-colors hover:border-white/30 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
1788
+ children: "CSV"
1789
+ }
1790
+ )
1791
+ ] })
1792
+ ] })
1793
+ ]
1794
+ }
1795
+ );
1796
+ }
1797
+ function Row({ label, value }) {
1798
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
1799
+ /* @__PURE__ */ jsx("dt", { className: "text-white/40", children: label }),
1800
+ /* @__PURE__ */ jsx("dd", { className: "tabular-nums text-white/70", children: value })
1801
+ ] });
1802
+ }
1803
+
1373
1804
  // src/changelog.ts
1374
1805
  var changelog = [];
1375
1806
  var changelog_default = changelog;
@@ -2431,6 +2862,13 @@ function Desktop({ profile }) {
2431
2862
  ] }),
2432
2863
  "Manage Widgets\u2026"
2433
2864
  ] }),
2865
+ !hiddenMenuItems.includes("perf-stats") && /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: () => {
2866
+ setContextMenu(null);
2867
+ saveShellPrefs({ show_desktop_stats: prefs.show_desktop_stats !== true });
2868
+ }, children: [
2869
+ /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-emerald-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3.75 20.25V15m5.25 5.25V9.75m5.25 10.5V4.5m5.25 15.75v-7.5" }) }),
2870
+ prefs.show_desktop_stats === true ? "Hide Performance Stats" : "Show Performance Stats"
2871
+ ] }),
2434
2872
  !(hiddenMenuItems.includes("customization") && hiddenMenuItems.includes("favorites") && hiddenMenuItems.includes("about")) && /* @__PURE__ */ jsx(PopupMenuDivider, {}),
2435
2873
  !hiddenMenuItems.includes("customization") && /* @__PURE__ */ jsxs(PopupMenuItem, { onClick: () => {
2436
2874
  setContextMenu(null);
@@ -2539,6 +2977,13 @@ function Desktop({ profile }) {
2539
2977
  ] })
2540
2978
  ] }) });
2541
2979
  })(),
2980
+ prefs.show_desktop_stats === true && (() => {
2981
+ const versionShowing = prefs.show_desktop_version === true && !!(host.productVersion ?? APP_VERSION);
2982
+ const taskbarAtBottom = prefs.taskbar_position !== "top" && prefs.taskbar_position !== "left" && prefs.taskbar_position !== "right";
2983
+ const side = prefs.taskbar_position === "right" ? "left-3" : "right-3";
2984
+ const bottom = taskbarAtBottom ? versionShowing ? "bottom-24" : "bottom-16" : versionShowing ? "bottom-8" : "bottom-3";
2985
+ return /* @__PURE__ */ jsx("div", { className: `absolute z-[240] ${side} ${bottom}`, children: /* @__PURE__ */ jsx(PerfStats, { onClose: () => saveShellPrefs({ show_desktop_stats: false }) }) });
2986
+ })(),
2542
2987
  prefs.show_desktop_version === true && (host.productVersion ?? APP_VERSION) && /* @__PURE__ */ jsx(
2543
2988
  "button",
2544
2989
  {
@@ -4063,7 +4508,7 @@ function previewColor(resolved, light, dark, pink, green, grey, blue) {
4063
4508
  }
4064
4509
  function Customization({ omit, section } = {}) {
4065
4510
  const omitSet = new Set(omit ?? []);
4066
- const inSection = (group) => section === void 0 || section === group;
4511
+ const inSection = (group2) => section === void 0 || section === group2;
4067
4512
  const host = useDesktopHost();
4068
4513
  const WALLPAPERS = host.wallpapers && host.wallpapers.length > 0 ? host.wallpapers : DEFAULT_WALLPAPERS;
4069
4514
  const { prefs, save } = useShellPrefs();
@@ -4375,6 +4820,25 @@ function Customization({ omit, section } = {}) {
4375
4820
  })
4376
4821
  }
4377
4822
  )
4823
+ ] }),
4824
+ /* @__PURE__ */ jsxs("div", { children: [
4825
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-900 mb-3", children: "Diagnostics" }),
4826
+ /* @__PURE__ */ jsxs("label", { className: "flex items-start gap-2 cursor-pointer", children: [
4827
+ /* @__PURE__ */ jsx(
4828
+ "input",
4829
+ {
4830
+ type: "checkbox",
4831
+ checked: !!pref("show_desktop_stats"),
4832
+ onChange: (e) => savePref("show_desktop_stats", e.target.checked),
4833
+ className: "h-4 w-4 mt-0.5 rounded border-gray-300 text-blue-600"
4834
+ }
4835
+ ),
4836
+ /* @__PURE__ */ jsxs("span", { className: "text-sm text-gray-700", children: [
4837
+ "Show performance stats",
4838
+ /* @__PURE__ */ jsx("span", { className: "block text-xs text-gray-400", children: "Overlays frame rate, worst frame time and a bottleneck reading on the desktop, beside the version number. Turn it on to check whether Reduce transparency actually helps on this machine \u2014 or whether the slowness is coming from somewhere else." }),
4839
+ /* @__PURE__ */ jsx("span", { className: "block text-xs text-gray-400 mt-1", children: "While it is on, each reading is recorded with what was happening at the time \u2014 how many windows were open, which one was on top, and counts of clicks, keystrokes, scrolls and window drags. No page content or keystroke text is recorded. The log stays on this device and is only shared if you export it with the JSON or CSV button on the overlay." })
4840
+ ] })
4841
+ ] })
4378
4842
  ] })
4379
4843
  ] }),
4380
4844
  inSection("layout") && /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -4614,7 +5078,7 @@ function BehaviorPanel() {
4614
5078
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-900 mb-3", children: "Behavior" }),
4615
5079
  /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
4616
5080
  /* @__PURE__ */ jsx(
4617
- Row,
5081
+ Row2,
4618
5082
  {
4619
5083
  label: "New window position",
4620
5084
  options: WINDOW_POSITIONS,
@@ -4623,7 +5087,7 @@ function BehaviorPanel() {
4623
5087
  }
4624
5088
  ),
4625
5089
  /* @__PURE__ */ jsx(
4626
- Row,
5090
+ Row2,
4627
5091
  {
4628
5092
  label: "Double-click desktop",
4629
5093
  options: DESKTOP_DBLCLICK,
@@ -4632,7 +5096,7 @@ function BehaviorPanel() {
4632
5096
  }
4633
5097
  ),
4634
5098
  /* @__PURE__ */ jsx(
4635
- Row,
5099
+ Row2,
4636
5100
  {
4637
5101
  label: "Default window size",
4638
5102
  options: WINDOW_SIZES,
@@ -4673,7 +5137,7 @@ function BehaviorPanel() {
4673
5137
  /* @__PURE__ */ jsx(ModalActions, { children: /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400", children: "Changes are saved automatically" }) })
4674
5138
  ] });
4675
5139
  }
4676
- function Row({
5140
+ function Row2({
4677
5141
  label,
4678
5142
  options,
4679
5143
  value,
@@ -10003,6 +10467,6 @@ function useEditHotkey(callback) {
10003
10467
  }, [callback, isActive]);
10004
10468
  }
10005
10469
 
10006
- export { ALT, ALT_SHIFT_D, ALT_SHIFT_E, ALT_SHIFT_N, Accordion, AuthScreen, Avatar, AvatarGroup, Banner, BarChart, BehaviorPanel, BulkImportGrid, Button_default as Button, CMD_A, CMD_DOT, CMD_ENTER, CMD_K, CMD_S, Card, ChangePasswordForm, ChatTemplate, Checkbox_default as Checkbox, CheckoutTemplate, ColoredBadge, ContainerFillChart, Customization, DEV_BANNER_TEXT, DashboardTemplate, DataTablePage, DateRangePicker, Desktop, DesktopHostProvider, DevIndicator, DonutChart, ENTER, EmailTemplate, EmptyState, EntityList, ErrorPage, FilterBar, FormField, FormLayoutPage, GalleryTemplate, GlobalSearch, HelpCenter, INPUT_BASE, Input_default as Input, Kanban, Label, Layout, ListFooter, ListLoadError, LoadingSpinner2 as LoadingSpinner, MOD, Markdown, MediaUploadField, MediaUploadGrid, MetricBar, MilestoneTimeline, NativeSelect, NotificationBell, PageHeader, Pagination, PdfActionButton, Radio_default as Radio, ResizableTable, SHIFT, SearchableSelect, Select_default as Select, ServerStatusIndicator, ShellEntityFetcherProvider, ShortcutHelp, SidebarActionButton, SidebarGroupLabel, SidebarNavItem, SoundsPanel, Sparkline, StartMenu, StatCard, StatusBadge, StatusBadgeProvider, SystemPreferences, Tabs, Textarea_default as Textarea, Tooltip, TopNav, WidgetManager, applyDevTitle, createWindowRegistry, findDuplicateKeys, formatDate, inputClasses, isDevEnv, isMac, isSeverityTone, mediaFileName, mergeBulkItems, severityOf, toISODate2 as toISODate, useClickOutside, useColumnConfig, useDesktopHost, useEditHotkey, useFilters, useInfiniteScroll, useNewHotkey, useShellEntityFetcher, useSort, useTableNav };
10470
+ export { ALT, ALT_SHIFT_D, ALT_SHIFT_E, ALT_SHIFT_N, Accordion, AuthScreen, Avatar, AvatarGroup, BLOCKED_PCT_CPU, Banner, BarChart, BehaviorPanel, BulkImportGrid, Button_default as Button, CMD_A, CMD_DOT, CMD_ENTER, CMD_K, CMD_S, Card, ChangePasswordForm, ChatTemplate, Checkbox_default as Checkbox, CheckoutTemplate, ColoredBadge, ContainerFillChart, Customization, DEV_BANNER_TEXT, DashboardTemplate, DataTablePage, DateRangePicker, Desktop, DesktopHostProvider, DevIndicator, DonutChart, ENTER, EmailTemplate, EmptyState, EntityList, ErrorPage, FilterBar, FormField, FormLayoutPage, GalleryTemplate, GlobalSearch, HelpCenter, INPUT_BASE, Input_default as Input, Kanban, LOG_CAP, Label, Layout, ListFooter, ListLoadError, LoadingSpinner2 as LoadingSpinner, MIN_GROUP_SAMPLES, MOD, Markdown, MediaUploadField, MediaUploadGrid, MetricBar, MilestoneTimeline, NativeSelect, NotificationBell, PageHeader, Pagination, PdfActionButton, PerfStats, Radio_default as Radio, ResizableTable, SHIFT, SMOOTH_FPS, SearchableSelect, Select_default as Select, ServerStatusIndicator, ShellEntityFetcherProvider, ShortcutHelp, SidebarActionButton, SidebarGroupLabel, SidebarNavItem, SoundsPanel, Sparkline, StartMenu, StatCard, StatusBadge, StatusBadgeProvider, SystemPreferences, Tabs, Textarea_default as Textarea, Tooltip, TopNav, WidgetManager, appendRecord as appendPerfRecord, applyDevTitle, classify as classifyPerf, createWindowRegistry, findDuplicateKeys, formatDate, inputClasses, isDevEnv, isInteracting, isMac, isSeverityTone, mediaFileName, mergeBulkItems, toCsv as perfLogToCsv, severityOf, summariseFrames, summariseLog as summarisePerfLog, toISODate2 as toISODate, useClickOutside, useColumnConfig, useDesktopHost, useEditHotkey, useFilters, useInfiniteScroll, useNewHotkey, useShellEntityFetcher, useSort, useTableNav };
10007
10471
  //# sourceMappingURL=index.js.map
10008
10472
  //# sourceMappingURL=index.js.map