Skip to content

Commit bbbfe8e

Browse files
oratisclaude
andcommitted
Settings export/import + tab middle-click close
- Export Settings: palette command — serialises every persisted setting (12 fields) as pretty JSON and writes to clipboard. Toast on success / failure. - Import Settings…: palette command — prompts for JSON, runs through setSettings (which clamps + falls-back). Bad input toasts an error; a blank prompt is cancel. - Tab middle-click closes the tab (browser convention). Pinned tabs ignore the gesture; the user must explicitly unpin first. - 2 new tests for middle-click (closes unpinned tab, no-op on pinned). - 28 test files / 206 tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 86ce02f commit bbbfe8e

5 files changed

Lines changed: 87 additions & 0 deletions

File tree

src/App.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,54 @@ export function App() {
13131313
label: "Rename Active File…",
13141314
run: renameActiveFile,
13151315
},
1316+
{
1317+
id: "export_settings",
1318+
label: "Export Settings (Copy JSON)",
1319+
run: () => {
1320+
const s = useAppStore.getState();
1321+
const payload = JSON.stringify(
1322+
{
1323+
fontSize: s.fontSize,
1324+
proseMaxWidth: s.proseMaxWidth,
1325+
autosaveMs: s.autosaveMs,
1326+
imagePasteDir: s.imagePasteDir,
1327+
exportTheme: s.exportTheme,
1328+
spellcheck: s.spellcheck,
1329+
lineWrap: s.lineWrap,
1330+
sidebarWidth: s.sidebarWidth,
1331+
outlineWidth: s.outlineWidth,
1332+
saveOnBlur: s.saveOnBlur,
1333+
trimOnSave: s.trimOnSave,
1334+
showLineNumbers: s.showLineNumbers,
1335+
},
1336+
null,
1337+
2,
1338+
);
1339+
navigator.clipboard
1340+
.writeText(payload)
1341+
.then(() => showToast(tr("toast.settingsCopied")))
1342+
.catch(() => showToast(tr("toast.copyFailed")));
1343+
},
1344+
},
1345+
{
1346+
id: "import_settings",
1347+
label: "Import Settings…",
1348+
run: () => {
1349+
const raw = window.prompt(tr("prompt.importSettings"), "");
1350+
if (!raw) return;
1351+
try {
1352+
const parsed = JSON.parse(raw);
1353+
if (parsed && typeof parsed === "object") {
1354+
useAppStore.getState().setSettings(parsed);
1355+
showToast(tr("toast.settingsImported"));
1356+
} else {
1357+
showToast(tr("toast.settingsImportBad"));
1358+
}
1359+
} catch {
1360+
showToast(tr("toast.settingsImportBad"));
1361+
}
1362+
},
1363+
},
13161364
{
13171365
id: "insert_hr",
13181366
label: "Insert Horizontal Rule",

src/components/TabBar.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,29 @@ describe("TabBar", () => {
4949
expect(useAppStore.getState().activeTabId).toBe("/b.md");
5050
});
5151

52+
it("middle-click on a tab closes it", () => {
53+
useAppStore.setState({
54+
tabs: [makeTab("/a.md", "a.md"), makeTab("/b.md", "b.md")],
55+
activeTabId: "/a.md",
56+
});
57+
render(<TabBar />);
58+
const aRow = screen.getByText("a.md").parentElement!;
59+
fireEvent.mouseDown(aRow, { button: 1 });
60+
const ids = useAppStore.getState().tabs.map((t) => t.id);
61+
expect(ids).toEqual(["/b.md"]);
62+
});
63+
64+
it("middle-click on a pinned tab is a no-op", () => {
65+
useAppStore.setState({
66+
tabs: [{ ...makeTab("/a.md", "a.md"), pinned: true }, makeTab("/b.md", "b.md")],
67+
activeTabId: "/a.md",
68+
});
69+
render(<TabBar />);
70+
const aRow = screen.getByText("a.md").parentElement!;
71+
fireEvent.mouseDown(aRow, { button: 1 });
72+
expect(useAppStore.getState().tabs).toHaveLength(2);
73+
});
74+
5275
it("hides the close button on pinned tabs and shows a pin glyph", () => {
5376
useAppStore.setState({
5477
tabs: [{ ...makeTab("/a.md", "a.md"), pinned: true }, makeTab("/b.md", "b.md")],

src/components/TabBar.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ export function TabBar() {
6868
e.preventDefault();
6969
setCtx({ id: tab.id, x: e.clientX, y: e.clientY });
7070
}}
71+
onMouseDown={(e) => {
72+
// Middle-click closes the tab (browser convention).
73+
// Pinned tabs ignore the gesture; explicit unpin first.
74+
if (e.button === 1 && !tab.pinned) {
75+
e.preventDefault();
76+
closeTab(tab.id);
77+
}
78+
}}
7179
className={`group titlebar-no-drag relative flex items-center gap-2 pl-3 pr-1 py-1.5 text-[12px] cursor-pointer border-r border-black/5 dark:border-white/10 select-none ${
7280
isActive
7381
? "bg-canvas-light dark:bg-canvas-dark text-ink-light dark:text-ink-dark"

src/lib/locales/en.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,14 @@ export const en = {
8484
"toast.renameNoFile": "No file to rename",
8585
"toast.renameBadName": "Name can't contain '/'",
8686
"toast.renameFailed": "Rename failed",
87+
"toast.settingsCopied": "Settings copied to clipboard",
88+
"toast.settingsImported": "Settings imported",
89+
"toast.settingsImportBad": "Invalid settings JSON",
8790
"prompt.tableSize": "Table size as rows x cols (e.g. 3x4):",
8891
"prompt.linkUrl": "Link URL:",
8992
"prompt.linkText": "Link text:",
9093
"prompt.rename": "New file name:",
94+
"prompt.importSettings": "Paste settings JSON:",
9195
"reload.confirmDirty":
9296
'"{0}" has unsaved changes that will be lost on reload. Continue?',
9397
"toast.wikilinkMiss": 'No vault file matches "{0}"',

src/lib/locales/zh.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,14 @@ export const zh: Strings = {
8585
"toast.renameNoFile": "没有可重命名的文件",
8686
"toast.renameBadName": "文件名不能包含 '/'",
8787
"toast.renameFailed": "重命名失败",
88+
"toast.settingsCopied": "设置已复制到剪贴板",
89+
"toast.settingsImported": "设置已导入",
90+
"toast.settingsImportBad": "无效的设置 JSON",
8891
"prompt.tableSize": "表格尺寸:行 x 列(例如 3x4):",
8992
"prompt.linkUrl": "链接 URL:",
9093
"prompt.linkText": "链接文本:",
9194
"prompt.rename": "新文件名:",
95+
"prompt.importSettings": "粘贴设置 JSON:",
9296
"reload.confirmDirty": "「{0}」有未保存的修改,重新加载会丢失。继续吗?",
9397
"toast.wikilinkMiss": "Vault 里没有匹配的文件:{0}",
9498
"toast.openFailed": "打开 {0} 失败",

0 commit comments

Comments
 (0)