feat(web): move system information into personal settings

This commit is contained in:
tianma-if committed 2026-09-29 17:42:28 +08:00
1 parent b8a0b88d88
commit d04814009f
13 files changed
+70 -281

No files matched your search

-3
View File
@@ -419,7 +419,6 @@ const RichEditorPane = ({
const resourceInsertionLimit = useMemo(createFileBatchQueue, []);
const isSelectionMode = Boolean(selectionActionBar);
const [title, setTitle] = useState("");
const [systemInfoOpen, setSystemInfoOpen] = useState(false);
const [tagsText, setTagsText] = useState("");
const {
dirtyVersion,
@@ -2918,7 +2917,6 @@ const RichEditorPane = ({
historyOpen ||
shareOpen ||
aiAssistantOpen ||
systemInfoOpen ||
mobileNotebookSheetOpen ||
noteLinkPickerOpen ||
externalLinkDialogOpen ||
@@ -3853,7 +3851,6 @@ const RichEditorPane = ({
moreMenuClassName="w-56 rounded-md"
onOpenExecutionCenter={onOpenExecutionCenter}
onSearch={() => openNoteSearch()}
onSystemInfoOpenChange={setSystemInfoOpen}
textNoteActions={(
<>
{!effectiveReadOnly && (
@@ -15,7 +15,8 @@ describe("shared memo editor header actions", () => {
test("owns every action shared by text and diagram notes", () => {
expect(actionsSource).toContain("<Search");
expect(actionsSource).toContain("<GitHubRepositoryLink");
expect(actionsSource).toContain("<SystemInfoDialog");
expect(actionsSource).not.toContain("SystemInfoDialog");
expect(actionsSource).not.toContain("systemInfo.title");
expect(actionsSource).toContain("<ExecutionCenterButton");
expect(actionsSource).toContain("setPreference(nextTheme)");
expect(actionsSource).toContain("<MoreHorizontal");
@@ -1,9 +1,8 @@
import { useState, type ReactNode } from "react";
import { Info, MoonStar, MoreHorizontal, Search, SunMedium } from "lucide-react";
import { type ReactNode } from "react";
import { MoonStar, MoreHorizontal, Search, SunMedium } from "lucide-react";
import { useTranslation } from "react-i18next";
import { ExecutionCenterButton } from "@/components/execution/ExecutionCenterButton";
import { GitHubRepositoryLink } from "@/components/GitHubRepositoryLink";
import { SystemInfoDialog } from "@/components/SystemInfoDialog";
import { useAppearanceTheme } from "@/components/ThemeProvider";
import { Button, buttonVariants } from "@/components/ui/button";
import {
@@ -12,7 +11,6 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useDeployedUpdateNotice } from "@/hooks/useDeployedUpdateNotice";
import { cn } from "@/lib/utils";
export const MemoEditorHeaderActions = ({
@@ -21,7 +19,6 @@ export const MemoEditorHeaderActions = ({
moreMenuItems,
onOpenExecutionCenter,
onSearch,
onSystemInfoOpenChange,
textNoteActions,
textNoteMenuItems,
}: {
@@ -30,20 +27,13 @@ export const MemoEditorHeaderActions = ({
moreMenuItems: ReactNode;
onOpenExecutionCenter: () => void;
onSearch: () => void;
onSystemInfoOpenChange?: (open: boolean) => void;
textNoteActions?: ReactNode;
textNoteMenuItems?: ReactNode;
}) => {
const { t } = useTranslation();
const [systemInfoOpen, setSystemInfoOpen] = useState(false);
const { unseen: deployedUpdateUnseen } = useDeployedUpdateNotice();
const { resolvedTheme, setPreference } = useAppearanceTheme();
const nextTheme = resolvedTheme === "dark" ? "light" : "dark";
const themeLabel = nextTheme === "dark" ? t("settings.themeToggleToDark") : t("settings.themeToggleToLight");
const handleSystemInfoOpenChange = (open: boolean) => {
setSystemInfoOpen(open);
onSystemInfoOpenChange?.(open);
};
return (
<>
@@ -59,14 +49,13 @@ export const MemoEditorHeaderActions = ({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className={cn("relative", moreButtonClassName)}
className={moreButtonClassName}
size="icon"
variant="ghost"
title={t("editor.more")}
aria-label={t("editor.moreAria")}
>
<MoreHorizontal className="h-4 w-4" />
{deployedUpdateUnseen ? <span className="absolute right-1 top-1 h-1.5 w-1.5 rounded-full bg-emerald-500 ring-2 ring-card" /> : null}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
@@ -96,19 +85,8 @@ export const MemoEditorHeaderActions = ({
{t("common.githubRepository")}
</GitHubRepositoryLink>
{moreMenuItems}
<DropdownMenuItem
className="flex h-9 w-full items-center gap-2 px-3 text-left text-xs text-slate-700 hover:bg-slate-50 cursor-pointer outline-none"
onClick={() => handleSystemInfoOpenChange(true)}
>
<span className="relative flex h-4 w-4 shrink-0 items-center justify-center">
<Info className="h-4 w-4 text-slate-500" />
{deployedUpdateUnseen ? <span className="absolute -right-0.5 -top-0.5 h-1.5 w-1.5 rounded-full bg-emerald-500 ring-2 ring-card" /> : null}
</span>
{t("systemInfo.title")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<SystemInfoDialog open={systemInfoOpen} onOpenChange={handleSystemInfoOpenChange} />
</>
);
};
+9 -2
View File
@@ -67,6 +67,7 @@ import type { EdgeEverRepository } from "@/lib/repository";
import type { EdgeEverPluginHost } from "@/lib/plugins/plugin-host";
import { statusSettleMotion } from "@/lib/motion";
import { DesktopUpdateNotice } from "./DesktopUpdateNotice";
import { useDeployedUpdateNotice } from "@/hooks/useDeployedUpdateNotice";
import { PluginToolbarMenu } from "./plugins/PluginToolbarMenu";
const DesktopSyncIssuesDialog = lazy(() => import("./DesktopSyncIssuesDialog").then((module) => ({ default: module.DesktopSyncIssuesDialog })));
@@ -274,12 +275,14 @@ const SidebarRailButton = ({
active = false,
icon,
label,
notice = false,
onClick,
disabled = false,
}: {
active?: boolean;
icon: ReactNode;
label: string;
notice?: boolean;
onClick: () => void;
disabled?: boolean;
}) => (
@@ -292,11 +295,12 @@ const SidebarRailButton = ({
aria-label={label}
onClick={onClick}
className={cn(
"flex h-9 w-9 items-center justify-center rounded-lg text-slate-600 transition-colors duration-150 hover:bg-slate-100 hover:text-slate-950 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-900/20 disabled:cursor-not-allowed disabled:opacity-50",
"relative flex h-9 w-9 items-center justify-center rounded-lg text-slate-600 transition-colors duration-150 hover:bg-slate-100 hover:text-slate-950 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-900/20 disabled:cursor-not-allowed disabled:opacity-50",
active && "edgeever-workspace-selection text-slate-950"
)}
>
{icon}
{notice ? <span className="absolute right-1 top-1 h-1.5 w-1.5 rounded-full bg-emerald-500 ring-2 ring-[var(--workspace-sidebar)]" /> : null}
</button>
</TooltipTrigger>
<TooltipContent side="right">{label}</TooltipContent>
@@ -538,6 +542,7 @@ export const NotebookPane = ({
onToggleCollapsed?: () => void;
}) => {
const { t } = useTranslation();
const { unseen: deployedUpdateUnseen } = useDeployedUpdateNotice();
// Temporarily keep template actions out of the primary workspace navigation.
const showTemplateEntry = true;
const notebookScrollRef = useRef<HTMLDivElement | null>(null);
@@ -926,6 +931,7 @@ export const NotebookPane = ({
<SidebarRailButton
icon={<CircleUserRound className="h-4 w-4" />}
label={t("notebookPane.profile")}
notice={deployedUpdateUnseen}
onClick={onOpenSettings}
/>
<SidebarCollapseButton collapsed onToggle={onToggleCollapsed} className="h-9 w-9" tooltipSide="right" />
@@ -1153,8 +1159,9 @@ export const NotebookPane = ({
type="button"
aria-label={t("notebookPane.profile")}
>
<span className="flex h-4 w-4 shrink-0 items-center justify-center">
<span className="relative flex h-4 w-4 shrink-0 items-center justify-center">
<CircleUserRound className="h-4 w-4" />
{deployedUpdateUnseen ? <span className="absolute -right-1 -top-1 h-1.5 w-1.5 rounded-full bg-emerald-500 ring-2 ring-[var(--workspace-sidebar)]" /> : null}
</span>
<span className="min-w-0 flex-1 truncate">{t("notebookPane.profile")}</span>
</button>
+16 -24
View File
@@ -16,7 +16,6 @@ import {
import { useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import * as m from "motion/react-m";
import { SystemInfoDialog } from "@/components/SystemInfoDialog";
import { Button } from "@/components/ui/button";
import type { ShortcutSettings } from "@/lib/app-helpers";
@@ -30,6 +29,7 @@ import { DesktopLocalDataCard } from "./settings/DesktopLocalDataCard";
import { LoginDevicesCard } from "./settings/LoginDevicesCard";
import { EvernoteImportGuideCard } from "./settings/EvernoteImportGuideCard";
import { FeedbackLink } from "./settings/FeedbackLink";
import { SystemInfoPanel } from "./settings/SystemInfoPanel";
import { McpConfigCard } from "./settings/McpConfigCard";
import { PreferenceCard } from "./settings/PreferenceCard";
import { ShortcutSettingsItem } from "./settings/ShortcutSettingsItem";
@@ -72,7 +72,7 @@ const SettingsGroup = ({ children }: { children: ReactNode }) => (
</div>
);
type TabKey = "general" | "shortcuts" | "users" | "data" | "ai" | "mcp" | "advanced" | "account";
type TabKey = "general" | "shortcuts" | "users" | "data" | "ai" | "mcp" | "advanced" | "account" | "system";
interface TabItem {
key: TabKey;
@@ -105,7 +105,6 @@ export const SettingsPane = ({
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<TabKey>("general");
const [activeMobileTab, setActiveMobileTab] = useState<TabKey | null>(null);
const [systemInfoOpen, setSystemInfoOpen] = useState(false);
const { unseen: deployedUpdateUnseen } = useDeployedUpdateNotice();
const canClearLocalData = Boolean(window.edgeeverDesktop?.canClearLocalData);
@@ -158,6 +157,11 @@ export const SettingsPane = ({
label: t("settings.tabs.account"),
icon: Shield,
},
{
key: "system",
label: t("systemInfo.title"),
icon: Info,
},
];
const mobileTabItems = tabItems.filter((item) => item.key !== "shortcuts");
@@ -257,6 +261,8 @@ export const SettingsPane = ({
)}
</SettingsGroup>
);
case "system":
return <SystemInfoPanel />;
default:
return null;
}
@@ -309,7 +315,10 @@ export const SettingsPane = ({
: "font-normal text-slate-600 hover:bg-workspace-hover hover:text-slate-900"
)}
>
<Icon className={cn("h-4 w-4 shrink-0 transition-colors", isSelected ? "text-slate-950" : "text-slate-400")} />
<span className="relative flex h-4 w-4 shrink-0 items-center justify-center">
<Icon className={cn("h-4 w-4 transition-colors", isSelected ? "text-slate-950" : "text-slate-400")} />
{item.key === "system" && deployedUpdateUnseen ? <span className="absolute -right-1 -top-1 h-1.5 w-1.5 rounded-full bg-emerald-500 ring-2 ring-workspace-canvas" /> : null}
</span>
<span className="flex-1 truncate">{item.label}</span>
{item.badge ? (
<span
@@ -380,8 +389,9 @@ export const SettingsPane = ({
className="flex w-full items-center justify-between gap-4 p-4 text-left transition-colors hover:bg-slate-50/50"
>
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-100">
<div className="relative flex h-8 w-8 items-center justify-center rounded-lg bg-slate-100">
<Icon className="h-4 w-4 text-slate-700" />
{item.key === "system" && deployedUpdateUnseen ? <span className="absolute right-0.5 top-0.5 h-1.5 w-1.5 rounded-full bg-emerald-500 ring-2 ring-card" /> : null}
</div>
<span className="text-xs font-normal leading-5 text-slate-800">{item.label}</span>
{item.badge ? (
@@ -393,24 +403,7 @@ export const SettingsPane = ({
);
})}
</div>
<div className="divide-y divide-slate-100 overflow-hidden rounded-2xl border border-slate-200 bg-card">
<button
type="button"
onClick={() => setSystemInfoOpen(true)}
className="flex min-h-16 w-full items-center justify-between gap-3 px-4 py-2.5 text-left text-slate-600 transition-colors hover:bg-slate-200/50 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-900/20"
>
<span className="flex min-w-0 items-center gap-3">
<span className="relative flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-slate-100">
<Info className="h-4 w-4 text-slate-700" />
{deployedUpdateUnseen ? <span className="absolute right-0.5 top-0.5 h-1.5 w-1.5 rounded-full bg-emerald-500 ring-2 ring-card" /> : null}
</span>
<span className="min-w-0">
<span className="block truncate text-xs font-normal leading-5">{t("systemInfo.title")}</span>
<span className="mt-0.5 block truncate text-xs text-slate-500">{t("systemInfo.description")}</span>
</span>
</span>
<ChevronRight className="h-4 w-4 shrink-0 text-slate-400" />
</button>
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-card">
<FeedbackLink />
</div>
</div>
@@ -422,7 +415,6 @@ export const SettingsPane = ({
)}
</div>
</div>
<SystemInfoDialog open={systemInfoOpen} onOpenChange={setSystemInfoOpen} />
</div>
);
};
@@ -3,26 +3,37 @@ import { readFileSync } from "node:fs";
const headerActionsSource = readFileSync(new URL("./MemoEditorHeaderActions.tsx", import.meta.url), "utf8");
const settingsPaneSource = readFileSync(new URL("./SettingsPane.tsx", import.meta.url), "utf8");
const notebookPaneSource = readFileSync(new URL("./NotebookPane.tsx", import.meta.url), "utf8");
const mobileChromeSource = readFileSync(new URL("./WorkspaceMobileChrome.tsx", import.meta.url), "utf8");
const panelSource = readFileSync(new URL("./settings/SystemInfoPanel.tsx", import.meta.url), "utf8");
describe("system information access", () => {
test("keeps a compact-workspace entry in the editor overflow menu", () => {
expect(headerActionsSource).toMatch(
/<DropdownMenuItem[\s\S]*?handleSystemInfoOpenChange\(true\)[\s\S]*?systemInfo\.title[\s\S]*?<\/DropdownMenuItem>/,
);
expect(headerActionsSource).not.toContain("min-[1600px]:inline-flex");
test("keeps system information out of the note overflow menu", () => {
expect(headerActionsSource).not.toContain("systemInfo.title");
expect(headerActionsSource).not.toContain("SystemInfoDialog");
expect(headerActionsSource).not.toContain("deployedUpdateUnseen");
});
test("does not duplicate the entry in desktop settings", () => {
const desktopSettings = settingsPaneSource.match(/桌面端布局:双栏[\s\S]*?移动端布局/)?.[0];
test("adds system information to the settings navigation on desktop and mobile", () => {
expect(settingsPaneSource).toContain('key: "system"');
expect(settingsPaneSource).toContain('label: t("systemInfo.title")');
expect(settingsPaneSource).toContain("<SystemInfoPanel");
expect(settingsPaneSource).not.toContain("SystemInfoDialog");
expect(settingsPaneSource).toContain('item.key !== "shortcuts"');
expect(settingsPaneSource).not.toContain('item.key !== "system"');
expect(desktopSettings).not.toContain("setSystemInfoOpen(true)");
expect(desktopSettings).not.toContain('t("systemInfo.title")');
const desktopSettings = settingsPaneSource.match(/桌面端布局:双栏[\s\S]*?移动端布局/)?.[0] ?? "";
const mobileSettings = settingsPaneSource.match(/移动端布局[\s\S]*$/)?.[0] ?? "";
expect(desktopSettings).toContain("tabItems.map");
expect(mobileSettings).toContain("mobileTabItems.map");
expect(desktopSettings).toContain('item.key === "system" && deployedUpdateUnseen');
expect(mobileSettings).toContain('item.key === "system" && deployedUpdateUnseen');
});
test("keeps the entry in mobile settings", () => {
const mobileSettings = settingsPaneSource.match(/移动端布局[\s\S]*?<SystemInfoDialog/)?.[0];
expect(mobileSettings).toContain("setSystemInfoOpen(true)");
expect(mobileSettings).toContain('t("systemInfo.title")');
test("points the deployed-update dot at personal settings until the page is opened", () => {
expect(notebookPaneSource).toContain("notice={deployedUpdateUnseen}");
expect(notebookPaneSource).toContain("deployedUpdateUnseen ?");
expect(mobileChromeSource).toContain("notice={deployedUpdateUnseen}");
expect(panelSource).toContain("if (active) markSeen()");
});
});
@@ -1,15 +0,0 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
const source = readFileSync(new URL("./SystemInfoDialog.tsx", import.meta.url), "utf8");
describe("system information dialog dragging", () => {
test("moves from the title bar and keeps the body scrollable", () => {
expect(source).toContain("handleContentPointerDown");
expect(source).toContain("data-dialog-drag-handle");
expect(source).toContain("clampDialogPixelPosition");
expect(source).toContain("cursor-grab");
expect(source).toContain("grid-rows-[auto_minmax(0,1fr)_auto]");
expect(source).toContain("min-h-0 overflow-y-auto");
});
});
@@ -1,142 +0,0 @@
import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { useDeployedUpdateNotice } from "@/hooks/useDeployedUpdateNotice";
import { clampDialogPixelPosition } from "@/lib/dialog-drag";
import { cn } from "@/lib/utils";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { SystemInfoPanel } from "./settings/SystemInfoPanel";
type DialogDragSession = {
pointerId: number;
startX: number;
startY: number;
startPointerX: number;
startPointerY: number;
};
export const SystemInfoDialog = ({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) => {
const { t } = useTranslation();
const { markSeen } = useDeployedUpdateNotice();
const contentRef = useRef<HTMLDivElement>(null);
const dragRef = useRef<DialogDragSession | null>(null);
const positionRef = useRef<{ x: number; y: number } | null>(null);
const [position, setPosition] = useState<{ x: number; y: number } | null>(null);
const [dragging, setDragging] = useState(false);
useEffect(() => {
if (open) markSeen();
}, [markSeen, open]);
useEffect(() => {
if (open) return;
dragRef.current = null;
positionRef.current = null;
setPosition(null);
setDragging(false);
}, [open]);
const setContentNode = (node: HTMLDivElement | null) => {
contentRef.current = node;
if (!node || positionRef.current) return;
const rect = node.getBoundingClientRect();
const next = { x: rect.left, y: rect.top };
positionRef.current = next;
setPosition(next);
};
const updatePosition = (next: { x: number; y: number }) => {
positionRef.current = next;
setPosition(next);
};
const handleDragMove = (event: ReactPointerEvent<HTMLDivElement>) => {
const drag = dragRef.current;
const content = contentRef.current;
if (!drag || !content || event.pointerId !== drag.pointerId) return;
updatePosition(clampDialogPixelPosition({
x: drag.startX + (event.clientX - drag.startPointerX),
y: drag.startY + (event.clientY - drag.startPointerY),
width: content.offsetWidth,
height: content.offsetHeight,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
}));
};
const stopDragging = (event: ReactPointerEvent<HTMLDivElement>) => {
const drag = dragRef.current;
if (!drag || event.pointerId !== drag.pointerId) return;
dragRef.current = null;
setDragging(false);
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
};
const handleContentPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
if (event.button !== 0) return;
const target = event.target as Element | null;
if (!target?.closest("[data-dialog-drag-handle]")) return;
if (target.closest("button, a, input, textarea, [role='button']")) return;
const content = contentRef.current;
const current = positionRef.current;
if (!content || !current) return;
event.preventDefault();
event.currentTarget.setPointerCapture(event.pointerId);
dragRef.current = {
pointerId: event.pointerId,
startX: current.x,
startY: current.y,
startPointerX: event.clientX,
startPointerY: event.clientY,
};
setDragging(true);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
ref={setContentNode}
unstyledPosition
className={cn(
"grid max-h-[min(720px,calc(100dvh-2rem))] max-w-2xl grid-rows-[auto_minmax(0,1fr)_auto] gap-4 overflow-hidden p-0",
dragging && "cursor-grabbing",
)}
data-offset-x={position?.x ?? ""}
data-offset-y={position?.y ?? ""}
onLostPointerCapture={stopDragging}
onPointerCancel={stopDragging}
onPointerDown={handleContentPointerDown}
onPointerMove={handleDragMove}
onPointerUp={stopDragging}
style={position
? { left: position.x, top: position.y, transform: "none" }
: { left: "50%", top: "50%", transform: "translate(-50%, -50%)" }}
>
<DialogHeader
className={cn(
"touch-none select-none px-4 pr-12 pt-4 text-left sm:px-6 sm:pt-6",
dragging ? "cursor-grabbing" : "cursor-grab",
)}
data-dialog-drag-handle="true"
>
<DialogTitle>{t("systemInfo.title")}</DialogTitle>
</DialogHeader>
<div className="min-h-0 overflow-y-auto px-4 sm:px-6">
<SystemInfoPanel />
</div>
<div className="flex justify-end px-4 pb-4 sm:px-6 sm:pb-6">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
{t("common.close")}
</Button>
</div>
</DialogContent>
</Dialog>
);
};
@@ -11,6 +11,7 @@ import {
DrawerTitle,
} from "@/components/ui/drawer";
import { cn } from "@/lib/utils";
import { useDeployedUpdateNotice } from "@/hooks/useDeployedUpdateNotice";
import { BETA_BADGE_CLASSNAME } from "@/lib/workspace-ui";
import type { NoteCreateKind, Notebook } from "@edgeever/shared";
import type { MobileBottomNavItem, NotebookNode } from "@/lib/app-helpers";
@@ -26,11 +27,13 @@ const MobileBottomNavButton = ({
active = false,
icon,
label,
notice = false,
onClick,
}: {
active?: boolean;
icon: ReactNode;
label: string;
notice?: boolean;
onClick: () => void;
}) => (
<button
@@ -46,7 +49,10 @@ const MobileBottomNavButton = ({
onClick={onClick}
>
{active ? <span aria-hidden="true" className="absolute top-0 h-0.5 w-5 rounded-full bg-slate-950" /> : null}
{icon}
<span className="relative">
{icon}
{notice ? <span className="absolute -right-1 -top-1 h-1.5 w-1.5 rounded-full bg-emerald-500 ring-2 ring-card" /> : null}
</span>
<span>{label}</span>
</button>
);
@@ -67,6 +73,7 @@ export const MobileBottomNav = ({
onOpenSettings: () => void;
}) => {
const { t } = useTranslation();
const { unseen: deployedUpdateUnseen } = useDeployedUpdateNotice();
const createMemoLabel = !canCreateMemo ? t("nav.createDisabled") : isCreating ? t("nav.creating") : t("nav.createMemo");
return (
@@ -117,7 +124,7 @@ export const MobileBottomNav = ({
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<MobileBottomNavButton active={activeItem === "settings"} icon={<UserRound className="h-5 w-5" />} label={t("nav.mine")} onClick={onOpenSettings} />
<MobileBottomNavButton active={activeItem === "settings"} icon={<UserRound className="h-5 w-5" />} label={t("nav.mine")} notice={deployedUpdateUnseen} onClick={onOpenSettings} />
</div>
</nav>
);
@@ -269,7 +269,7 @@ export const SystemInfoPanel = ({ active = true }: { active?: boolean }) => {
const [desktopUpdateChecked, setDesktopUpdateChecked] = useState(false);
const [viewportRevision, setViewportRevision] = useState(0);
const queryClient = useQueryClient();
const { release } = useDeployedUpdateNotice();
const { markSeen, release } = useDeployedUpdateNotice();
const desktopBridge = window.edgeeverDesktop;
const desktopAvailable = desktopBridge?.isAvailable === true;
const instanceUrl = desktopAvailable ? getConfiguredDesktopApiBaseUrl() : window.location.origin;
@@ -305,6 +305,9 @@ export const SystemInfoPanel = ({ active = true }: { active?: boolean }) => {
refetchInterval: (query) => query.state.data?.state === "available" ? 1_000 : false,
retry: 1,
});
useEffect(() => {
if (active) markSeen();
}, [active, markSeen]);
useEffect(() => {
if (!active) return;
const onResize = () => setViewportRevision((value) => value + 1);
+2 -4
View File
@@ -27,16 +27,14 @@ const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
unstyledPosition?: boolean;
}
>(({ className, children, showCloseButton = true, unstyledPosition = false, ...props }, ref) => (
>(({ className, children, showCloseButton = true, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full max-w-lg gap-4 rounded-lg border border-slate-200 bg-card p-6 shadow-lg",
!unstyledPosition && "left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-1/2 data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-1/2",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-slate-200 bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-1/2 data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-1/2",
className
)}
{...props}
-20
View File
@@ -1,20 +0,0 @@
import { describe, expect, test } from "bun:test";
import { clampDialogPixelPosition } from "./dialog-drag.ts";
describe("clampDialogPixelPosition", () => {
const base = {
width: 400,
height: 300,
viewportWidth: 1000,
viewportHeight: 800,
};
test("keeps unconstrained movement", () => {
expect(clampDialogPixelPosition({ ...base, x: 240, y: 80 })).toEqual({ x: 240, y: 80 });
});
test("keeps a slice of the dialog inside the viewport", () => {
expect(clampDialogPixelPosition({ ...base, x: 2000, y: 2000 })).toEqual({ x: 952, y: 752 });
expect(clampDialogPixelPosition({ ...base, x: -2000, y: -2000 })).toEqual({ x: -352, y: 0 });
});
});
-28
View File
@@ -1,28 +0,0 @@
export const DIALOG_DRAG_MIN_VISIBLE = 48;
export const clampDialogPixelPosition = ({
x,
y,
width,
height,
viewportWidth,
viewportHeight,
minVisible = DIALOG_DRAG_MIN_VISIBLE,
}: {
x: number;
y: number;
width: number;
height: number;
viewportWidth: number;
viewportHeight: number;
minVisible?: number;
}) => {
const minX = minVisible - width;
const maxX = viewportWidth - minVisible;
const minY = 0;
const maxY = viewportHeight - minVisible;
return {
x: Math.min(Math.max(x, minX), Math.max(minX, maxX)),
y: Math.min(Math.max(y, minY), Math.max(minY, maxY)),
};
};