@peers-app/peers-ui 0.19.12 → 0.19.13

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.
Files changed (53) hide show
  1. package/README.md +7 -2
  2. package/dist/components/voice-playback.d.ts +15 -0
  3. package/dist/components/voice-playback.js +86 -0
  4. package/dist/components/voice-subscribe-events.d.ts +0 -4
  5. package/dist/index.d.ts +10 -2
  6. package/dist/index.js +23 -2
  7. package/dist/operator-console/operator-console-state.d.ts +49 -0
  8. package/dist/operator-console/operator-console-state.js +97 -0
  9. package/dist/operator-console/operator-console.d.ts +32 -0
  10. package/dist/operator-console/operator-console.js +88 -0
  11. package/dist/root-layout/global-overlays.d.ts +37 -0
  12. package/dist/root-layout/global-overlays.js +37 -0
  13. package/dist/root-layout/layout-picker.d.ts +9 -0
  14. package/dist/root-layout/layout-picker.js +22 -0
  15. package/dist/root-layout/layouts/full-screen-layout.d.ts +12 -0
  16. package/dist/root-layout/layouts/full-screen-layout.js +60 -0
  17. package/dist/root-layout/layouts/tabs-with-console-layout.d.ts +20 -0
  18. package/dist/root-layout/layouts/tabs-with-console-layout.js +158 -0
  19. package/dist/root-layout/root-layout-host.d.ts +19 -0
  20. package/dist/root-layout/root-layout-host.js +88 -0
  21. package/dist/root-layout/root-layout-registry.d.ts +15 -0
  22. package/dist/root-layout/root-layout-registry.js +50 -0
  23. package/dist/root-layout/root-layout-state.d.ts +23 -0
  24. package/dist/root-layout/root-layout-state.js +54 -0
  25. package/dist/root-layout/root-layout.d.ts +30 -0
  26. package/dist/root-layout/root-layout.js +12 -0
  27. package/dist/screens/network-viewer/connection-troubleshooter.js +7 -11
  28. package/dist/screens/settings/settings-page.js +2 -1
  29. package/dist/tabs-layout/tabs-layout.d.ts +48 -4
  30. package/dist/tabs-layout/tabs-layout.js +27 -53
  31. package/dist/tabs-layout/tabs-state.d.ts +10 -0
  32. package/dist/tabs-layout/tabs-state.js +33 -1
  33. package/package.json +3 -3
  34. package/src/components/voice-playback.tsx +97 -0
  35. package/src/components/voice-subscribe-events.ts +0 -4
  36. package/src/index.tsx +24 -2
  37. package/src/operator-console/operator-console-state.ts +119 -0
  38. package/src/operator-console/operator-console.tsx +233 -0
  39. package/src/root-layout/global-overlays.ts +50 -0
  40. package/src/root-layout/layout-picker.tsx +47 -0
  41. package/src/root-layout/layouts/full-screen-layout.tsx +144 -0
  42. package/src/root-layout/layouts/tabs-with-console-layout.tsx +260 -0
  43. package/src/root-layout/root-layout-host.tsx +113 -0
  44. package/src/root-layout/root-layout-registry.ts +51 -0
  45. package/src/root-layout/root-layout-state.ts +55 -0
  46. package/src/root-layout/root-layout.ts +32 -0
  47. package/src/screens/network-viewer/connection-troubleshooter.tsx +10 -14
  48. package/src/screens/settings/settings-page.tsx +7 -1
  49. package/src/tabs-layout/tabs-layout.tsx +28 -74
  50. package/src/tabs-layout/tabs-state.ts +31 -0
  51. package/dist/components/chat-overlay.d.ts +0 -14
  52. package/dist/components/chat-overlay.js +0 -477
  53. package/src/components/chat-overlay.tsx +0 -854
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FullScreenLayout = FullScreenLayout;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ /**
6
+ * Full-screen root layout — one screen at a time, no tab strip.
7
+ *
8
+ * Renders only the active tab's content under a minimal header. All navigation flows
9
+ * through the command palette (Cmd/Ctrl+K), which {@link RootLayoutHost} always mounts,
10
+ * plus a Home button back to the app launcher. While mounted, this layout enables
11
+ * single-tab mode (see `setSingleTabMode`) so opening an app replaces the current screen
12
+ * instead of accumulating tabs. This is non-destructive: any tabs opened under a multi-tab
13
+ * layout are left untouched and reappear if the user switches back.
14
+ */
15
+ const react_1 = require("react");
16
+ const command_palette_1 = require("../../command-palette/command-palette");
17
+ const group_switcher_1 = require("../../components/group-switcher");
18
+ const globals_1 = require("../../globals");
19
+ const hooks_1 = require("../../hooks");
20
+ const color_mode_dropdown_1 = require("../../screens/settings/color-mode-dropdown");
21
+ const tabs_layout_1 = require("../../tabs-layout/tabs-layout");
22
+ const tabs_state_1 = require("../../tabs-layout/tabs-state");
23
+ const isMac = typeof navigator !== "undefined" ? /Mac|iPod|iPhone|iPad/.test(navigator.userAgent) : false;
24
+ const paletteHint = isMac ? "⌘K" : "Ctrl+K";
25
+ /** The full-screen layout body: minimal header + the single active screen. */
26
+ function FullScreenLayout() {
27
+ const [mode] = (0, hooks_1.useObservable)(color_mode_dropdown_1.colorMode);
28
+ const [tabs] = (0, hooks_1.useObservable)(tabs_state_1.activeTabs);
29
+ const [activeId] = (0, hooks_1.useObservable)(tabs_state_1.activeTabId);
30
+ (0, hooks_1.useObservable)(globals_1.isDesktop);
31
+ const isMobile = !(0, globals_1.isDesktop)();
32
+ // Single-tab navigation while this layout is active; restore on unmount/layout swap.
33
+ (0, react_1.useEffect)(() => {
34
+ (0, tabs_state_1.setSingleTabMode)(true);
35
+ return () => (0, tabs_state_1.setSingleTabMode)(false);
36
+ }, []);
37
+ const activeTab = tabs.find((t) => t.tabId === activeId) ?? tabs_state_1.launcherApp;
38
+ const isLauncher = activeTab.packageId === "launcher" || activeTab.tabId === "launcher";
39
+ const isLight = mode === "light";
40
+ const title = isLauncher ? "Apps" : activeTab.title;
41
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "d-flex flex-column", style: { height: "calc(100vh - 25px)", overflow: "hidden" }, children: [(0, jsx_runtime_1.jsxs)("div", { className: `border-bottom d-flex align-items-center px-1 ${isLight ? "bg-light" : "bg-dark"}`, style: { height: "36px", borderBottomColor: isLight ? "#dee2e6" : "#495057" }, children: [(0, jsx_runtime_1.jsx)(group_switcher_1.GroupSwitcher, { colorMode: mode, isMobile: isMobile }), (0, jsx_runtime_1.jsx)(HeaderButton, { isLight: isLight, active: isLauncher, icon: "bi-grid-3x3-gap", label: "Apps", onClick: () => (0, tabs_state_1.activeTabId)("launcher") }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center flex-grow-1 overflow-hidden px-2", children: [!isLauncher && activeTab.iconClassName && ((0, jsx_runtime_1.jsx)("i", { className: `${activeTab.iconClassName} me-2` })), (0, jsx_runtime_1.jsx)("span", { className: "text-truncate fw-medium", style: { fontSize: "13px" }, children: title })] }), (0, jsx_runtime_1.jsx)(HeaderButton, { isLight: isLight, icon: "bi-search", label: `Search and navigate (${paletteHint})`, onClick: command_palette_1.openCommandPalette, children: (0, jsx_runtime_1.jsx)("span", { className: "d-none d-md-inline ms-1", style: { fontSize: "11px" }, children: paletteHint }) })] }), (0, jsx_runtime_1.jsx)("div", { className: "flex-grow-1 overflow-hidden", children: (0, jsx_runtime_1.jsx)(tabs_layout_1.TabErrorBoundary, { tabTitle: activeTab.title, tabId: activeTab.tabId, children: (0, jsx_runtime_1.jsx)(tabs_layout_1.TabContent, { tab: activeTab, isMobile: isMobile, isActive: true }) }, activeTab.tabId) })] }));
42
+ }
43
+ /** A small, transparent header button matching the tabs layout's toolbar styling. */
44
+ function HeaderButton({ isLight, icon, label, onClick, active, children }) {
45
+ const baseColor = active ? (isLight ? "#0d6efd" : "#ffffff") : isLight ? "#6c757d" : "#adb5bd";
46
+ return ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm d-flex align-items-center", onClick: onClick, title: label, "aria-label": label, style: {
47
+ padding: "4px 8px",
48
+ fontSize: "12px",
49
+ borderRadius: "6px",
50
+ border: "none",
51
+ background: "transparent",
52
+ color: baseColor,
53
+ }, onMouseEnter: (e) => {
54
+ e.currentTarget.style.backgroundColor = isLight ? "#f8f9fa" : "#495057";
55
+ e.currentTarget.style.color = isLight ? "#0d6efd" : "#ffffff";
56
+ }, onMouseLeave: (e) => {
57
+ e.currentTarget.style.backgroundColor = "transparent";
58
+ e.currentTarget.style.color = baseColor;
59
+ }, children: [(0, jsx_runtime_1.jsx)("i", { className: icon, style: { fontSize: "14px" } }), children] }));
60
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Tabs + Console root layout.
3
+ *
4
+ * Renders the standard tab UI and the Operator Console as two real, side-by-side panels
5
+ * in a flex split — not an overlay. Neither panel covers the other; each scrolls
6
+ * independently and the tab UI is sized to the space the console does not occupy.
7
+ *
8
+ * This layout owns everything the old overlay used to self-manage: the split sizing, the
9
+ * drag-to-resize divider, the collapsed edge strip, and the docked edge. The console's
10
+ * own chrome is the presentational {@link OperatorConsolePanel}. Panel state (expanded,
11
+ * dock edge, sizes, maximized) is read reactively from `operatorConsolePanel`, the same
12
+ * per-device state the `Ctrl+`` `` shortcut and the "Toggle Console" command drive.
13
+ */
14
+ /**
15
+ * The `tabs-with-console` layout body. Splits its area between the tab UI and the
16
+ * Operator Console along the docked edge, with a drag-to-resize divider. Collapses the
17
+ * console to a thin clickable strip and supports maximizing it to fill the layout body.
18
+ */
19
+ export declare function TabsWithConsoleLayout(): import("react/jsx-runtime").JSX.Element;
20
+ export default TabsWithConsoleLayout;
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TabsWithConsoleLayout = TabsWithConsoleLayout;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const hooks_1 = require("../../hooks");
7
+ const operator_console_1 = require("../../operator-console/operator-console");
8
+ const operator_console_state_1 = require("../../operator-console/operator-console-state");
9
+ const color_mode_dropdown_1 = require("../../screens/settings/color-mode-dropdown");
10
+ const tabs_layout_1 = require("../../tabs-layout/tabs-layout");
11
+ const STRIP_THICKNESS = 26;
12
+ const HANDLE_THICKNESS = 6;
13
+ const MIN_SIZE = 120;
14
+ function clamp(value, min, max) {
15
+ return Math.max(min, Math.min(max, value));
16
+ }
17
+ /** Pixels occupied by the fixed titlebar (macOS = 25, hidden elsewhere). */
18
+ function getTopOffset() {
19
+ if (typeof document === "undefined")
20
+ return 0;
21
+ return Number.parseInt(getComputedStyle(document.body).paddingTop, 10) || 0;
22
+ }
23
+ /**
24
+ * The `tabs-with-console` layout body. Splits its area between the tab UI and the
25
+ * Operator Console along the docked edge, with a drag-to-resize divider. Collapses the
26
+ * console to a thin clickable strip and supports maximizing it to fill the layout body.
27
+ */
28
+ function TabsWithConsoleLayout() {
29
+ const [state] = (0, hooks_1.useObservable)(operator_console_state_1.operatorConsolePanel);
30
+ const [mode] = (0, hooks_1.useObservable)(color_mode_dropdown_1.colorMode);
31
+ const [topOffset] = (0, react_1.useState)(getTopOffset);
32
+ const [dragSize, setDragSize] = (0, react_1.useState)(null);
33
+ const [isResizing, setIsResizing] = (0, react_1.useState)(false);
34
+ const dragStartRef = (0, react_1.useRef)(null);
35
+ const latestSizeRef = (0, react_1.useRef)(null);
36
+ const { expanded, dock, bottomHeight, sideWidth, maximized } = state;
37
+ (0, react_1.useEffect)(() => {
38
+ if (!isResizing)
39
+ return;
40
+ const onMove = (e) => {
41
+ const start = dragStartRef.current;
42
+ if (!start)
43
+ return;
44
+ let next;
45
+ if (dock === "bottom") {
46
+ next = clamp(start.startSize + (start.startY - e.clientY), MIN_SIZE, window.innerHeight - topOffset - MIN_SIZE);
47
+ }
48
+ else if (dock === "left") {
49
+ next = clamp(start.startSize + (e.clientX - start.startX), MIN_SIZE, window.innerWidth - MIN_SIZE);
50
+ }
51
+ else {
52
+ next = clamp(start.startSize + (start.startX - e.clientX), MIN_SIZE, window.innerWidth - MIN_SIZE);
53
+ }
54
+ latestSizeRef.current = next;
55
+ setDragSize(next);
56
+ };
57
+ const onUp = () => {
58
+ setIsResizing(false);
59
+ const finalSize = latestSizeRef.current;
60
+ if (finalSize != null) {
61
+ (0, operator_console_state_1.setConsoleSize)(dock === "bottom" ? { bottomHeight: finalSize } : { sideWidth: finalSize });
62
+ }
63
+ setDragSize(null);
64
+ latestSizeRef.current = null;
65
+ dragStartRef.current = null;
66
+ };
67
+ document.addEventListener("mousemove", onMove);
68
+ document.addEventListener("mouseup", onUp);
69
+ return () => {
70
+ document.removeEventListener("mousemove", onMove);
71
+ document.removeEventListener("mouseup", onUp);
72
+ };
73
+ }, [isResizing, dock, topOffset]);
74
+ const theme = (0, operator_console_1.getConsoleTheme)(mode === "dark");
75
+ const isVertical = dock !== "bottom";
76
+ const consoleSize = dragSize ?? (dock === "bottom" ? bottomHeight : sideWidth);
77
+ const onResizeStart = (e) => {
78
+ e.preventDefault();
79
+ const startSize = dock === "bottom" ? bottomHeight : sideWidth;
80
+ dragStartRef.current = { startX: e.clientX, startY: e.clientY, startSize };
81
+ latestSizeRef.current = startSize;
82
+ setIsResizing(true);
83
+ };
84
+ // Tabs panel stays mounted across maximize so the tab subtree keeps its state; it is
85
+ // hidden (not unmounted) when the console is maximized.
86
+ const tabsHidden = expanded && maximized;
87
+ const tabsPanel = ((0, jsx_runtime_1.jsx)("div", { style: {
88
+ flex: 1,
89
+ minWidth: 0,
90
+ minHeight: 0,
91
+ overflow: "hidden",
92
+ display: tabsHidden ? "none" : "block",
93
+ }, children: (0, jsx_runtime_1.jsx)(tabs_layout_1.TabsLayoutInternal, { fillContainer: true }) }, "tabs"));
94
+ let consoleRegion;
95
+ if (!expanded) {
96
+ consoleRegion = (0, jsx_runtime_1.jsx)(CollapsedStrip, { dock: dock, theme: theme }, "console");
97
+ }
98
+ else if (maximized) {
99
+ consoleRegion = ((0, jsx_runtime_1.jsx)("div", { style: { flex: 1, minWidth: 0, minHeight: 0 }, children: (0, jsx_runtime_1.jsx)(operator_console_1.OperatorConsolePanel, {}) }, "console"));
100
+ }
101
+ else {
102
+ consoleRegion = ((0, jsx_runtime_1.jsx)("div", { style: {
103
+ flex: "0 0 auto",
104
+ minWidth: 0,
105
+ minHeight: 0,
106
+ ...(dock === "bottom" ? { height: `${consoleSize}px` } : { width: `${consoleSize}px` }),
107
+ }, children: (0, jsx_runtime_1.jsx)(operator_console_1.OperatorConsolePanel, {}) }, "console"));
108
+ }
109
+ const divider = expanded && !maximized ? ((0, jsx_runtime_1.jsx)(ResizeDivider, { isVertical: isVertical, isResizing: isResizing, theme: theme, onMouseDown: onResizeStart }, "divider")) : null;
110
+ const children = dock === "left" ? [consoleRegion, divider, tabsPanel] : [tabsPanel, divider, consoleRegion];
111
+ return ((0, jsx_runtime_1.jsx)("div", { style: {
112
+ height: "calc(100vh - 25px)",
113
+ display: "flex",
114
+ flexDirection: isVertical ? "row" : "column",
115
+ overflow: "hidden",
116
+ userSelect: isResizing ? "none" : undefined,
117
+ }, children: children }));
118
+ }
119
+ /** Thin clickable edge strip shown when the console is collapsed; opens it on click. */
120
+ function CollapsedStrip({ dock, theme }) {
121
+ const isVertical = dock !== "bottom";
122
+ return ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: operator_console_state_1.openConsole, title: "Open Console (Ctrl+`)", style: {
123
+ flex: `0 0 ${STRIP_THICKNESS}px`,
124
+ display: "flex",
125
+ alignItems: "center",
126
+ justifyContent: "center",
127
+ gap: "6px",
128
+ border: "none",
129
+ padding: 0,
130
+ backgroundColor: theme.headerBg,
131
+ color: theme.muted,
132
+ cursor: "pointer",
133
+ userSelect: "none",
134
+ fontSize: "12px",
135
+ ...(dock === "bottom"
136
+ ? { width: "100%", borderTop: `1px solid ${theme.border}` }
137
+ : dock === "left"
138
+ ? { height: "100%", borderRight: `1px solid ${theme.border}` }
139
+ : { height: "100%", borderLeft: `1px solid ${theme.border}` }),
140
+ }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-terminal", style: { fontSize: "14px" } }), !isVertical && (0, jsx_runtime_1.jsx)("span", { children: "Console" }), (0, jsx_runtime_1.jsx)("i", { className: isVertical ? "bi-chevron-left" : "bi-chevron-up" })] }));
141
+ }
142
+ /** Drag handle between the tabs and console panels; resizes the console along the dock edge. */
143
+ function ResizeDivider({ isVertical, isResizing, theme, onMouseDown }) {
144
+ return (
145
+ // biome-ignore lint/a11y/noStaticElementInteractions: pointer-only resize gutter; size is also adjustable via persisted state
146
+ (0, jsx_runtime_1.jsx)("div", { onMouseDown: onMouseDown, onMouseEnter: (e) => {
147
+ if (!isResizing)
148
+ e.currentTarget.style.backgroundColor = theme.accent;
149
+ }, onMouseLeave: (e) => {
150
+ if (!isResizing)
151
+ e.currentTarget.style.backgroundColor = "transparent";
152
+ }, style: {
153
+ flex: `0 0 ${HANDLE_THICKNESS}px`,
154
+ backgroundColor: isResizing ? theme.accent : "transparent",
155
+ cursor: isVertical ? "ew-resize" : "ns-resize",
156
+ } }));
157
+ }
158
+ exports.default = TabsWithConsoleLayout;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Root layout host — the single thing a Peers client mounts.
3
+ *
4
+ * `RootLayoutHost` owns everything that must exist regardless of which layout is active:
5
+ * the user gate / setup flow, user-context init, global + route loading, the
6
+ * always-available command palette, and the first-run welcome modal. It then delegates
7
+ * the visible shell to the selected {@link RootLayout}, swapping it live when the
8
+ * per-device selection changes.
9
+ *
10
+ * This is the bootstrap logic that previously lived (tangled with the tab UI) in
11
+ * `TabsLayoutApp` / `TabsLayout`; the tab UI itself is now just the `tabs` layout body
12
+ * (`TabsLayoutInternal`).
13
+ */
14
+ /**
15
+ * Top-level host mounted by the client. Resolves the user, then hands off to the
16
+ * bootstrap component once a user exists. Kept as a thin outer component so the hook
17
+ * order downstream is stable across the user/no-user branches.
18
+ */
19
+ export declare function RootLayoutHost(): import("react/jsx-runtime").JSX.Element | undefined;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RootLayoutHost = RootLayoutHost;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ /**
6
+ * Root layout host — the single thing a Peers client mounts.
7
+ *
8
+ * `RootLayoutHost` owns everything that must exist regardless of which layout is active:
9
+ * the user gate / setup flow, user-context init, global + route loading, the
10
+ * always-available command palette, and the first-run welcome modal. It then delegates
11
+ * the visible shell to the selected {@link RootLayout}, swapping it live when the
12
+ * per-device selection changes.
13
+ *
14
+ * This is the bootstrap logic that previously lived (tangled with the tab UI) in
15
+ * `TabsLayoutApp` / `TabsLayout`; the tab UI itself is now just the `tabs` layout body
16
+ * (`TabsLayoutInternal`).
17
+ */
18
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
19
+ const react_1 = require("react");
20
+ const command_palette_ui_1 = require("../command-palette/command-palette-ui");
21
+ const globals_1 = require("../globals");
22
+ const hooks_1 = require("../hooks");
23
+ const color_mode_dropdown_1 = require("../screens/settings/color-mode-dropdown");
24
+ const setup_user_1 = require("../screens/setup-user");
25
+ const welcome_modal_1 = require("../screens/welcome-modal");
26
+ const tabs_state_1 = require("../tabs-layout/tabs-state");
27
+ const routes_loader_1 = require("../ui-router/routes-loader");
28
+ const global_overlays_1 = require("./global-overlays");
29
+ const root_layout_registry_1 = require("./root-layout-registry");
30
+ const root_layout_state_1 = require("./root-layout-state");
31
+ /**
32
+ * Top-level host mounted by the client. Resolves the user, then hands off to the
33
+ * bootstrap component once a user exists. Kept as a thin outer component so the hook
34
+ * order downstream is stable across the user/no-user branches.
35
+ */
36
+ function RootLayoutHost() {
37
+ const userId = (0, hooks_1.usePromise)(async () => {
38
+ const _userId = await peers_sdk_1.rpcServerCalls.getUserId();
39
+ return _userId || null;
40
+ });
41
+ if (userId === undefined) {
42
+ return;
43
+ }
44
+ if (!userId) {
45
+ document.getElementById("appLoadingDiv")?.remove();
46
+ return (0, jsx_runtime_1.jsx)(setup_user_1.SetupUser, {});
47
+ }
48
+ return (0, jsx_runtime_1.jsx)(RootLayoutBootstrap, { userId: userId });
49
+ }
50
+ let userContextInitialized = false;
51
+ /**
52
+ * Inner bootstrap: initializes the user context, awaits global/route/tab loading, then
53
+ * renders the selected layout body alongside the always-available command palette and
54
+ * the first-run welcome modal.
55
+ */
56
+ function RootLayoutBootstrap(props) {
57
+ if (!userContextInitialized) {
58
+ (0, peers_sdk_1.setDefaultClientUserContext)(props.userId);
59
+ userContextInitialized = true;
60
+ }
61
+ const loaded = (0, hooks_1.usePromise)(async () => {
62
+ await (0, globals_1.loadGlobals)();
63
+ await (0, routes_loader_1.loadAllRoutes)();
64
+ await Promise.all([
65
+ tabs_state_1.activeTabs.loadingPromise,
66
+ tabs_state_1.activeTabId.loadingPromise,
67
+ tabs_state_1.recentlyUsedApps.loadingPromise,
68
+ peers_sdk_1.hasShownWelcomeModal.loadingPromise,
69
+ ]);
70
+ return true;
71
+ });
72
+ const [showWelcomeModal, setShowWelcomeModal] = (0, react_1.useState)(false);
73
+ const [_colorMode] = (0, hooks_1.useObservable)(color_mode_dropdown_1.colorMode);
74
+ const [layoutId] = (0, hooks_1.useObservable)(root_layout_state_1.selectedRootLayoutId);
75
+ const [overlays] = (0, hooks_1.useObservable)(global_overlays_1.globalOverlays);
76
+ // Check if we should show the welcome modal after loading
77
+ (0, react_1.useEffect)(() => {
78
+ if (loaded && !(0, peers_sdk_1.hasShownWelcomeModal)()) {
79
+ setShowWelcomeModal(true);
80
+ }
81
+ }, [loaded]);
82
+ if (!loaded)
83
+ return false;
84
+ document.getElementById("appLoadingDiv")?.remove();
85
+ const layout = (0, root_layout_registry_1.getRootLayout)(layoutId ?? root_layout_registry_1.DEFAULT_ROOT_LAYOUT_ID) ?? (0, root_layout_registry_1.getRootLayout)(root_layout_registry_1.DEFAULT_ROOT_LAYOUT_ID);
86
+ const Body = layout?.Component;
87
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [Body && (0, jsx_runtime_1.jsx)(Body, {}), (0, jsx_runtime_1.jsx)(command_palette_ui_1.CommandPaletteOverlay, {}), overlays.map(({ id, Component }) => ((0, jsx_runtime_1.jsx)(Component, {}, id))), showWelcomeModal && ((0, jsx_runtime_1.jsx)(welcome_modal_1.WelcomeModal, { colorMode: _colorMode, onClose: () => setShowWelcomeModal(false) }))] }));
88
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Root layout registry — the static list of built-in layouts owned by peers-ui.
3
+ *
4
+ * For now this is a hardcoded list. The accessor functions ({@link getRootLayouts} /
5
+ * {@link getRootLayout}) intentionally hide the underlying array so that dynamic,
6
+ * package-contributed layouts (a future `registerRootLayout`) can be added later
7
+ * without changing any consumer.
8
+ */
9
+ import type { RootLayout } from "./root-layout";
10
+ /** Id of the layout used when no selection has been made (or the selection is unknown). */
11
+ export declare const DEFAULT_ROOT_LAYOUT_ID = "tabs";
12
+ /** Returns all registered root layouts, in display order. */
13
+ export declare function getRootLayouts(): RootLayout[];
14
+ /** Returns the layout with the given id, or `undefined` if none is registered. */
15
+ export declare function getRootLayout(id: string): RootLayout | undefined;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * Root layout registry — the static list of built-in layouts owned by peers-ui.
4
+ *
5
+ * For now this is a hardcoded list. The accessor functions ({@link getRootLayouts} /
6
+ * {@link getRootLayout}) intentionally hide the underlying array so that dynamic,
7
+ * package-contributed layouts (a future `registerRootLayout`) can be added later
8
+ * without changing any consumer.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.DEFAULT_ROOT_LAYOUT_ID = void 0;
12
+ exports.getRootLayouts = getRootLayouts;
13
+ exports.getRootLayout = getRootLayout;
14
+ const tabs_layout_1 = require("../tabs-layout/tabs-layout");
15
+ const full_screen_layout_1 = require("./layouts/full-screen-layout");
16
+ const tabs_with_console_layout_1 = require("./layouts/tabs-with-console-layout");
17
+ /** Id of the layout used when no selection has been made (or the selection is unknown). */
18
+ exports.DEFAULT_ROOT_LAYOUT_ID = "tabs";
19
+ /** Built-in layouts, in picker display order. */
20
+ const builtInLayouts = [
21
+ {
22
+ id: "tabs",
23
+ name: "Tabs",
24
+ description: "The classic Peers shell: a tab strip with one app per tab.",
25
+ iconClassName: "bi-window-stack",
26
+ Component: tabs_layout_1.TabsLayoutInternal,
27
+ },
28
+ {
29
+ id: "tabs-with-console",
30
+ name: "Tabs + Console",
31
+ description: "The tab strip with the Operator Console docked alongside as a resizable panel.",
32
+ iconClassName: "bi-layout-split",
33
+ Component: tabs_with_console_layout_1.TabsWithConsoleLayout,
34
+ },
35
+ {
36
+ id: "fullscreen",
37
+ name: "Full-screen",
38
+ description: "One screen at a time, no tabs — navigate with the Cmd/Ctrl+K command palette.",
39
+ iconClassName: "bi-fullscreen",
40
+ Component: full_screen_layout_1.FullScreenLayout,
41
+ },
42
+ ];
43
+ /** Returns all registered root layouts, in display order. */
44
+ function getRootLayouts() {
45
+ return builtInLayouts;
46
+ }
47
+ /** Returns the layout with the given id, or `undefined` if none is registered. */
48
+ function getRootLayout(id) {
49
+ return builtInLayouts.find((layout) => layout.id === id);
50
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Root layout selection — persistent, per-device state plus palette integration.
3
+ *
4
+ * The selected layout id is stored in a `deviceVar` (per-device, not synced) to match
5
+ * the precedent of other root-UI preferences (`colorMode`, the operator console panel):
6
+ * layout choice is tightly coupled to the device/form factor. `RootLayoutHost` reads
7
+ * this reactively and swaps the layout live when it changes.
8
+ */
9
+ /**
10
+ * Per-device id of the currently selected root layout. Defaults to
11
+ * {@link DEFAULT_ROOT_LAYOUT_ID}. Observable — subscribers (the host, the picker)
12
+ * re-render when it changes.
13
+ */
14
+ export declare const selectedRootLayoutId: import("@peers-app/peers-sdk").PersistentVar<string>;
15
+ /** Select the root layout with the given id (persists per-device). */
16
+ export declare function setRootLayout(id: string): void;
17
+ /**
18
+ * Register a `"Use <name> Layout"` command per layout so the selector is keyboard-driven
19
+ * and discoverable from the command palette (mirrors the operator console's "Toggle
20
+ * Console" command). Idempotent. The registry is static today, so this runs once at
21
+ * module load.
22
+ */
23
+ export declare function registerLayoutCommands(): void;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ /**
3
+ * Root layout selection — persistent, per-device state plus palette integration.
4
+ *
5
+ * The selected layout id is stored in a `deviceVar` (per-device, not synced) to match
6
+ * the precedent of other root-UI preferences (`colorMode`, the operator console panel):
7
+ * layout choice is tightly coupled to the device/form factor. `RootLayoutHost` reads
8
+ * this reactively and swaps the layout live when it changes.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.selectedRootLayoutId = void 0;
12
+ exports.setRootLayout = setRootLayout;
13
+ exports.registerLayoutCommands = registerLayoutCommands;
14
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
15
+ const command_palette_1 = require("../command-palette/command-palette");
16
+ const root_layout_registry_1 = require("./root-layout-registry");
17
+ /**
18
+ * Per-device id of the currently selected root layout. Defaults to
19
+ * {@link DEFAULT_ROOT_LAYOUT_ID}. Observable — subscribers (the host, the picker)
20
+ * re-render when it changes.
21
+ */
22
+ exports.selectedRootLayoutId = (0, peers_sdk_1.deviceVar)("rootLayout:selectedId", {
23
+ defaultValue: root_layout_registry_1.DEFAULT_ROOT_LAYOUT_ID,
24
+ });
25
+ /** Select the root layout with the given id (persists per-device). */
26
+ function setRootLayout(id) {
27
+ (0, exports.selectedRootLayoutId)(id);
28
+ }
29
+ let initialized = false;
30
+ /**
31
+ * Register a `"Use <name> Layout"` command per layout so the selector is keyboard-driven
32
+ * and discoverable from the command palette (mirrors the operator console's "Toggle
33
+ * Console" command). Idempotent. The registry is static today, so this runs once at
34
+ * module load.
35
+ */
36
+ function registerLayoutCommands() {
37
+ if (initialized)
38
+ return;
39
+ initialized = true;
40
+ for (const layout of (0, root_layout_registry_1.getRootLayouts)()) {
41
+ (0, command_palette_1.registerCommand)({
42
+ id: `use-root-layout:${layout.id}`,
43
+ label: `Use ${layout.name} Layout`,
44
+ description: layout.description,
45
+ iconClassName: layout.iconClassName,
46
+ category: "View",
47
+ action: () => {
48
+ (0, command_palette_1.closeCommandPalette)();
49
+ setRootLayout(layout.id);
50
+ },
51
+ });
52
+ }
53
+ }
54
+ registerLayoutCommands();
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Root layout system — the layout interface.
3
+ *
4
+ * A {@link RootLayout} is a top-level, swappable app shell. The user (and, in the
5
+ * future, a package) can choose which root layout owns the visible chrome. Layouts
6
+ * are rendered *inside* `RootLayoutHost`, which has already done all bootstrapping
7
+ * (user context, route loading) and mounted the always-available command palette.
8
+ * A layout therefore owns 100% of the visible chrome (tab strip, content panels,
9
+ * console panel, etc.) while the host owns the invisible foundation.
10
+ */
11
+ import type React from "react";
12
+ /**
13
+ * A selectable top-level app layout. The `Component` is the layout body rendered by
14
+ * `RootLayoutHost`; everything visible on screen is the layout's responsibility.
15
+ */
16
+ export interface RootLayout {
17
+ /** Stable id persisted in the layout-selection pvar (e.g. `"tabs"`). */
18
+ id: string;
19
+ /** User-facing name shown in the layout picker. */
20
+ name: string;
21
+ /** One-line description shown in the picker. */
22
+ description: string;
23
+ /** bootstrap-icon class used in the picker. */
24
+ iconClassName: string;
25
+ /**
26
+ * The layout body. Rendered inside `RootLayoutHost` after bootstrapping is
27
+ * complete and the command palette is mounted. The body owns all visible chrome.
28
+ */
29
+ Component: React.ComponentType;
30
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ /**
3
+ * Root layout system — the layout interface.
4
+ *
5
+ * A {@link RootLayout} is a top-level, swappable app shell. The user (and, in the
6
+ * future, a package) can choose which root layout owns the visible chrome. Layouts
7
+ * are rendered *inside* `RootLayoutHost`, which has already done all bootstrapping
8
+ * (user context, route loading) and mounted the always-available command palette.
9
+ * A layout therefore owns 100% of the visible chrome (tab strip, content panels,
10
+ * console panel, etc.) while the host owns the invisible foundation.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConnectionTroubleshooter = ConnectionTroubleshooter;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
- const peers_sdk_1 = require("@peers-app/peers-sdk");
6
5
  const react_1 = require("react");
7
6
  const network_viewer_ipc_1 = require("./network-viewer-ipc");
8
7
  const STATUS_ICON = {
@@ -149,16 +148,13 @@ function ConnectionTroubleshooter({ onBack }) {
149
148
  const handleAskAssistant = () => {
150
149
  const report = formatReportForAI(checks.filter((c) => c.status !== "running"));
151
150
  const message = `I ran the Peers connection troubleshooter and got the following results. Please help me understand what's wrong and how to fix it:\n\n${report}`;
152
- (0, peers_sdk_1.emit)({ name: "chat:openWithMessage", data: { message } }).catch(() => {
153
- // Fallback: copy to clipboard
154
- navigator.clipboard
155
- .writeText(message)
156
- .then(() => {
157
- alert("Diagnostic report copied to clipboard. Paste it into the AI assistant chat.");
158
- })
159
- .catch(() => {
160
- alert("Could not open AI assistant. Please open the chat manually and paste the diagnostic report.");
161
- });
151
+ navigator.clipboard
152
+ .writeText(message)
153
+ .then(() => {
154
+ alert("Diagnostic report copied to clipboard. Paste it into the AI assistant chat.");
155
+ })
156
+ .catch(() => {
157
+ alert("Could not copy the diagnostic report. Please open the chat manually and describe the results.");
162
158
  });
163
159
  };
164
160
  const groups = groupByLayer(checks.filter((c) => !c.id.startsWith("placeholder-")));
@@ -42,6 +42,7 @@ const tabs_1 = require("../../components/tabs");
42
42
  const tooltip_1 = require("../../components/tooltip");
43
43
  const globals = __importStar(require("../../globals"));
44
44
  const hooks_1 = require("../../hooks");
45
+ const layout_picker_1 = require("../../root-layout/layout-picker");
45
46
  const color_mode_dropdown_1 = require("./color-mode-dropdown");
46
47
  const voice_settings_1 = require("./voice-settings");
47
48
  function getAppVersion() {
@@ -73,7 +74,7 @@ const UserSettingsTab = () => {
73
74
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ProfileSection, {}), (0, jsx_runtime_1.jsx)(LogoutSection, {})] }));
74
75
  };
75
76
  const AppearanceSettingsTab = () => {
76
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h5", { className: "mb-3", children: "Appearance" }), (0, jsx_runtime_1.jsx)(color_mode_dropdown_1.ColorModeDropdown, {})] }));
77
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h5", { className: "mb-3", children: "Appearance" }), (0, jsx_runtime_1.jsx)("div", { className: "mb-3", children: (0, jsx_runtime_1.jsx)(color_mode_dropdown_1.ColorModeDropdown, {}) }), (0, jsx_runtime_1.jsx)("div", { className: "mb-3", children: (0, jsx_runtime_1.jsx)(layout_picker_1.LayoutPicker, {}) })] }));
77
78
  };
78
79
  const AdvancedSettingsTab = () => {
79
80
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h5", { className: "mb-3", children: "Advanced Settings" }), (0, jsx_runtime_1.jsx)(PackagesRootDirectory, {}), (0, jsx_runtime_1.jsx)(ReloadPackagesOnPageRefresh, {}), (0, jsx_runtime_1.jsx)(AutoUpdatePeersCore, {}), (0, jsx_runtime_1.jsx)(ResetDeviceSyncInfos, {}), (0, jsx_runtime_1.jsx)(DeleteLocalDatabase, {}), (0, jsx_runtime_1.jsx)(CleanupOldDataDir, {}), (0, jsx_runtime_1.jsx)(ImportOldPeersData, {}), (0, jsx_runtime_1.jsx)(ResetChangeTracking, {})] }));
@@ -1,4 +1,48 @@
1
- export declare function TabsLayoutApp(): import("react/jsx-runtime").JSX.Element | undefined;
2
- export declare function TabsLayout(props: {
3
- userId: string;
4
- }): false | import("react/jsx-runtime").JSX.Element;
1
+ import React, { Component } from "react";
2
+ import { type TabState } from "./tabs-state";
3
+ interface TabErrorBoundaryProps {
4
+ tabTitle: string;
5
+ tabId: string;
6
+ children: React.ReactNode;
7
+ }
8
+ interface TabErrorBoundaryState {
9
+ hasError: boolean;
10
+ error: Error | null;
11
+ }
12
+ /**
13
+ * Wraps a single tab's content so a crash in one tab doesn't take down the whole app.
14
+ * Exported so other root layouts (e.g. the full-screen layout) can render a tab body with
15
+ * the same crash isolation. Reset it across screens by giving it a `key`.
16
+ */
17
+ export declare class TabErrorBoundary extends Component<TabErrorBoundaryProps, TabErrorBoundaryState> {
18
+ constructor(props: TabErrorBoundaryProps);
19
+ static getDerivedStateFromError(error: Error): TabErrorBoundaryState;
20
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void;
21
+ render(): string | number | boolean | Iterable<React.ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
22
+ }
23
+ /**
24
+ * The tab UI body: the tab strip plus the active tab's content. Rendered as the body of
25
+ * the default `tabs` root layout inside `RootLayoutHost` (which owns bootstrapping, the
26
+ * command palette, and the welcome modal). Branches on `isDesktop` for mobile vs. desktop
27
+ * chrome.
28
+ *
29
+ * @param fillContainer When `true`, the body fills its parent (`height: 100%`) instead of
30
+ * sizing itself to the viewport. Set this when embedding the tabs UI inside another
31
+ * layout that owns the vertical space (e.g. the `tabs-with-console` split layout);
32
+ * leave it `false`/unset for the standalone `tabs` layout, which sizes to the viewport.
33
+ */
34
+ export declare function TabsLayoutInternal({ fillContainer }?: {
35
+ fillContainer?: boolean;
36
+ }): import("react/jsx-runtime").JSX.Element;
37
+ interface TabContentProps {
38
+ tab: TabState;
39
+ isMobile: boolean;
40
+ isActive: boolean;
41
+ }
42
+ /**
43
+ * Renders a single tab's body, lazily initializing it on first activation and keeping it
44
+ * mounted thereafter (`initializedTabs`). Exported so other root layouts (e.g. full-screen)
45
+ * can render the active screen through the same routing/launcher path.
46
+ */
47
+ export declare function TabContent({ tab, isMobile, isActive }: TabContentProps): import("react/jsx-runtime").JSX.Element | null;
48
+ export {};