feat(FN-2271): optimize chat sidebar layout for mobile and desktop
- Add explicit sidebar section class names in ChatView for header, search, list, and footer layout control - Add a mobile-only footer New Chat action while keeping the desktop header button to avoid duplicate desktop CTAs - Update mobile chat sidebar CSS to use full-height layout with hidden header/search and a scrollable session list - Expand ChatView tests to validate sidebar structure, mobile New Chat behavior, and mobile CSS contract rules
This commit is contained in:
@@ -26,7 +26,7 @@ import {
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { fetchPiSettings, updatePiSettings, installPiPackage, type PiSettings } from "../api";
|
||||
import { fetchPiSettings, updatePiSettings, installPiPackage, fetchPiExtensions, updatePiExtensions, type PiSettings, type PiExtensionEntry } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
interface PiExtensionsManagerProps {
|
||||
@@ -34,6 +34,22 @@ interface PiExtensionsManagerProps {
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
/** Map source value to CSS class suffix */
|
||||
function getSourceClass(source: PiExtensionEntry["source"]): string {
|
||||
return source.replace(/-/g, "-");
|
||||
}
|
||||
|
||||
/** Get display label for extension source */
|
||||
function getSourceLabel(source: PiExtensionEntry["source"]): string {
|
||||
const labels: Record<PiExtensionEntry["source"], string> = {
|
||||
"fusion-global": "Fusion Global",
|
||||
"pi-global": "Pi Global",
|
||||
"fusion-project": "Fusion Project",
|
||||
"pi-project": "Pi Project",
|
||||
};
|
||||
return labels[source] ?? source;
|
||||
}
|
||||
|
||||
/** Determine package source type from the source string */
|
||||
function getPackageType(source: string): "npm" | "git" | "local" {
|
||||
if (source.startsWith("npm:")) return "npm";
|
||||
@@ -46,13 +62,18 @@ function getPackageLabel(source: string): string {
|
||||
return source.replace(/^(npm:|git:)/, "");
|
||||
}
|
||||
|
||||
export function PiExtensionsManager({ addToast }: PiExtensionsManagerProps) {
|
||||
export function PiExtensionsManager({ addToast, projectId }: PiExtensionsManagerProps) {
|
||||
const [settings, setSettings] = useState<PiSettings | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [newSource, setNewSource] = useState("");
|
||||
const [expandedPackages, setExpandedPackages] = useState<Set<number>>(new Set());
|
||||
|
||||
// Discovered extensions state
|
||||
const [extensions, setExtensions] = useState<PiExtensionEntry[]>([]);
|
||||
const [extensionsLoading, setExtensionsLoading] = useState(true);
|
||||
const [updatingExtensions, setUpdatingExtensions] = useState(false);
|
||||
|
||||
const loadSettings = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -65,10 +86,42 @@ export function PiExtensionsManager({ addToast }: PiExtensionsManagerProps) {
|
||||
}
|
||||
}, [addToast]);
|
||||
|
||||
const loadExtensions = useCallback(async () => {
|
||||
try {
|
||||
setExtensionsLoading(true);
|
||||
const data = await fetchPiExtensions(projectId);
|
||||
setExtensions(data.extensions);
|
||||
} catch (err) {
|
||||
addToast(`Failed to load extensions: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||
} finally {
|
||||
setExtensionsLoading(false);
|
||||
}
|
||||
}, [addToast, projectId]);
|
||||
|
||||
const handleToggleExtension = useCallback(async (ext: PiExtensionEntry) => {
|
||||
try {
|
||||
setUpdatingExtensions(true);
|
||||
const disabledIds = ext.enabled
|
||||
? [...extensions.filter((e) => e.enabled && e.id !== ext.id).map((e) => e.id), ext.id]
|
||||
: extensions.filter((e) => e.enabled && e.id !== ext.id).map((e) => e.id);
|
||||
await updatePiExtensions(disabledIds, projectId);
|
||||
await loadExtensions();
|
||||
addToast(ext.enabled ? "Extension disabled" : "Extension enabled", "success");
|
||||
} catch (err) {
|
||||
addToast(`Failed to update extension: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||
} finally {
|
||||
setUpdatingExtensions(false);
|
||||
}
|
||||
}, [extensions, projectId, loadExtensions, addToast]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadSettings();
|
||||
}, [loadSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadExtensions();
|
||||
}, [loadExtensions]);
|
||||
|
||||
const toggleExpanded = (index: number) => {
|
||||
setExpandedPackages((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -330,6 +383,60 @@ export function PiExtensionsManager({ addToast }: PiExtensionsManagerProps) {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Discovered Extensions Section */}
|
||||
<div className="pi-ext-discovered-section">
|
||||
<div className="pi-ext-discovered-header">
|
||||
<h4>Discovered Extensions</h4>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={loadExtensions}
|
||||
disabled={extensionsLoading}
|
||||
title="Refresh extensions"
|
||||
>
|
||||
<RefreshCw size={14} className={extensionsLoading ? "spin" : ""} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="pi-ext-description">
|
||||
Installed extensions resolved from packages and configured paths.
|
||||
</p>
|
||||
|
||||
{extensionsLoading ? (
|
||||
<div className="loading-state">Loading extensions…</div>
|
||||
) : extensions.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<Package size={32} className="text-muted" />
|
||||
<p>No extensions discovered.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pi-ext-list">
|
||||
{extensions.map((ext) => (
|
||||
<div key={ext.id} className="pi-ext-item">
|
||||
<div className="pi-ext-item-content">
|
||||
<div className="pi-ext-info">
|
||||
<span className="pi-ext-name">{ext.name}</span>
|
||||
<span className={`pi-ext-source-badge pi-ext-source-badge--${getSourceClass(ext.source)}`}>
|
||||
{getSourceLabel(ext.source)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="pi-ext-path">{ext.path}</span>
|
||||
</div>
|
||||
<div className="pi-ext-actions">
|
||||
<label className="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={ext.enabled}
|
||||
onChange={() => void handleToggleExtension(ext)}
|
||||
disabled={updatingExtensions}
|
||||
/>
|
||||
<span className="toggle-slider" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user