mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-09-04 09:00:41 +02:00
fix: improve UI interactions and page consistency
This commit is contained in:
@@ -198,11 +198,11 @@ export function AccountPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose} subPage={subPage}>
|
<Dialog open={isOpen} onOpenChange={onClose} subPage={subPage}>
|
||||||
<DialogContent className="flex max-h-[calc(100vh-4rem)] max-w-2xl flex-col">
|
<DialogContent className="flex max-h-[calc(100vh-5rem)] max-w-3xl flex-col">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4",
|
"min-h-0 flex-1 overflow-y-auto",
|
||||||
subPage && "mx-auto w-full max-w-2xl",
|
subPage && "mx-auto w-full max-w-3xl",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<AnimatedTabs defaultValue="account">
|
<AnimatedTabs defaultValue="account">
|
||||||
|
|||||||
@@ -1101,7 +1101,7 @@ export function ExtensionManagementDialog({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={isOpen} onOpenChange={onClose} subPage={subPage}>
|
<Dialog open={isOpen} onOpenChange={onClose} subPage={subPage}>
|
||||||
<DialogContent className="flex max-h-[90vh] max-w-[min(80rem,calc(100%-4rem))] flex-col">
|
<DialogContent className="flex max-h-[85vh] max-w-[min(80rem,calc(100%-4rem))] flex-col">
|
||||||
{!subPage && (
|
{!subPage && (
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -557,7 +557,7 @@ export function GroupManagementDialog({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={isOpen} onOpenChange={onClose} subPage={subPage}>
|
<Dialog open={isOpen} onOpenChange={onClose} subPage={subPage}>
|
||||||
<DialogContent className="flex max-h-[90vh] max-w-[min(60rem,calc(100%-4rem))] flex-col">
|
<DialogContent className="flex max-h-[85vh] max-w-[min(80rem,calc(100%-4rem))] flex-col">
|
||||||
{!subPage && (
|
{!subPage && (
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("groups.management")}</DialogTitle>
|
<DialogTitle>{t("groups.management")}</DialogTitle>
|
||||||
@@ -567,15 +567,13 @@ export function GroupManagementDialog({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex min-h-0 w-full flex-1 flex-col gap-4">
|
<div className="@container flex min-h-0 w-full flex-1 flex-col">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex shrink-0 flex-wrap items-center justify-between gap-2">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="inline-flex h-7 items-center justify-center gap-1.5 rounded-md bg-accent px-3 text-sm font-medium whitespace-nowrap text-foreground">
|
||||||
<h2 className="text-base font-semibold">
|
<span>{t("groups.pageTitle")}</span>
|
||||||
{t("groups.pageTitle")}
|
<span className="text-xs text-muted-foreground tabular-nums">
|
||||||
</h2>
|
{groups.length}
|
||||||
<p className="text-xs text-muted-foreground">
|
</span>
|
||||||
{t("groups.pageDescription")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<RippleButton
|
<RippleButton
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -583,31 +581,34 @@ export function GroupManagementDialog({
|
|||||||
setCreateDialogOpen(true);
|
setCreateDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
className="flex shrink-0 items-center gap-2"
|
className="flex shrink-0 items-center gap-2"
|
||||||
|
aria-label={t("proxies.management.create")}
|
||||||
>
|
>
|
||||||
<GoPlus className="size-4" />
|
<GoPlus className="size-4" />
|
||||||
{t("proxies.management.create")}
|
<span className="hidden @2xl:inline">
|
||||||
|
{t("proxies.management.create")}
|
||||||
|
</span>
|
||||||
</RippleButton>
|
</RippleButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
<div className="mt-4 rounded-md bg-destructive/10 p-3 text-sm text-destructive">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Groups list */}
|
{/* Groups list */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="mt-4 text-sm text-muted-foreground">
|
||||||
{t("common.buttons.loading")}
|
{t("common.buttons.loading")}
|
||||||
</div>
|
</div>
|
||||||
) : groups.length === 0 ? (
|
) : groups.length === 0 ? (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="mt-4 text-sm text-muted-foreground">
|
||||||
{t("groups.noGroupsDescription")}
|
{t("groups.noGroupsDescription")}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<FadingScrollArea
|
<FadingScrollArea
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-h-0 flex-1",
|
"mt-4 min-h-0 flex-1",
|
||||||
selectedGroupsForBulk.length > 0 && "pb-16",
|
selectedGroupsForBulk.length > 0 && "pb-16",
|
||||||
)}
|
)}
|
||||||
style={
|
style={
|
||||||
|
|||||||
@@ -1665,6 +1665,30 @@ export function ProfilesDataTable({
|
|||||||
};
|
};
|
||||||
}, [browserState.isClient, loadAllTags]);
|
}, [browserState.isClient, loadAllTags]);
|
||||||
|
|
||||||
|
// A running browser keeps the name it launched with, so close any inline
|
||||||
|
// rename that was open when the profile entered a runtime transition.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!profileToRename) return;
|
||||||
|
|
||||||
|
const profileId = profileToRename.id;
|
||||||
|
const isRuntimeLocked =
|
||||||
|
(browserState.isClient && runningProfiles.has(profileId)) ||
|
||||||
|
launchingProfiles.has(profileId) ||
|
||||||
|
stoppingProfiles.has(profileId);
|
||||||
|
|
||||||
|
if (isRuntimeLocked) {
|
||||||
|
setProfileToRename(null);
|
||||||
|
setNewProfileName("");
|
||||||
|
setRenameError(null);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
profileToRename,
|
||||||
|
runningProfiles,
|
||||||
|
launchingProfiles,
|
||||||
|
stoppingProfiles,
|
||||||
|
browserState.isClient,
|
||||||
|
]);
|
||||||
|
|
||||||
// Automatically deselect profiles that become running, updating, launching, or stopping
|
// Automatically deselect profiles that become running, updating, launching, or stopping
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const newSet = new Set(selectedProfiles);
|
const newSet = new Set(selectedProfiles);
|
||||||
@@ -2479,7 +2503,15 @@ export function ProfilesDataTable({
|
|||||||
const profile = row.original as BrowserProfile;
|
const profile = row.original as BrowserProfile;
|
||||||
const rawName: string = row.getValue("name");
|
const rawName: string = row.getValue("name");
|
||||||
const name = getBrowserDisplayName(rawName);
|
const name = getBrowserDisplayName(rawName);
|
||||||
const isEditing = meta.profileToRename?.id === profile.id;
|
const isRuntimeLocked =
|
||||||
|
(meta.isClient && meta.runningProfiles.has(profile.id)) ||
|
||||||
|
meta.launchingProfiles.has(profile.id) ||
|
||||||
|
meta.stoppingProfiles.has(profile.id);
|
||||||
|
const isCrossOsBlocked = isCrossOsProfile(profile);
|
||||||
|
const isEditing =
|
||||||
|
meta.profileToRename?.id === profile.id &&
|
||||||
|
!isRuntimeLocked &&
|
||||||
|
!isCrossOsBlocked;
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return (
|
return (
|
||||||
@@ -2530,45 +2562,44 @@ export function ProfilesDataTable({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const isCrossOs = isCrossOsProfile(profile);
|
|
||||||
const isCrossOsBlocked = isCrossOs;
|
|
||||||
const isRunning =
|
|
||||||
meta.isClient && meta.runningProfiles.has(profile.id);
|
|
||||||
const isLaunching = meta.launchingProfiles.has(profile.id);
|
|
||||||
const isStopping = meta.stoppingProfiles.has(profile.id);
|
|
||||||
const isDisabled =
|
|
||||||
isRunning || isLaunching || isStopping || isCrossOsBlocked;
|
|
||||||
const lockedEmail = meta.getProfileLockEmail(profile.id);
|
const lockedEmail = meta.getProfileLockEmail(profile.id);
|
||||||
const isLocked = meta.isProfileLockedByAnother(profile.id);
|
const isLocked = meta.isProfileLockedByAnother(profile.id);
|
||||||
|
const nameControl = isRuntimeLocked ? (
|
||||||
return (
|
<div className="mr-auto h-6 max-w-full min-w-0 cursor-text overflow-hidden rounded px-2 py-1 text-left select-text">
|
||||||
<div className="flex max-w-full min-w-0 items-center gap-1.5 overflow-hidden">
|
{display}
|
||||||
<button
|
</div>
|
||||||
type="button"
|
) : (
|
||||||
className={cn(
|
<button
|
||||||
"mr-auto h-6 max-w-full min-w-0 overflow-hidden rounded border-none bg-transparent px-2 py-1 text-left",
|
type="button"
|
||||||
isDisabled
|
className={cn(
|
||||||
? "cursor-not-allowed opacity-60"
|
"mr-auto h-6 max-w-full min-w-0 overflow-hidden rounded border-none bg-transparent px-2 py-1 text-left",
|
||||||
: "cursor-pointer hover:bg-accent/50",
|
isCrossOsBlocked
|
||||||
)}
|
? "cursor-not-allowed opacity-60"
|
||||||
onClick={() => {
|
: "cursor-pointer hover:bg-accent/50",
|
||||||
if (isDisabled) return;
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (isCrossOsBlocked) return;
|
||||||
|
meta.setProfileToRename(profile);
|
||||||
|
meta.setNewProfileName(profile.name);
|
||||||
|
meta.setRenameError(null);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (isCrossOsBlocked) return;
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
meta.setProfileToRename(profile);
|
meta.setProfileToRename(profile);
|
||||||
meta.setNewProfileName(profile.name);
|
meta.setNewProfileName(profile.name);
|
||||||
meta.setRenameError(null);
|
meta.setRenameError(null);
|
||||||
}}
|
}
|
||||||
onKeyDown={(e) => {
|
}}
|
||||||
if (isDisabled) return;
|
>
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
{display}
|
||||||
e.preventDefault();
|
</button>
|
||||||
meta.setProfileToRename(profile);
|
);
|
||||||
meta.setNewProfileName(profile.name);
|
|
||||||
meta.setRenameError(null);
|
return (
|
||||||
}
|
<div className="flex max-w-full min-w-0 items-center gap-1.5 overflow-hidden">
|
||||||
}}
|
{nameControl}
|
||||||
>
|
|
||||||
{display}
|
|
||||||
</button>
|
|
||||||
{isLocked && (
|
{isLocked && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -2595,14 +2626,7 @@ export function ProfilesDataTable({
|
|||||||
cell: ({ row, table }) => {
|
cell: ({ row, table }) => {
|
||||||
const meta = table.options.meta as TableMeta;
|
const meta = table.options.meta as TableMeta;
|
||||||
const profile = row.original;
|
const profile = row.original;
|
||||||
const isCrossOs = isCrossOsProfile(profile);
|
const isDisabled = isCrossOsProfile(profile);
|
||||||
const isCrossOsBlocked = isCrossOs;
|
|
||||||
const isRunning =
|
|
||||||
meta.isClient && meta.runningProfiles.has(profile.id);
|
|
||||||
const isLaunching = meta.launchingProfiles.has(profile.id);
|
|
||||||
const isStopping = meta.stoppingProfiles.has(profile.id);
|
|
||||||
const isDisabled =
|
|
||||||
isRunning || isLaunching || isStopping || isCrossOsBlocked;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TagsCell
|
<TagsCell
|
||||||
@@ -2628,14 +2652,7 @@ export function ProfilesDataTable({
|
|||||||
cell: ({ row, table }) => {
|
cell: ({ row, table }) => {
|
||||||
const meta = table.options.meta as TableMeta;
|
const meta = table.options.meta as TableMeta;
|
||||||
const profile = row.original;
|
const profile = row.original;
|
||||||
const isCrossOs = isCrossOsProfile(profile);
|
const isDisabled = isCrossOsProfile(profile);
|
||||||
const isCrossOsBlocked = isCrossOs;
|
|
||||||
const isRunning =
|
|
||||||
meta.isClient && meta.runningProfiles.has(profile.id);
|
|
||||||
const isLaunching = meta.launchingProfiles.has(profile.id);
|
|
||||||
const isStopping = meta.stoppingProfiles.has(profile.id);
|
|
||||||
const isDisabled =
|
|
||||||
isRunning || isLaunching || isStopping || isCrossOsBlocked;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NoteCell
|
<NoteCell
|
||||||
|
|||||||
@@ -490,7 +490,9 @@ export function SettingsDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setOriginalSettings(settingsToSave);
|
setOriginalSettings(settingsToSave);
|
||||||
onClose();
|
if (!subPage) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save settings:", error);
|
console.error("Failed to save settings:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -498,6 +500,7 @@ export function SettingsDialog({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
onClose,
|
onClose,
|
||||||
|
subPage,
|
||||||
setTheme,
|
setTheme,
|
||||||
settings,
|
settings,
|
||||||
customThemeState,
|
customThemeState,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function AnimatedTabsList({
|
|||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
data-slot="animated-tabs-list"
|
data-slot="animated-tabs-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative inline-flex max-w-full scrollbar-none items-center gap-1 overflow-x-auto rounded-md p-0",
|
"relative isolate inline-flex max-w-full scrollbar-none items-center gap-1 overflow-x-auto rounded-md p-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onMouseLeave={(event) => {
|
onMouseLeave={(event) => {
|
||||||
@@ -120,7 +120,7 @@ function AnimatedTabsTrigger({
|
|||||||
onMouseEnter?.(event);
|
onMouseEnter?.(event);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative isolate inline-flex h-7 cursor-pointer items-center justify-center gap-1.5 rounded-md px-3 text-sm font-medium whitespace-nowrap transition-colors duration-150",
|
"relative inline-flex h-7 cursor-pointer items-center justify-center gap-1.5 rounded-md px-3 text-sm font-medium whitespace-nowrap transition-colors duration-150",
|
||||||
"text-muted-foreground hover:text-foreground",
|
"text-muted-foreground hover:text-foreground",
|
||||||
isActive && "text-foreground",
|
isActive && "text-foreground",
|
||||||
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none",
|
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none",
|
||||||
@@ -132,7 +132,7 @@ function AnimatedTabsTrigger({
|
|||||||
{showIndicator && (
|
{showIndicator && (
|
||||||
<motion.span
|
<motion.span
|
||||||
layoutId={`animated-tabs-indicator-${indicatorId}`}
|
layoutId={`animated-tabs-indicator-${indicatorId}`}
|
||||||
className="absolute inset-0 -z-10 rounded-md bg-accent"
|
className="pointer-events-none absolute inset-0 -z-10 rounded-md bg-accent"
|
||||||
transition={{ type: "spring", stiffness: 360, damping: 32 }}
|
transition={{ type: "spring", stiffness: 360, damping: 32 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user