Add push-style host details panels (#649)
Refs: https://github.com/binaricat/Netcatty/issues/640
This commit is contained in:
@@ -39,6 +39,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
AsidePanel,
|
AsidePanel,
|
||||||
AsidePanelContent,
|
AsidePanelContent,
|
||||||
|
type AsidePanelLayout,
|
||||||
} from "./ui/aside-panel";
|
} from "./ui/aside-panel";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
@@ -63,6 +64,7 @@ interface GroupDetailsPanelProps {
|
|||||||
terminalFontSize: number;
|
terminalFontSize: number;
|
||||||
onSave: (config: GroupConfig, newName?: string, newParent?: string | null) => void;
|
onSave: (config: GroupConfig, newName?: string, newParent?: string | null) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
layout?: AsidePanelLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupDetailsPanel: React.FC<GroupDetailsPanelProps> = ({
|
const GroupDetailsPanel: React.FC<GroupDetailsPanelProps> = ({
|
||||||
@@ -76,6 +78,7 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelProps> = ({
|
|||||||
terminalFontSize,
|
terminalFontSize,
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
layout = "overlay",
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const availableFonts = useAvailableFonts();
|
const availableFonts = useAvailableFonts();
|
||||||
@@ -351,6 +354,7 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelProps> = ({
|
|||||||
onClearProxy={clearProxyConfig}
|
onClearProxy={clearProxyConfig}
|
||||||
onBack={() => setActiveSubPanel("none")}
|
onBack={() => setActiveSubPanel("none")}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
layout={layout}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -368,6 +372,7 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelProps> = ({
|
|||||||
onClearChain={clearHostChain}
|
onClearChain={clearHostChain}
|
||||||
onBack={() => setActiveSubPanel("none")}
|
onBack={() => setActiveSubPanel("none")}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
layout={layout}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -395,6 +400,7 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelProps> = ({
|
|||||||
}}
|
}}
|
||||||
onBack={() => setActiveSubPanel("none")}
|
onBack={() => setActiveSubPanel("none")}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
layout={layout}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -411,6 +417,7 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelProps> = ({
|
|||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
onBack={() => setActiveSubPanel("none")}
|
onBack={() => setActiveSubPanel("none")}
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
|
layout={layout}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -427,6 +434,7 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelProps> = ({
|
|||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
width="w-[380px]"
|
width="w-[380px]"
|
||||||
title={t("vault.groups.details")}
|
title={t("vault.groups.details")}
|
||||||
|
layout={layout}
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
AsidePanel,
|
AsidePanel,
|
||||||
AsidePanelContent,
|
AsidePanelContent,
|
||||||
AsidePanelFooter,
|
AsidePanelFooter,
|
||||||
|
type AsidePanelLayout,
|
||||||
} from "./ui/aside-panel";
|
} from "./ui/aside-panel";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
||||||
@@ -100,6 +101,7 @@ interface HostDetailsPanelProps {
|
|||||||
onCreateGroup?: (groupPath: string) => void; // Callback to create a new group
|
onCreateGroup?: (groupPath: string) => void; // Callback to create a new group
|
||||||
onCreateTag?: (tag: string) => void; // Callback to create a new tag
|
onCreateTag?: (tag: string) => void; // Callback to create a new tag
|
||||||
groupDefaults?: Partial<import('../domain/models').GroupConfig>;
|
groupDefaults?: Partial<import('../domain/models').GroupConfig>;
|
||||||
|
layout?: AsidePanelLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HostDetailsPanel: React.FC<HostDetailsPanelProps> = ({
|
const HostDetailsPanel: React.FC<HostDetailsPanelProps> = ({
|
||||||
@@ -118,6 +120,7 @@ const HostDetailsPanel: React.FC<HostDetailsPanelProps> = ({
|
|||||||
onCreateGroup,
|
onCreateGroup,
|
||||||
onCreateTag,
|
onCreateTag,
|
||||||
groupDefaults,
|
groupDefaults,
|
||||||
|
layout = "overlay",
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { checkSshAgent } = useApplicationBackend();
|
const { checkSshAgent } = useApplicationBackend();
|
||||||
@@ -502,6 +505,7 @@ const HostDetailsPanel: React.FC<HostDetailsPanelProps> = ({
|
|||||||
onSave={handleCreateGroup}
|
onSave={handleCreateGroup}
|
||||||
onBack={() => setActiveSubPanel("none")}
|
onBack={() => setActiveSubPanel("none")}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
layout={layout}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -514,6 +518,7 @@ const HostDetailsPanel: React.FC<HostDetailsPanelProps> = ({
|
|||||||
onClearProxy={clearProxyConfig}
|
onClearProxy={clearProxyConfig}
|
||||||
onBack={() => setActiveSubPanel("none")}
|
onBack={() => setActiveSubPanel("none")}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
layout={layout}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -531,6 +536,7 @@ const HostDetailsPanel: React.FC<HostDetailsPanelProps> = ({
|
|||||||
onClearChain={clearHostChain}
|
onClearChain={clearHostChain}
|
||||||
onBack={() => setActiveSubPanel("none")}
|
onBack={() => setActiveSubPanel("none")}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
layout={layout}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -559,6 +565,7 @@ const HostDetailsPanel: React.FC<HostDetailsPanelProps> = ({
|
|||||||
}}
|
}}
|
||||||
onBack={() => setActiveSubPanel("none")}
|
onBack={() => setActiveSubPanel("none")}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
layout={layout}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -576,6 +583,7 @@ const HostDetailsPanel: React.FC<HostDetailsPanelProps> = ({
|
|||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
onBack={() => setActiveSubPanel("none")}
|
onBack={() => setActiveSubPanel("none")}
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
|
layout={layout}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -614,6 +622,7 @@ const HostDetailsPanel: React.FC<HostDetailsPanelProps> = ({
|
|||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
onBack={() => setActiveSubPanel("none")}
|
onBack={() => setActiveSubPanel("none")}
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
|
layout={layout}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -624,6 +633,7 @@ const HostDetailsPanel: React.FC<HostDetailsPanelProps> = ({
|
|||||||
open={true}
|
open={true}
|
||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
width="w-[420px]"
|
width="w-[420px]"
|
||||||
|
layout={layout}
|
||||||
title={
|
title={
|
||||||
initialData ? t("hostDetails.title.details") : t("hostDetails.title.new")
|
initialData ? t("hostDetails.title.details") : t("hostDetails.title.new")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
AsidePanel,
|
AsidePanel,
|
||||||
AsidePanelContent,
|
AsidePanelContent,
|
||||||
AsidePanelFooter,
|
AsidePanelFooter,
|
||||||
|
type AsidePanelLayout,
|
||||||
} from './ui/aside-panel';
|
} from './ui/aside-panel';
|
||||||
|
|
||||||
interface SerialPort {
|
interface SerialPort {
|
||||||
@@ -35,6 +36,7 @@ interface SerialHostDetailsPanelProps {
|
|||||||
groups?: string[];
|
groups?: string[];
|
||||||
onSave: (host: Host) => void;
|
onSave: (host: Host) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
layout?: AsidePanelLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BAUD_RATES = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600];
|
const BAUD_RATES = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600];
|
||||||
@@ -49,6 +51,7 @@ export const SerialHostDetailsPanel: React.FC<SerialHostDetailsPanelProps> = ({
|
|||||||
groups = [],
|
groups = [],
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
layout = 'overlay',
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const terminalBackend = useTerminalBackend();
|
const terminalBackend = useTerminalBackend();
|
||||||
@@ -164,6 +167,7 @@ export const SerialHostDetailsPanel: React.FC<SerialHostDetailsPanelProps> = ({
|
|||||||
title={t('serial.edit.title')}
|
title={t('serial.edit.title')}
|
||||||
subtitle={initialData.label}
|
subtitle={initialData.label}
|
||||||
className="z-40"
|
className="z-40"
|
||||||
|
layout={layout}
|
||||||
>
|
>
|
||||||
<AsidePanelContent>
|
<AsidePanelContent>
|
||||||
{/* Label */}
|
{/* Label */}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import {
|
import {
|
||||||
AsidePanel,
|
AsidePanel,
|
||||||
AsidePanelContent,
|
AsidePanelContent,
|
||||||
|
type AsidePanelLayout,
|
||||||
} from './ui/aside-panel';
|
} from './ui/aside-panel';
|
||||||
import { ScrollArea } from './ui/scroll-area';
|
import { ScrollArea } from './ui/scroll-area';
|
||||||
import { ThemeList } from './ThemeList';
|
import { ThemeList } from './ThemeList';
|
||||||
@@ -13,6 +14,7 @@ interface ThemeSelectPanelProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
showBackButton?: boolean;
|
showBackButton?: boolean;
|
||||||
|
layout?: AsidePanelLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemeSelectPanel: React.FC<ThemeSelectPanelProps> = ({
|
const ThemeSelectPanel: React.FC<ThemeSelectPanelProps> = ({
|
||||||
@@ -22,6 +24,7 @@ const ThemeSelectPanel: React.FC<ThemeSelectPanelProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onBack,
|
onBack,
|
||||||
showBackButton = true,
|
showBackButton = true,
|
||||||
|
layout = 'overlay',
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<AsidePanel
|
<AsidePanel
|
||||||
@@ -30,6 +33,7 @@ const ThemeSelectPanel: React.FC<ThemeSelectPanelProps> = ({
|
|||||||
title="Select Color Theme"
|
title="Select Color Theme"
|
||||||
showBackButton={showBackButton}
|
showBackButton={showBackButton}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
|
layout={layout}
|
||||||
>
|
>
|
||||||
<AsidePanelContent className="p-0">
|
<AsidePanelContent className="p-0">
|
||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
|
|||||||
@@ -1443,6 +1443,15 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
|||||||
}, [managedSources]);
|
}, [managedSources]);
|
||||||
|
|
||||||
const isHostsSectionActive = currentSection === "hosts";
|
const isHostsSectionActive = currentSection === "hosts";
|
||||||
|
const hasHostsSidePanel =
|
||||||
|
isHostsSectionActive &&
|
||||||
|
((isGroupPanelOpen && !!editingGroupPath) || isHostPanelOpen);
|
||||||
|
const splitViewGridStyle = hasHostsSidePanel
|
||||||
|
? {
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 220px), 280px))",
|
||||||
|
justifyContent: "start" as const,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const isSameDropTarget = useCallback((a: DropTarget | null, b: DropTarget | null) => {
|
const isSameDropTarget = useCallback((a: DropTarget | null, b: DropTarget | null) => {
|
||||||
if (!a || !b) return a === b;
|
if (!a || !b) return a === b;
|
||||||
@@ -1973,9 +1982,13 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
|||||||
</h3>
|
</h3>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
viewMode === "grid"
|
viewMode === "grid"
|
||||||
? "grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
? cn(
|
||||||
|
"grid gap-3",
|
||||||
|
!hasHostsSidePanel && "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
|
||||||
|
)
|
||||||
: "flex flex-col gap-0",
|
: "flex flex-col gap-0",
|
||||||
)}>
|
)}
|
||||||
|
style={viewMode === "grid" ? splitViewGridStyle : undefined}>
|
||||||
{pinnedHosts.map((host) => {
|
{pinnedHosts.map((host) => {
|
||||||
const safeHost = sanitizeHost(host);
|
const safeHost = sanitizeHost(host);
|
||||||
const effectiveDistro = getEffectiveHostDistro(safeHost);
|
const effectiveDistro = getEffectiveHostDistro(safeHost);
|
||||||
@@ -2073,9 +2086,13 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
|||||||
</h3>
|
</h3>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
viewMode === "grid"
|
viewMode === "grid"
|
||||||
? "grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
? cn(
|
||||||
|
"grid gap-3",
|
||||||
|
!hasHostsSidePanel && "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
|
||||||
|
)
|
||||||
: "flex flex-col gap-0",
|
: "flex flex-col gap-0",
|
||||||
)}>
|
)}
|
||||||
|
style={viewMode === "grid" ? splitViewGridStyle : undefined}>
|
||||||
{recentHosts.map((host) => {
|
{recentHosts.map((host) => {
|
||||||
const safeHost = sanitizeHost(host);
|
const safeHost = sanitizeHost(host);
|
||||||
const effectiveDistro = getEffectiveHostDistro(safeHost);
|
const effectiveDistro = getEffectiveHostDistro(safeHost);
|
||||||
@@ -2174,9 +2191,13 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
displayedGroups.length === 0 ? "hidden" : "",
|
displayedGroups.length === 0 ? "hidden" : "",
|
||||||
viewMode === "grid"
|
viewMode === "grid"
|
||||||
? "grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
? cn(
|
||||||
|
"grid gap-3",
|
||||||
|
!hasHostsSidePanel && "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
|
||||||
|
)
|
||||||
: "flex flex-col gap-0",
|
: "flex flex-col gap-0",
|
||||||
)}
|
)}
|
||||||
|
style={viewMode === "grid" ? splitViewGridStyle : undefined}
|
||||||
onDragOver={(e) => {
|
onDragOver={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
@@ -2414,9 +2435,13 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
viewMode === "grid"
|
viewMode === "grid"
|
||||||
? "grid gap-3 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
? cn(
|
||||||
|
"grid gap-3",
|
||||||
|
!hasHostsSidePanel && "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
|
||||||
|
)
|
||||||
: "flex flex-col gap-0",
|
: "flex flex-col gap-0",
|
||||||
)}
|
)}
|
||||||
|
style={viewMode === "grid" ? splitViewGridStyle : undefined}
|
||||||
>
|
>
|
||||||
{group.hosts.filter((h) => selectedGroupPath || !pinnedRecentIds.has(h.id)).map((host) => {
|
{group.hosts.filter((h) => selectedGroupPath || !pinnedRecentIds.has(h.id)).map((host) => {
|
||||||
const safeHost = sanitizeHost(host);
|
const safeHost = sanitizeHost(host);
|
||||||
@@ -2555,9 +2580,13 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
viewMode === "grid"
|
viewMode === "grid"
|
||||||
? "grid gap-3 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
? cn(
|
||||||
|
"grid gap-3",
|
||||||
|
!hasHostsSidePanel && "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
|
||||||
|
)
|
||||||
: "flex flex-col gap-0",
|
: "flex flex-col gap-0",
|
||||||
)}
|
)}
|
||||||
|
style={viewMode === "grid" ? splitViewGridStyle : undefined}
|
||||||
>
|
>
|
||||||
{displayedHosts.filter((h) => selectedGroupPath || !pinnedRecentIds.has(h.id)).map((host) => {
|
{displayedHosts.filter((h) => selectedGroupPath || !pinnedRecentIds.has(h.id)).map((host) => {
|
||||||
const safeHost = sanitizeHost(host);
|
const safeHost = sanitizeHost(host);
|
||||||
@@ -2823,6 +2852,7 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
|||||||
setIsGroupPanelOpen(false);
|
setIsGroupPanelOpen(false);
|
||||||
setEditingGroupPath(null);
|
setEditingGroupPath(null);
|
||||||
}}
|
}}
|
||||||
|
layout="inline"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -2862,6 +2892,7 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
|||||||
Array.from(new Set([...customGroups, groupPath])),
|
Array.from(new Set([...customGroups, groupPath])),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
layout="inline"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -2882,6 +2913,7 @@ const VaultViewInner: React.FC<VaultViewProps> = ({
|
|||||||
setIsHostPanelOpen(false);
|
setIsHostPanelOpen(false);
|
||||||
setEditingHost(null);
|
setEditingHost(null);
|
||||||
}}
|
}}
|
||||||
|
layout="inline"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import React, { useMemo, useState } from 'react';
|
|||||||
import { useI18n } from '../../application/i18n/I18nProvider';
|
import { useI18n } from '../../application/i18n/I18nProvider';
|
||||||
import { Host } from '../../types';
|
import { Host } from '../../types';
|
||||||
import { DistroAvatar } from '../DistroAvatar';
|
import { DistroAvatar } from '../DistroAvatar';
|
||||||
import { AsidePanel } from '../ui/aside-panel';
|
import { AsidePanel, type AsidePanelLayout } from '../ui/aside-panel';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
@@ -24,6 +24,7 @@ export interface ChainPanelProps {
|
|||||||
onClearChain: () => void;
|
onClearChain: () => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
layout?: AsidePanelLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChainPanel: React.FC<ChainPanelProps> = ({
|
export const ChainPanel: React.FC<ChainPanelProps> = ({
|
||||||
@@ -37,6 +38,7 @@ export const ChainPanel: React.FC<ChainPanelProps> = ({
|
|||||||
onClearChain,
|
onClearChain,
|
||||||
onBack,
|
onBack,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
layout = 'overlay',
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -54,6 +56,7 @@ export const ChainPanel: React.FC<ChainPanelProps> = ({
|
|||||||
title={t('hostDetails.chain.title')}
|
title={t('hostDetails.chain.title')}
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
|
layout={layout}
|
||||||
actions={
|
actions={
|
||||||
<Button size="sm" onClick={onBack}>
|
<Button size="sm" onClick={onBack}>
|
||||||
{t('common.save')}
|
{t('common.save')}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { FolderPlus,HelpCircle,Plus } from 'lucide-react';
|
import { FolderPlus,HelpCircle,Plus } from 'lucide-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useI18n } from '../../application/i18n/I18nProvider';
|
import { useI18n } from '../../application/i18n/I18nProvider';
|
||||||
import { AsidePanel,AsidePanelContent } from '../ui/aside-panel';
|
import { AsidePanel,AsidePanelContent,type AsidePanelLayout } from '../ui/aside-panel';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
@@ -42,6 +42,7 @@ export interface CreateGroupPanelProps {
|
|||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
layout?: AsidePanelLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateGroupPanel: React.FC<CreateGroupPanelProps> = ({
|
export const CreateGroupPanel: React.FC<CreateGroupPanelProps> = ({
|
||||||
@@ -53,6 +54,7 @@ export const CreateGroupPanel: React.FC<CreateGroupPanelProps> = ({
|
|||||||
onSave,
|
onSave,
|
||||||
onBack,
|
onBack,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
layout = 'overlay',
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
@@ -62,6 +64,7 @@ export const CreateGroupPanel: React.FC<CreateGroupPanelProps> = ({
|
|||||||
title={t('hostDetails.group.title')}
|
title={t('hostDetails.group.title')}
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
|
layout={layout}
|
||||||
actions={
|
actions={
|
||||||
<Button size="sm" onClick={onSave} disabled={!newGroupName.trim()}>
|
<Button size="sm" onClick={onSave} disabled={!newGroupName.trim()}>
|
||||||
{t('common.save')}
|
{t('common.save')}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Plus,X } from 'lucide-react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useI18n } from '../../application/i18n/I18nProvider';
|
import { useI18n } from '../../application/i18n/I18nProvider';
|
||||||
import { EnvVar } from '../../types';
|
import { EnvVar } from '../../types';
|
||||||
import { AsidePanel,AsidePanelContent } from '../ui/aside-panel';
|
import { AsidePanel,AsidePanelContent,type AsidePanelLayout } from '../ui/aside-panel';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
@@ -25,6 +25,7 @@ export interface EnvVarsPanelProps {
|
|||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
layout?: AsidePanelLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EnvVarsPanel: React.FC<EnvVarsPanelProps> = ({
|
export const EnvVarsPanel: React.FC<EnvVarsPanelProps> = ({
|
||||||
@@ -41,6 +42,7 @@ export const EnvVarsPanel: React.FC<EnvVarsPanelProps> = ({
|
|||||||
onSave,
|
onSave,
|
||||||
onBack,
|
onBack,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
layout = 'overlay',
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
@@ -50,6 +52,7 @@ export const EnvVarsPanel: React.FC<EnvVarsPanelProps> = ({
|
|||||||
title={t('hostDetails.envVars.title')}
|
title={t('hostDetails.envVars.title')}
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
|
layout={layout}
|
||||||
actions={
|
actions={
|
||||||
<Button size="sm" onClick={onSave}>
|
<Button size="sm" onClick={onSave}>
|
||||||
{t('common.save')}
|
{t('common.save')}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import React from 'react';
|
|||||||
import { useI18n } from '../../application/i18n/I18nProvider';
|
import { useI18n } from '../../application/i18n/I18nProvider';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
import { ProxyConfig } from '../../types';
|
import { ProxyConfig } from '../../types';
|
||||||
import { AsidePanel,AsidePanelContent } from '../ui/aside-panel';
|
import { AsidePanel,AsidePanelContent,type AsidePanelLayout } from '../ui/aside-panel';
|
||||||
import { Badge } from '../ui/badge';
|
import { Badge } from '../ui/badge';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
@@ -19,6 +19,7 @@ export interface ProxyPanelProps {
|
|||||||
onClearProxy: () => void;
|
onClearProxy: () => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
layout?: AsidePanelLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProxyPanel: React.FC<ProxyPanelProps> = ({
|
export const ProxyPanel: React.FC<ProxyPanelProps> = ({
|
||||||
@@ -27,6 +28,7 @@ export const ProxyPanel: React.FC<ProxyPanelProps> = ({
|
|||||||
onClearProxy,
|
onClearProxy,
|
||||||
onBack,
|
onBack,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
layout = 'overlay',
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
@@ -36,6 +38,7 @@ export const ProxyPanel: React.FC<ProxyPanelProps> = ({
|
|||||||
title={t('hostDetails.proxyPanel.title')}
|
title={t('hostDetails.proxyPanel.title')}
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
|
layout={layout}
|
||||||
actions={
|
actions={
|
||||||
<Button size="sm" onClick={onBack} disabled={!proxyConfig?.host}>
|
<Button size="sm" onClick={onBack} disabled={!proxyConfig?.host}>
|
||||||
{t('common.save')}
|
{t('common.save')}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ArrowLeft, MoreVertical, X } from 'lucide-react';
|
import { ArrowLeft, MoreVertical, X } from 'lucide-react';
|
||||||
import React, { createContext, ReactNode, useCallback, useContext, useState } from 'react';
|
import React, { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from './popover';
|
import { Popover, PopoverContent, PopoverTrigger } from './popover';
|
||||||
import { ScrollArea } from './scroll-area';
|
import { ScrollArea } from './scroll-area';
|
||||||
@@ -44,6 +44,7 @@ interface AsidePanelProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
width?: string;
|
width?: string;
|
||||||
|
layout?: AsidePanelLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AsidePanelHeaderProps {
|
interface AsidePanelHeaderProps {
|
||||||
@@ -171,14 +172,34 @@ interface AsidePanelStackProps {
|
|||||||
initialItem: AsideContentItem;
|
initialItem: AsideContentItem;
|
||||||
className?: string;
|
className?: string;
|
||||||
width?: string;
|
width?: string;
|
||||||
|
layout?: AsidePanelLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AsidePanelLayout = 'overlay' | 'inline';
|
||||||
|
|
||||||
|
const resolveInlineWidth = (width: string) => {
|
||||||
|
const arbitraryWidthMatch = width.match(/w-\[(.+)\]/);
|
||||||
|
if (arbitraryWidthMatch) {
|
||||||
|
return arbitraryWidthMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (width) {
|
||||||
|
case 'w-full':
|
||||||
|
return '100%';
|
||||||
|
case 'w-screen':
|
||||||
|
return '100vw';
|
||||||
|
default:
|
||||||
|
return '380px';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const AsidePanelStack: React.FC<AsidePanelStackProps> = ({
|
export const AsidePanelStack: React.FC<AsidePanelStackProps> = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
initialItem,
|
initialItem,
|
||||||
className,
|
className,
|
||||||
width = 'w-[380px]',
|
width = 'w-[380px]',
|
||||||
|
layout = 'overlay',
|
||||||
}) => {
|
}) => {
|
||||||
const [stack, setStack] = useState<AsideContentItem[]>([initialItem]);
|
const [stack, setStack] = useState<AsideContentItem[]>([initialItem]);
|
||||||
|
|
||||||
@@ -205,6 +226,13 @@ export const AsidePanelStack: React.FC<AsidePanelStackProps> = ({
|
|||||||
|
|
||||||
const currentItem = stack[stack.length - 1];
|
const currentItem = stack[stack.length - 1];
|
||||||
const canGoBack = stack.length > 1;
|
const canGoBack = stack.length > 1;
|
||||||
|
const inlineWidth = useMemo(() => resolveInlineWidth(width), [width]);
|
||||||
|
const inlineStyle = layout === 'inline'
|
||||||
|
? ({
|
||||||
|
width: inlineWidth,
|
||||||
|
['--aside-inline-width' as string]: inlineWidth,
|
||||||
|
} as React.CSSProperties)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// Reset stack when panel closes/opens
|
// Reset stack when panel closes/opens
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -218,10 +246,13 @@ export const AsidePanelStack: React.FC<AsidePanelStackProps> = ({
|
|||||||
return (
|
return (
|
||||||
<AsidePanelContext.Provider value={{ push, pop, replace, clear, canGoBack, currentItem }}>
|
<AsidePanelContext.Provider value={{ push, pop, replace, clear, canGoBack, currentItem }}>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"absolute right-0 top-0 bottom-0 max-w-full border-l border-border/60 bg-background z-30 flex flex-col app-no-drag overflow-hidden",
|
layout === 'inline'
|
||||||
width,
|
? "relative split-panel-enter shrink-0 h-full min-h-0 max-w-full border-l border-border/60 bg-background z-30 flex flex-col app-no-drag overflow-hidden shadow-[-16px_0_32px_hsl(var(--foreground)/0.08)]"
|
||||||
|
: "absolute right-0 top-0 bottom-0 max-w-full border-l border-border/60 bg-background z-30 flex flex-col app-no-drag overflow-hidden",
|
||||||
|
layout === 'overlay' && width,
|
||||||
className
|
className
|
||||||
)}>
|
)}
|
||||||
|
style={inlineStyle}>
|
||||||
<AsidePanelHeader
|
<AsidePanelHeader
|
||||||
title={currentItem.title}
|
title={currentItem.title}
|
||||||
subtitle={currentItem.subtitle}
|
subtitle={currentItem.subtitle}
|
||||||
@@ -248,15 +279,27 @@ export const AsidePanel: React.FC<AsidePanelProps> = ({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
width = 'w-[380px]',
|
width = 'w-[380px]',
|
||||||
|
layout = 'overlay',
|
||||||
}) => {
|
}) => {
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
|
const inlineWidth = resolveInlineWidth(width);
|
||||||
|
const inlineStyle = layout === 'inline'
|
||||||
|
? ({
|
||||||
|
width: inlineWidth,
|
||||||
|
['--aside-inline-width' as string]: inlineWidth,
|
||||||
|
} as React.CSSProperties)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"absolute right-0 top-0 bottom-0 max-w-full border-l border-border/60 bg-background z-30 flex flex-col app-no-drag overflow-hidden",
|
layout === 'inline'
|
||||||
width,
|
? "relative split-panel-enter shrink-0 h-full min-h-0 max-w-full border-l border-border/60 bg-background z-30 flex flex-col app-no-drag overflow-hidden shadow-[-16px_0_32px_hsl(var(--foreground)/0.08)]"
|
||||||
|
: "absolute right-0 top-0 bottom-0 max-w-full border-l border-border/60 bg-background z-30 flex flex-col app-no-drag overflow-hidden",
|
||||||
|
layout === 'overlay' && width,
|
||||||
className
|
className
|
||||||
)}>
|
)}
|
||||||
|
style={inlineStyle}>
|
||||||
{title && (
|
{title && (
|
||||||
<AsidePanelHeader
|
<AsidePanelHeader
|
||||||
title={title}
|
title={title}
|
||||||
|
|||||||
23
index.css
23
index.css
@@ -102,6 +102,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes split-panel-enter {
|
||||||
|
0% {
|
||||||
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(22px);
|
||||||
|
}
|
||||||
|
55% {
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: var(--aside-inline-width);
|
||||||
|
min-width: var(--aside-inline-width);
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-panel-enter {
|
||||||
|
animation: split-panel-enter 220ms cubic-bezier(0.24, 0.84, 0.32, 1) both;
|
||||||
|
will-change: width, opacity, transform;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user