mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-13 19:20:48 +02:00
style: tight table design
This commit is contained in:
1 parent
35ba7e2d96
commit
c7c910d1ca
3 files changed
+61
-38
No files matched your search
@@ -100,7 +100,7 @@ type TableMeta = {
|
|||||||
profileId: string,
|
profileId: string,
|
||||||
proxyId: string | null,
|
proxyId: string | null,
|
||||||
) => void | Promise<void>;
|
) => void | Promise<void>;
|
||||||
checkingProxyId: string | null;
|
checkingProfileId: string | null;
|
||||||
proxyCheckResults: Record<string, ProxyCheckResult>;
|
proxyCheckResults: Record<string, ProxyCheckResult>;
|
||||||
|
|
||||||
// Selection helpers
|
// Selection helpers
|
||||||
@@ -209,6 +209,7 @@ const TagsCell = React.memo<{
|
|||||||
const [visibleCount, setVisibleCount] = React.useState<number>(
|
const [visibleCount, setVisibleCount] = React.useState<number>(
|
||||||
effectiveTags.length,
|
effectiveTags.length,
|
||||||
);
|
);
|
||||||
|
const [isFocused, setIsFocused] = React.useState(false);
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
// Only measure when not editing this profile's tags
|
// Only measure when not editing this profile's tags
|
||||||
@@ -298,7 +299,7 @@ const TagsCell = React.memo<{
|
|||||||
type="button"
|
type="button"
|
||||||
ref={containerRef as unknown as React.RefObject<HTMLButtonElement>}
|
ref={containerRef as unknown as React.RefObject<HTMLButtonElement>}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex overflow-hidden gap-1 items-center p-2.5 w-full bg-transparent rounded border-none cursor-pointer",
|
"flex overflow-hidden gap-1 items-center px-2 py-1 h-6 w-full bg-transparent rounded border-none cursor-pointer",
|
||||||
isDisabled ? "opacity-60" : "cursor-pointer hover:bg-accent/50",
|
isDisabled ? "opacity-60" : "cursor-pointer hover:bg-accent/50",
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -322,7 +323,7 @@ const TagsCell = React.memo<{
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-48 h-full cursor-pointer">
|
<div className="w-48 h-6 cursor-pointer">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>{ButtonContent}</TooltipTrigger>
|
<TooltipTrigger asChild>{ButtonContent}</TooltipTrigger>
|
||||||
{hiddenCount > 0 && (
|
{hiddenCount > 0 && (
|
||||||
@@ -348,11 +349,14 @@ const TagsCell = React.memo<{
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-48 h-8",
|
"w-48 h-6 relative",
|
||||||
isDisabled && "opacity-60 pointer-events-none",
|
isDisabled && "opacity-60 pointer-events-none",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div ref={editorRef}>
|
<div
|
||||||
|
ref={editorRef}
|
||||||
|
className="absolute top-0 left-0 z-50 w-48 min-h-6 bg-popover rounded-md shadow-md"
|
||||||
|
>
|
||||||
<MultipleSelector
|
<MultipleSelector
|
||||||
value={valueOptions}
|
value={valueOptions}
|
||||||
options={allOptions}
|
options={allOptions}
|
||||||
@@ -360,13 +364,22 @@ const TagsCell = React.memo<{
|
|||||||
creatable
|
creatable
|
||||||
selectFirstItem={false}
|
selectFirstItem={false}
|
||||||
placeholder={effectiveTags.length === 0 ? "Add tags" : ""}
|
placeholder={effectiveTags.length === 0 ? "Add tags" : ""}
|
||||||
className="overflow-x-hidden overflow-y-scroll h-7 bg-transparent"
|
className={cn(
|
||||||
badgeClassName=""
|
"bg-transparent !border-0 focus-within:!ring-0",
|
||||||
|
"[&_div:first-child]:!border-0 [&_div:first-child]:!ring-0 [&_div:first-child]:focus-within:!ring-0",
|
||||||
|
"[&_div:first-child]:!min-h-6 [&_div:first-child]:!px-2 [&_div:first-child]:!py-1",
|
||||||
|
"[&_div:first-child>div]:items-center [&_div:first-child>div]:!h-6",
|
||||||
|
"[&_input]:!ml-0 [&_input]:!mt-0 [&_input]:!px-0",
|
||||||
|
!isFocused && "[&_div:first-child>div]:justify-center",
|
||||||
|
)}
|
||||||
|
badgeClassName="shrink-0"
|
||||||
inputProps={{
|
inputProps={{
|
||||||
className: "py-1",
|
className: "!py-0 text-sm caret-current !ml-0 !mt-0 !px-0",
|
||||||
onKeyDown: (e) => {
|
onKeyDown: (e) => {
|
||||||
if (e.key === "Escape") setOpenTagsEditorFor(null);
|
if (e.key === "Escape") setOpenTagsEditorFor(null);
|
||||||
},
|
},
|
||||||
|
onFocus: () => setIsFocused(true),
|
||||||
|
onBlur: () => setIsFocused(false),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -440,9 +453,9 @@ export function ProfilesDataTable({
|
|||||||
const [openProxySelectorFor, setOpenProxySelectorFor] = React.useState<
|
const [openProxySelectorFor, setOpenProxySelectorFor] = React.useState<
|
||||||
string | null
|
string | null
|
||||||
>(null);
|
>(null);
|
||||||
const [checkingProxyId, setCheckingProxyId] = React.useState<string | null>(
|
const [checkingProfileId, setCheckingProfileId] = React.useState<
|
||||||
null,
|
string | null
|
||||||
);
|
>(null);
|
||||||
const [proxyCheckResults, setProxyCheckResults] = React.useState<
|
const [proxyCheckResults, setProxyCheckResults] = React.useState<
|
||||||
Record<string, ProxyCheckResult>
|
Record<string, ProxyCheckResult>
|
||||||
>({});
|
>({});
|
||||||
@@ -818,7 +831,7 @@ export function ProfilesDataTable({
|
|||||||
proxyOverrides,
|
proxyOverrides,
|
||||||
storedProxies,
|
storedProxies,
|
||||||
handleProxySelection,
|
handleProxySelection,
|
||||||
checkingProxyId,
|
checkingProfileId,
|
||||||
proxyCheckResults,
|
proxyCheckResults,
|
||||||
|
|
||||||
// Selection helpers
|
// Selection helpers
|
||||||
@@ -864,7 +877,7 @@ export function ProfilesDataTable({
|
|||||||
proxyOverrides,
|
proxyOverrides,
|
||||||
storedProxies,
|
storedProxies,
|
||||||
handleProxySelection,
|
handleProxySelection,
|
||||||
checkingProxyId,
|
checkingProfileId,
|
||||||
proxyCheckResults,
|
proxyCheckResults,
|
||||||
handleToggleAll,
|
handleToggleAll,
|
||||||
handleCheckboxChange,
|
handleCheckboxChange,
|
||||||
@@ -1033,7 +1046,7 @@ export function ProfilesDataTable({
|
|||||||
size="sm"
|
size="sm"
|
||||||
disabled={!canLaunch || isLaunching || isStopping}
|
disabled={!canLaunch || isLaunching || isStopping}
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer min-w-[70px]",
|
"cursor-pointer min-w-[70px] h-7",
|
||||||
!canLaunch && "opacity-50",
|
!canLaunch && "opacity-50",
|
||||||
)}
|
)}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -1118,7 +1131,7 @@ export function ProfilesDataTable({
|
|||||||
meta.setRenameError(null);
|
meta.setRenameError(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="inline-block w-30"
|
className="w-30 h-6 px-2 py-1 text-sm font-medium leading-none border-0 shadow-none focus-visible:ring-0"
|
||||||
/>
|
/>
|
||||||
<div className="flex absolute right-0 top-full z-50 gap-1 translate-y-[30%] opacity-100 bg-black rounded-md">
|
<div className="flex absolute right-0 top-full z-50 gap-1 translate-y-[30%] opacity-100 bg-black rounded-md">
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
@@ -1138,11 +1151,11 @@ export function ProfilesDataTable({
|
|||||||
|
|
||||||
const display =
|
const display =
|
||||||
name.length < 14 ? (
|
name.length < 14 ? (
|
||||||
<div className="font-medium text-left">{name}</div>
|
<div className="font-medium text-left leading-none">{name}</div>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span>{trimName(name, 14)}</span>
|
<span className="leading-none">{trimName(name, 14)}</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{name}</TooltipContent>
|
<TooltipContent>{name}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -1160,7 +1173,7 @@ export function ProfilesDataTable({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-2 mr-auto text-left bg-transparent rounded border-none w-30",
|
"px-2 py-1 mr-auto text-left bg-transparent rounded border-none w-30 h-6",
|
||||||
isDisabled
|
isDisabled
|
||||||
? "opacity-60 cursor-not-allowed"
|
? "opacity-60 cursor-not-allowed"
|
||||||
: "cursor-pointer hover:bg-accent/50",
|
: "cursor-pointer hover:bg-accent/50",
|
||||||
@@ -1330,7 +1343,7 @@ export function ProfilesDataTable({
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex gap-2 items-center p-2 rounded",
|
"flex gap-2 items-center px-2 py-1 rounded",
|
||||||
isDisabled
|
isDisabled
|
||||||
? "opacity-60 cursor-not-allowed pointer-events-none"
|
? "opacity-60 cursor-not-allowed pointer-events-none"
|
||||||
: "cursor-pointer hover:bg-accent/50",
|
: "cursor-pointer hover:bg-accent/50",
|
||||||
@@ -1355,7 +1368,11 @@ export function ProfilesDataTable({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{!isDisabled && (
|
{!isDisabled && (
|
||||||
<PopoverContent className="w-[240px] p-0" align="start">
|
<PopoverContent
|
||||||
|
className="w-[240px] p-0"
|
||||||
|
align="end"
|
||||||
|
sideOffset={8}
|
||||||
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput placeholder="Search proxies..." />
|
<CommandInput placeholder="Search proxies..." />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
@@ -1408,9 +1425,10 @@ export function ProfilesDataTable({
|
|||||||
{profileHasProxy && effectiveProxy && !isDisabled && (
|
{profileHasProxy && effectiveProxy && !isDisabled && (
|
||||||
<ProxyCheckButton
|
<ProxyCheckButton
|
||||||
proxy={effectiveProxy}
|
proxy={effectiveProxy}
|
||||||
checkingProxyId={meta.checkingProxyId}
|
profileId={profile.id}
|
||||||
|
checkingProfileId={meta.checkingProfileId}
|
||||||
cachedResult={meta.proxyCheckResults[effectiveProxy.id]}
|
cachedResult={meta.proxyCheckResults[effectiveProxy.id]}
|
||||||
setCheckingProxyId={setCheckingProxyId}
|
setCheckingProfileId={setCheckingProfileId}
|
||||||
onCheckComplete={(result) => {
|
onCheckComplete={(result) => {
|
||||||
setProxyCheckResults((prev) => ({
|
setProxyCheckResults((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|||||||
@@ -16,22 +16,24 @@ import type { ProxyCheckResult, StoredProxy } from "@/types";
|
|||||||
|
|
||||||
interface ProxyCheckButtonProps {
|
interface ProxyCheckButtonProps {
|
||||||
proxy: StoredProxy;
|
proxy: StoredProxy;
|
||||||
checkingProxyId: string | null;
|
profileId: string;
|
||||||
|
checkingProfileId: string | null;
|
||||||
cachedResult?: ProxyCheckResult;
|
cachedResult?: ProxyCheckResult;
|
||||||
onCheckComplete?: (result: ProxyCheckResult) => void;
|
onCheckComplete?: (result: ProxyCheckResult) => void;
|
||||||
onCheckFailed?: (result: ProxyCheckResult) => void;
|
onCheckFailed?: (result: ProxyCheckResult) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
setCheckingProxyId?: (id: string | null) => void;
|
setCheckingProfileId?: (id: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProxyCheckButton({
|
export function ProxyCheckButton({
|
||||||
proxy,
|
proxy,
|
||||||
checkingProxyId,
|
profileId,
|
||||||
|
checkingProfileId,
|
||||||
cachedResult,
|
cachedResult,
|
||||||
onCheckComplete,
|
onCheckComplete,
|
||||||
onCheckFailed,
|
onCheckFailed,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
setCheckingProxyId,
|
setCheckingProfileId,
|
||||||
}: ProxyCheckButtonProps) {
|
}: ProxyCheckButtonProps) {
|
||||||
const [localResult, setLocalResult] = React.useState<
|
const [localResult, setLocalResult] = React.useState<
|
||||||
ProxyCheckResult | undefined
|
ProxyCheckResult | undefined
|
||||||
@@ -42,9 +44,9 @@ export function ProxyCheckButton({
|
|||||||
}, [cachedResult]);
|
}, [cachedResult]);
|
||||||
|
|
||||||
const handleCheck = React.useCallback(async () => {
|
const handleCheck = React.useCallback(async () => {
|
||||||
if (checkingProxyId === proxy.id) return;
|
if (checkingProfileId === profileId) return;
|
||||||
|
|
||||||
setCheckingProxyId?.(proxy.id);
|
setCheckingProfileId?.(profileId);
|
||||||
try {
|
try {
|
||||||
const result = await invoke<ProxyCheckResult>("check_proxy_validity", {
|
const result = await invoke<ProxyCheckResult>("check_proxy_validity", {
|
||||||
proxyId: proxy.id,
|
proxyId: proxy.id,
|
||||||
@@ -86,17 +88,18 @@ export function ProxyCheckButton({
|
|||||||
setLocalResult(failedResult);
|
setLocalResult(failedResult);
|
||||||
onCheckFailed?.(failedResult);
|
onCheckFailed?.(failedResult);
|
||||||
} finally {
|
} finally {
|
||||||
setCheckingProxyId?.(null);
|
setCheckingProfileId?.(null);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
proxy,
|
proxy,
|
||||||
checkingProxyId,
|
profileId,
|
||||||
|
checkingProfileId,
|
||||||
onCheckComplete,
|
onCheckComplete,
|
||||||
onCheckFailed,
|
onCheckFailed,
|
||||||
setCheckingProxyId,
|
setCheckingProfileId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isCurrentlyChecking = checkingProxyId === proxy.id;
|
const isCurrentlyChecking = checkingProfileId === profileId;
|
||||||
const result = localResult;
|
const result = localResult;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -117,7 +120,7 @@ export function ProxyCheckButton({
|
|||||||
<FiCheck className="absolute bottom-[-6px] right-[-4px]" />
|
<FiCheck className="absolute bottom-[-6px] right-[-4px]" />
|
||||||
</span>
|
</span>
|
||||||
) : result && !result.is_valid ? (
|
) : result && !result.is_valid ? (
|
||||||
<span className="text-red-600 text-sm">✕</span>
|
<span className="text-destructive text-sm">✕</span>
|
||||||
) : (
|
) : (
|
||||||
<FiCheck className="w-3 h-3" />
|
<FiCheck className="w-3 h-3" />
|
||||||
)}
|
)}
|
||||||
@@ -135,15 +138,17 @@ export function ProxyCheckButton({
|
|||||||
{[result.city, result.country].filter(Boolean).join(", ") ||
|
{[result.city, result.country].filter(Boolean).join(", ") ||
|
||||||
"Unknown"}
|
"Unknown"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">IP: {result.ip}</p>
|
<p className="text-xs text-primary-foreground/70">
|
||||||
<p className="text-xs text-muted-foreground">
|
IP: {result.ip}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-primary-foreground/70">
|
||||||
Checked {formatRelativeTime(result.timestamp)}
|
Checked {formatRelativeTime(result.timestamp)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : result && !result.is_valid ? (
|
) : result && !result.is_valid ? (
|
||||||
<div>
|
<div>
|
||||||
<p>Proxy check failed</p>
|
<p>Proxy check failed</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-primary-foreground/70">
|
||||||
Failed {formatRelativeTime(result.timestamp)}
|
Failed {formatRelativeTime(result.timestamp)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|||||||
<th
|
<th
|
||||||
data-slot="table-head"
|
data-slot="table-head"
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 h-10 font-medium text-left align-middle whitespace-nowrap text-foreground",
|
"px-2 h-8 font-medium text-left align-middle whitespace-nowrap text-foreground",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -79,7 +79,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
data-slot="table-cell"
|
data-slot="table-cell"
|
||||||
className={cn("p-2 align-middle whitespace-nowrap", className)}
|
className={cn("px-2 py-1 align-middle whitespace-nowrap", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user