[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
import {
Copy,
FileText,
Share2,
} from "lucide-react";
import React, { useCallback, useState, useRef, useEffect } from "react";
import { useI18n } from "../../application/i18n/I18nProvider";
import {
type VaultNote,
} from "../../domain/notes";
import { copyToClipboard } from "../keychain/utils";
import { toast } from "../ui/toast";
export interface NoteExportMenuProps {
note: VaultNote | null;
allNotes: VaultNote[];
onExportNote: (note: VaultNote) => void;
onExportAll: () => void;
className?: string;
}
export const NoteExportMenu: React.FC<NoteExportMenuProps> = ({
note,
allNotes,
onExportNote,
onExportAll,
className = "",
}) => {
const { t } = useI18n();
const [open, setOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const closeAndRestoreFocus = useCallback(() => {
setOpen(false);
requestAnimationFrame(() => triggerRef.current?.focus());
}, []);
useEffect(() => {
if (!open) return;
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setOpen(false);
}
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
closeAndRestoreFocus();
return;
}
if (e.key === "Tab") {
setOpen(false);
return;
}
const items = Array.from(
menuRef.current?.querySelectorAll<HTMLButtonElement>('[role="menuitem"]:not(:disabled)') ?? [],
);
if (!items.length) return;
const current = items.indexOf(document.activeElement as HTMLButtonElement);
let next = current;
if (e.key === "ArrowDown") next = (current + 1 + items.length) % items.length;
else if (e.key === "ArrowUp") next = (current - 1 + items.length) % items.length;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = items.length - 1;
else return;
e.preventDefault();
items[next]?.focus();
};
window.addEventListener("mousedown", handleClickOutside);
window.addEventListener("keydown", handleKeyDown);
requestAnimationFrame(() => {
menuRef.current?.querySelector<HTMLButtonElement>('[role="menuitem"]')?.focus();
});
return () => {
window.removeEventListener("mousedown", handleClickOutside);
window.removeEventListener("keydown", handleKeyDown);
};
}, [closeAndRestoreFocus, open]);
const handleExportSingleMarkdown = () => {
if (!note) return;
onExportNote(note);
closeAndRestoreFocus();
};
const handleCopyMarkdown = async () => {
if (!note) return;
const ok = await copyToClipboard(note.content);
if (ok) {
toast.success(t("common.copied") || "已复制到剪贴板");
}
closeAndRestoreFocus();
};
const handleExportAllZip = () => {
if (!allNotes.length) return;
onExportAll();
closeAndRestoreFocus();
};
return (
<div className={`relative inline-block ${className}`} ref={menuRef}>
<button
ref={triggerRef}
type="button"
className="p-1.5 rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
title={t("notes.export.share")}
onClick={() => setOpen((prev) => !prev)}
aria-haspopup="menu"
aria-expanded={open}
>
<Share2 size={16} />
</button>
{open && (
<div role="menu" className="absolute right-0 top-full mt-1.5 w-56 bg-popover border border-border rounded-lg shadow-lg py-1.5 z-50 text-sm animate-in fade-in-50 zoom-in-95">
{note && (
<>
<div className="px-3 py-1 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
{t("notes.export.currentNote")}
</div>
<button
type="button"
role="menuitem"
className="w-full px-3 py-1.5 flex items-center gap-2.5 hover:bg-muted text-foreground transition-colors text-left"
onClick={handleExportSingleMarkdown}
>
<FileText size={14} className="text-primary" />
<span>{t("notes.export.exportMarkdown")}</span>
</button>
<button
type="button"
role="menuitem"
className="w-full px-3 py-1.5 flex items-center gap-2.5 hover:bg-muted text-foreground transition-colors text-left"
onClick={handleCopyMarkdown}
>
<Copy size={14} className="text-muted-foreground" />
<span>{t("notes.export.copyMarkdown")}</span>
</button>
<div className="my-1 border-t border-border" />
</>
)}
<div className="px-3 py-1 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
{t("notes.export.allNotes")}
</div>
<button
type="button"
role="menuitem"
className="w-full px-3 py-1.5 flex items-center gap-2.5 hover:bg-muted text-foreground transition-colors text-left"
onClick={handleExportAllZip}
disabled={!allNotes.length}
>
<FileText size={14} className="text-emerald-500" />
<span>{t("notes.export.exportAllZip")}</span>
</button>
</div>
)}
</div>
);
};