diff --git a/src/client/features/shows/cue-list-view-store.ts b/src/client/features/shows/cue-list-view-store.ts
index d9119ab..5d4d5f6 100644
--- a/src/client/features/shows/cue-list-view-store.ts
+++ b/src/client/features/shows/cue-list-view-store.ts
@@ -1,6 +1,8 @@
import { createContext, useContext } from "react";
import { proxy } from "valtio";
+export type CueListViewMode = "both" | "top" | "bottom";
+
export interface CueListViewState {
// Track and value selection for bottom pane filtering
selectedTrackId: string | null;
@@ -9,6 +11,9 @@ export interface CueListViewState {
// Horizontal splitter position as percentage (0-100)
// 50 = 50/50 split
splitterPositionPercent: number;
+
+ // Which pane(s) to display
+ viewMode: CueListViewMode;
}
export function createCueListViewStore(): CueListViewState {
@@ -16,6 +21,7 @@ export function createCueListViewStore(): CueListViewState {
selectedTrackId: null,
selectedTechnicalIdentifier: null,
splitterPositionPercent: 50,
+ viewMode: "both",
});
}
diff --git a/src/client/features/shows/cue-list-view.tsx b/src/client/features/shows/cue-list-view.tsx
index 1f6a29e..3b5ac27 100644
--- a/src/client/features/shows/cue-list-view.tsx
+++ b/src/client/features/shows/cue-list-view.tsx
@@ -8,13 +8,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/cli
import { cn, getContrastColor } from "@/client/lib/utils";
import {
CueListViewStoreContext,
+ CueListViewMode,
getOrCreateCueListViewStore,
destroyCueListViewStore,
useCueListViewStore,
} from "@/client/features/shows/cue-list-view-store";
import { Cue } from "@/server/db/entities/Cue";
import { Show } from "@/server/db/entities/Show";
-import { QrCodeIcon } from "lucide-react";
+import { PanelBottom, PanelTop, QrCodeIcon, Rows2 } from "lucide-react";
function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value));
@@ -257,12 +258,16 @@ function CueListViewContent() {
const params = new URLSearchParams(window.location.hash.slice(1));
const trackId = params.get("trackId");
const identifier = params.get("identifier");
+ const view = params.get("view");
if (trackId) {
store.selectedTrackId = trackId;
}
if (identifier) {
store.selectedTechnicalIdentifier = identifier;
}
+ if (view === "top" || view === "bottom" || view === "both") {
+ store.viewMode = view;
+ }
}
// On initial load, parse hash for selected track and technical identifier
@@ -367,7 +372,7 @@ function CueListViewContent() {
updateHash(store.selectedTrackId, selectedTechnicalIdentifier || null);
}
- function updateHash(selectedTrackId: string | null, selectedTechnicalIdentifier: string | null) {
+ function updateHash(selectedTrackId: string | null, selectedTechnicalIdentifier: string | null, viewMode?: CueListViewMode) {
const params = new URLSearchParams(window.location.hash.slice(1));
if (selectedTrackId) {
params.set("trackId", selectedTrackId);
@@ -379,38 +384,206 @@ function CueListViewContent() {
} else {
params.delete("identifier");
}
+ const resolvedViewMode = viewMode ?? store.viewMode;
+ if (resolvedViewMode && resolvedViewMode !== "both") {
+ params.set("view", resolvedViewMode);
+ } else {
+ params.delete("view");
+ }
window.location.hash = params.toString();
}
+ function handleViewModeChange(newViewMode: CueListViewMode) {
+ store.viewMode = newViewMode;
+ updateHash(store.selectedTrackId, store.selectedTechnicalIdentifier, newViewMode);
+ }
+
+ const viewModeToggle = (
+
+
handleViewModeChange("top")}
+ >
+
+
+
handleViewModeChange("both")}
+ >
+
+
+
handleViewModeChange("bottom")}
+ >
+
+
+
+ );
+
return (
{/* Top pane */}
-
-
- {topPaneCues.length === 0 && !currentCue ? (
-
- No cues to display
-
- ) : (
- <>
- {currentCue !== undefined ? (
-
+
+ {topPaneCues.length === 0 && !currentCue ? (
+
+ No cues to display
+
+ ) : (
+ <>
+ {currentCue !== undefined ? (
+
, ctv: any) => {
+ acc[ctv.trackId] = ctv.technicalIdentifier;
+ return acc;
+ },
+ {},
+ )}
+ />
+ ) : null}
+ {topPaneCues.map((cue: any, idx: number) => {
+ let status: "current" | "next" | "following" = "following";
+ if (cue.id === safeShow.currentCueId) {
+ status = "current";
+ } else if (cue.id === safeShow.nextCueId) {
+ status = "next";
+ }
+
+ const trackValues = cue.cueTrackValues?.reduce(
(acc: Record, ctv: any) => {
acc[ctv.trackId] = ctv.technicalIdentifier;
return acc;
},
{},
- )}
- />
- ) : null}
- {topPaneCues.map((cue: any, idx: number) => {
+ );
+
+ return (
+
+ );
+ })}
+ >
+ )}
+
+
+ )}
+
+ {/* Splitter — only when showing both panes */}
+ {snapshot.viewMode === "both" && (
+
+ )}
+
+ {/* Bottom pane */}
+ {(snapshot.viewMode === "both" || snapshot.viewMode === "bottom") && (
+
+
+
+
+ {/* Track selector */}
+
+
+ Track
+
+ handleSelectedTrackChange(e.target.value || undefined)}
+ className="w-full border border-border/70 bg-background px-3 py-2 text-sm"
+ >
+ — Select track —
+ {safeShow.tracks.map((track: any) => (
+
+ {track.name}
+
+ ))}
+
+
+
+ {/* Technical identifier selector */}
+
+
+ Value
+
+
+ handleSelectedTechnicalIdentifierChange(e.target.value || undefined)}
+ disabled={!snapshot.selectedTrackId}
+ className="w-full border border-border/70 bg-background px-3 py-2 text-sm disabled:opacity-50"
+ >
+ — Select value —
+ {selectedTrackTechnicalIdentifiers.map((identifier) => (
+
+ {identifier}
+
+ ))}
+
+ setIsQrModalOpen(true)}
+ >
+
+
+
+
+
+
+ {/* View mode toggle */}
+ {viewModeToggle}
+
+
+
+ {/* Bottom pane content */}
+
+ {bottomPaneCues.length === 0 ? (
+
+ {snapshot.selectedTrackId && snapshot.selectedTechnicalIdentifier
+ ? "No cues match the selected filter"
+ : "Select a track and value to filter cues"}
+
+ ) : (
+ bottomPaneCues.map((cue: any) => {
let status: "current" | "next" | "following" = "following";
if (cue.id === safeShow.currentCueId) {
status = "current";
@@ -437,119 +610,18 @@ function CueListViewContent() {
trackValues={trackValues}
/>
);
- })}
- >
- )}
-
-
-
- {/* Splitter */}
-
-
- {/* Bottom pane */}
-
-
-
- {/* Track selector */}
-
-
- Track
-
- handleSelectedTrackChange(e.target.value || undefined)}
- className="w-full border border-border/70 bg-background px-3 py-2 text-sm"
- >
- — Select track —
- {safeShow.tracks.map((track: any) => (
-
- {track.name}
-
- ))}
-
-
-
- {/* Technical identifier selector */}
-
-
- Value
-
-
- handleSelectedTechnicalIdentifierChange(e.target.value || undefined)}
- disabled={!snapshot.selectedTrackId}
- className="w-full border border-border/70 bg-background px-3 py-2 text-sm disabled:opacity-50"
- >
- — Select value —
- {selectedTrackTechnicalIdentifiers.map((identifier) => (
-
- {identifier}
-
- ))}
-
- setIsQrModalOpen(true)}
- >
-
-
-
-
+ })
+ )}
+ )}
- {/* Bottom pane content */}
-
- {bottomPaneCues.length === 0 ? (
-
- {snapshot.selectedTrackId && snapshot.selectedTechnicalIdentifier
- ? "No cues match the selected filter"
- : "Select a track and value to filter cues"}
-
- ) : (
- bottomPaneCues.map((cue: any) => {
- let status: "current" | "next" | "following" = "following";
- if (cue.id === safeShow.currentCueId) {
- status = "current";
- } else if (cue.id === safeShow.nextCueId) {
- status = "next";
- }
-
- const trackValues = cue.cueTrackValues?.reduce(
- (acc: Record
, ctv: any) => {
- acc[ctv.trackId] = ctv.technicalIdentifier;
- return acc;
- },
- {},
- );
-
- return (
-
- );
- })
- )}
+ {/* When only the top pane is shown, render the view mode toggle in a thin bar at the bottom */}
+ {snapshot.viewMode === "top" && (
+
+ {viewModeToggle}
-
+ )}
{isQrModalOpen ? (