Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import PreferencesDialog, {
type PreferencesDialogRef,
} from "./components/PreferencesDialog";
import LinkPickerDialog from "./components/LinkPickerDialog";
import Sidebar, { type SidebarRef } from "./components/sidebar";
import Sidebar, { clampSidebarWidth, type SidebarRef } from "./components/sidebar";
import Statusbar from "./components/statusbar";
import Toolbar from "./components/toolbar";
import WasmGuest from "./components/WasmGuest";
Expand All @@ -27,6 +27,11 @@ import {
autoLogService,
createAutoLogSessionDraft,
} from "./logging/AutoLogService";
import {
loadStoredValue,
saveStoredValue,
type LocalStorageSchema,
} from "./persistence";
import { usePreferences } from "./stores/preferencesStore";
import { useRoomStore } from "./stores/roomStore";
import { useConnectionStore } from "./stores/connectionStore";
Expand All @@ -36,6 +41,19 @@ import { ensurePushSubscription } from "./webpush";
const WINDOW_TITLE = "Mongoose Client";
const DOUBLE_PRESS_WINDOW_MS = 500;

// User-chosen sidebar width in px; absent until the user first resizes.
const sidebarWidthSchema: LocalStorageSchema<number> = {
key: "sidebarWidth",
version: 1,
migrate: (data) =>
typeof data === "number" && Number.isFinite(data) ? data : undefined,
};

function loadSidebarWidth(): number | null {
const stored = loadStoredValue<number | null>(sidebarWidthSchema, null);
return stored === null ? null : clampSidebarWidth(stored);
}

function setWindowSubtitle(subtitle?: string) {
document.title = subtitle ? `${WINDOW_TITLE} - ${subtitle}` : WINDOW_TITLE;
}
Expand Down Expand Up @@ -92,6 +110,18 @@ function App() {
const [client, setClient] = useState<MudClient | null>(null);
const [showSidebar, setShowSidebar] = useState<boolean>(false);
const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(false);
const [sidebarWidth, setSidebarWidth] = useState<number | null>(loadSidebarWidth);

// Persist the user-chosen sidebar width, debounced so drags don't
// write localStorage on every pointer move
useEffect(() => {
if (sidebarWidth === null) return;
const timer = window.setTimeout(
() => saveStoredValue(sidebarWidthSchema, sidebarWidth),
250,
);
return () => window.clearTimeout(timer);
}, [sidebarWidth]);
const [hostState, setHostState] = useState<WasmHostState>({
roomId: null,
guestCount: 0,
Expand All @@ -110,8 +140,8 @@ function App() {

const clientInitialized = useRef(false);
const hapticsRuntimeRef = useRef<HapticsRuntime | null>(null);
const midiEnabled = usePreferences((state) => state.midi.enabled);
const hapticsEnabled = usePreferences((state) => state.haptics.enabled);
const midiEnabled = usePreferences((state) => state.midi.enabled);
const hapticsEnabled = usePreferences((state) => state.haptics.enabled);
const connected = useConnectionStore((state) => state.connected);
const sessionReady = useConnectionStore((state) => state.sessionReady);
useFileTransferNotifications(client);
Expand Down Expand Up @@ -335,7 +365,7 @@ function App() {
}, [handleAppKeyDown]);

useEffect(() => {
if (!midiEnabled) return;
if (!midiEnabled) return;

let cancelled = false;
import("./VirtualMidiService")
Expand All @@ -356,7 +386,7 @@ function App() {
return () => {
cancelled = true;
};
}, [midiEnabled]);
}, [midiEnabled]);

// Window subtitle tracks the current room from the room store. On disconnect
// the client resets the store, which clears roomInfo and so clears the subtitle.
Expand All @@ -373,8 +403,8 @@ function App() {
}, [client, roomInfo]);

useEffect(() => {
hapticsRuntimeRef.current?.setEnabled(hapticsEnabled);
}, [hapticsEnabled]);
hapticsRuntimeRef.current?.setEnabled(hapticsEnabled);
}, [hapticsEnabled]);

const handleCommand = useCallback(
(text: string) => {
Expand Down Expand Up @@ -435,6 +465,11 @@ function App() {
{client && (
<div
className={`App ${showSidebar ? (sidebarCollapsed ? "sidebar-collapsed" : "sidebar-shown") : ""}`}
style={
sidebarWidth !== null
? ({ "--sidebar-width": `${sidebarWidth}px` } as React.CSSProperties)
: undefined
}
>
<header style={{ gridArea: "header" }}>
<Toolbar
Expand Down Expand Up @@ -478,6 +513,8 @@ function App() {
client={client}
collapsed={sidebarCollapsed}
onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)}
width={sidebarWidth}
onWidthChange={setSidebarWidth}
/>
</aside>
<footer style={{ gridArea: "status" }}>
Expand Down
65 changes: 63 additions & 2 deletions src/components/sidebar.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* Main sidebar container */
.sidebar {
position: relative;
height: 100%;
width: var(--sidebar-width);
display: flex;
Expand All @@ -9,6 +10,61 @@
transition: width var(--duration-slow) var(--ease-default);
}

/* Instant tracking while the user drags the resize handle */
.sidebar[data-resizing] {
transition: none;
}

/* Resize handle along the sidebar's left edge (ARIA window splitter) */
.sidebar-resize-handle {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 8px;
margin-left: -4px; /* straddle the border so it's easier to grab */
cursor: col-resize;
touch-action: none;
z-index: 1;
}

.sidebar-resize-handle:hover,
.sidebar[data-resizing] .sidebar-resize-handle {
background: var(--color-primary);
opacity: 0.4;
}

.sidebar-resize-handle:focus-visible {
outline: 3px solid var(--color-focus-ring);
outline-offset: -3px;
}

/* Mobile: sidebar is a full-width stacked row; no horizontal resizing */
@media (max-width: 768px) {
.sidebar-resize-handle {
display: none;
}

.sidebar {
height: auto;
border-left: none;
}

/* Cap the tab panel itself so long content scrolls top-to-bottom.
(max-height on the auto-height aside can't shrink flex children, so
the scroll cap has to live on the overflow container.) Panel cap +
tab bar stays within the aside's 40vh/30vh max-height. */
.sidebar div[role="tabpanel"] {
max-height: 32vh;
}
}

@media (max-width: 768px) and (max-height: 700px) {
.sidebar div[role="tabpanel"] {
max-height: 22vh;
}
}

/* Collapsed sidebar styles */
.sidebar.collapsed {
width: var(--sidebar-width-collapsed, 50px);
Expand Down Expand Up @@ -77,8 +133,13 @@
overflow: hidden;
}

/* Individual tab panel content area (inside the tabpanel) */
/* Keep the hidden attribute working despite the display: flex above */
.sidebar-content[hidden] {
display: none;
}

/* Individual tab panel content area (inside the tabpanel).
Padding comes from the tabpanel itself (tabs.css). */
.sidebar-tab-content {
padding: var(--space-5);
line-height: var(--line-height-base);
}
Loading