remove grid view for streams
- Reduces surface area - Reduces options for people (less cognitive load) - One way to use the product - List view works better for keyboard shortcuts - List view has basics such as recency
This commit is contained in:
@@ -1,14 +1,11 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { List, LayoutGrid, CircleDot, CircleCheckBig } from "lucide-react";
|
||||
import { CircleDot, CircleCheckBig } from "lucide-react";
|
||||
import { particlePath } from "@/lib/particle-path";
|
||||
import { ParticleListView } from "@/features/particles/particle-list-view";
|
||||
import { ParticleGridView } from "@/features/particles/particle-grid-view";
|
||||
import ControlsIndicator from "@/features/compose/controls-indicator";
|
||||
import { ComposeOverlay } from "./compose/compose-overlay";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { useViewModeStore } from "@/stores/view-mode-store";
|
||||
import { useStreamParticles } from "@/hooks/use-stream-particles";
|
||||
import { useStreamKeyboardNav } from "@/hooks/use-stream-keyboard-nav";
|
||||
import { useDockBadge } from "@/hooks/use-dock-badge";
|
||||
@@ -22,8 +19,6 @@ export default function NetworkRoot() {
|
||||
const { networkId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const path = particlePath(networkId!, []);
|
||||
const viewMode = useViewModeStore((s) => s.viewMode);
|
||||
const setViewMode = useViewModeStore((s) => s.setViewMode);
|
||||
|
||||
const { streams, isLoading } = useStreamParticles(path);
|
||||
const userId = useAuthStore((s) => s.user?.id);
|
||||
@@ -38,7 +33,6 @@ export default function NetworkRoot() {
|
||||
|
||||
const { selectedIndex } = useStreamKeyboardNav({
|
||||
streams: filteredStreams,
|
||||
viewMode,
|
||||
enabled: !composeActive,
|
||||
onNavigate: useCallback(
|
||||
(streamId: string) => navigate(`/${networkId}/${streamId}`),
|
||||
@@ -59,30 +53,11 @@ export default function NetworkRoot() {
|
||||
<TabsTrigger value="closed"><CircleCheckBig className="size-3" /> Closed</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={viewMode}
|
||||
onValueChange={(v) => {
|
||||
if (v) setViewMode(v as "list" | "grid");
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
<ToggleGroupItem value="list" aria-label="List view">
|
||||
<List className="size-3.5" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="grid" aria-label="Grid view">
|
||||
<LayoutGrid className="size-3.5" />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain pb-14 py-2">
|
||||
{viewMode === "list" ? (
|
||||
<ParticleListView streams={filteredStreams} networkId={networkId!} isLoading={isLoading} selectedIndex={selectedIndex} />
|
||||
) : (
|
||||
<ParticleGridView streams={filteredStreams} networkId={networkId!} isLoading={isLoading} selectedIndex={selectedIndex} />
|
||||
)}
|
||||
<ParticleListView streams={filteredStreams} networkId={networkId!} isLoading={isLoading} selectedIndex={selectedIndex} />
|
||||
</div>
|
||||
|
||||
<ComposeOverlay networkId={networkId!} onActiveChange={setComposeActive} />
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Radio } from "lucide-react";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import type { StreamParticle } from "@/hooks/use-stream-particles";
|
||||
import { StreamCard } from "@/features/particles/stream-card";
|
||||
import { StreamContextMenu } from "@/features/particles/stream-context-menu";
|
||||
|
||||
interface ParticleGridViewProps {
|
||||
streams: StreamParticle[];
|
||||
networkId: string;
|
||||
isLoading: boolean;
|
||||
selectedIndex?: number | null;
|
||||
}
|
||||
|
||||
export function ParticleGridView({ streams, networkId, isLoading, selectedIndex }: ParticleGridViewProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (isLoading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
if (streams.length === 0) {
|
||||
return (
|
||||
<div className="mx-auto flex h-full max-w-sm flex-col items-center justify-center gap-2 px-4 text-center">
|
||||
<Radio className="text-muted-foreground size-8" />
|
||||
<p className="text-muted-foreground text-sm">
|
||||
No recent streams yet. Start a conversation using the keyboard
|
||||
shortcuts below.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-3 px-3">
|
||||
{streams.map((stream, index) => (
|
||||
<StreamContextMenu key={stream.id} particle={stream} networkId={networkId}>
|
||||
<div ref={index === selectedIndex ? (el) => el?.scrollIntoView({ block: "nearest" }) : undefined}>
|
||||
<StreamCard
|
||||
particle={stream}
|
||||
networkId={networkId}
|
||||
onClick={() => navigate(`/${networkId}/${stream.id}`)}
|
||||
isSelected={index === selectedIndex}
|
||||
shortcutKey={index < 9 ? index + 1 : undefined}
|
||||
/>
|
||||
</div>
|
||||
</StreamContextMenu>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,25 +2,21 @@ import { useEffect, useState } from "react";
|
||||
|
||||
interface UseStreamKeyboardNavOptions {
|
||||
streams: Array<{ id: string }>;
|
||||
viewMode: "list" | "grid";
|
||||
enabled: boolean;
|
||||
onNavigate: (streamId: string) => void;
|
||||
gridColumns?: number;
|
||||
}
|
||||
|
||||
export function useStreamKeyboardNav({
|
||||
streams,
|
||||
viewMode,
|
||||
enabled,
|
||||
onNavigate,
|
||||
gridColumns = 3,
|
||||
}: UseStreamKeyboardNavOptions) {
|
||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||
|
||||
// Reset selection to first item when streams change or view mode switches
|
||||
useEffect(() => {
|
||||
setSelectedIndex(streams.length > 0 ? 0 : null);
|
||||
}, [streams.length, viewMode]);
|
||||
}, [streams.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || streams.length === 0) return;
|
||||
@@ -63,15 +59,8 @@ export function useStreamKeyboardNav({
|
||||
// Arrow keys: move selection
|
||||
let delta: number | null = null;
|
||||
|
||||
if (viewMode === "list") {
|
||||
if (e.key === "ArrowDown") delta = 1;
|
||||
else if (e.key === "ArrowUp") delta = -1;
|
||||
} else {
|
||||
if (e.key === "ArrowDown") delta = gridColumns;
|
||||
else if (e.key === "ArrowUp") delta = -gridColumns;
|
||||
else if (e.key === "ArrowRight") delta = 1;
|
||||
else if (e.key === "ArrowLeft") delta = -1;
|
||||
}
|
||||
if (e.key === "ArrowDown") delta = 1;
|
||||
else if (e.key === "ArrowUp") delta = -1;
|
||||
|
||||
if (delta !== null) {
|
||||
e.preventDefault();
|
||||
@@ -87,7 +76,7 @@ export function useStreamKeyboardNav({
|
||||
// components (ToggleGroup, etc.) consume them for their own navigation.
|
||||
window.addEventListener("keydown", handleKeyDown, true);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown, true);
|
||||
}, [enabled, viewMode, gridColumns, streams, onNavigate]);
|
||||
}, [enabled, streams, onNavigate]);
|
||||
|
||||
return { selectedIndex };
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
type ViewMode = "list" | "grid";
|
||||
|
||||
const STORAGE_KEY = "llink:view-mode";
|
||||
|
||||
function loadViewMode(): ViewMode {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === "grid") return "grid";
|
||||
return "list";
|
||||
} catch {
|
||||
return "list";
|
||||
}
|
||||
}
|
||||
|
||||
function saveViewMode(mode: ViewMode) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, mode);
|
||||
} catch {
|
||||
// Storage unavailable
|
||||
}
|
||||
}
|
||||
|
||||
interface ViewModeState {
|
||||
viewMode: ViewMode;
|
||||
setViewMode: (mode: ViewMode) => void;
|
||||
}
|
||||
|
||||
export const useViewModeStore = create<ViewModeState>((set) => ({
|
||||
viewMode: loadViewMode(),
|
||||
setViewMode: (mode) => {
|
||||
saveViewMode(mode);
|
||||
set({ viewMode: mode });
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user