From cf652dc444bee2c0b083a3184300863cf57a74de Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Fri, 13 Feb 2026 18:39:04 +0500 Subject: [PATCH 01/14] add option in settings to ask for file names on fist save --- src/global-state.ts | 3 +++ src/routes/_appRoot.settings.tsx | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/global-state.ts b/src/global-state.ts index 9028e300..c13c641b 100644 --- a/src/global-state.ts +++ b/src/global-state.ts @@ -880,3 +880,6 @@ export const openaiKeyAtom = atomWithStorage(OPENAI_KEY_STORAGE_KEY, "") export const hasOpenAIKeyAtom = selectAtom(openaiKeyAtom, (key) => key !== "") export const voiceAssistantEnabledAtom = atomWithStorage("voice_assistant_enabled", false) + + +export const promptToNameFilesAtom = atomWithStorage('prompt-to-name-files', true); \ No newline at end of file diff --git a/src/routes/_appRoot.settings.tsx b/src/routes/_appRoot.settings.tsx index 3d554ecd..79e4c580 100644 --- a/src/routes/_appRoot.settings.tsx +++ b/src/routes/_appRoot.settings.tsx @@ -19,6 +19,7 @@ import { isCloningRepoAtom, isRepoClonedAtom, isRepoNotClonedAtom, + promptToNameFilesAtom, vimModeAtom, voiceAssistantEnabledAtom, } from "../global-state" @@ -37,6 +38,7 @@ function RouteComponent() {
+ @@ -155,6 +157,30 @@ function GitHubSection() { ) } +function FileManagementSection() { + const [promptToName, setPromptToName] = useAtom(promptToNameFilesAtom) + + return ( + +
+
+ + +
+

+ If disabled, notes will be named automatically based on current time. +

+
+
+ ) +} + function AppearanceSection() { const [epaper, setEpaper] = useAtom(epaperAtom) From 511127ac2c8ff10d7dda8f39095176a08341abca Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Sun, 15 Feb 2026 12:25:28 +0500 Subject: [PATCH 02/14] feat(settings): change he text of file management option --- src/routes/_appRoot.settings.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/routes/_appRoot.settings.tsx b/src/routes/_appRoot.settings.tsx index 79e4c580..f6e6ae76 100644 --- a/src/routes/_appRoot.settings.tsx +++ b/src/routes/_appRoot.settings.tsx @@ -164,17 +164,13 @@ function FileManagementSection() {
- +

- If disabled, notes will be named automatically based on current time. + If disabled, files are automatically saved with current time.

From beaada508224354424690db7008f8dad943f23cb Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Sun, 15 Feb 2026 12:26:23 +0500 Subject: [PATCH 03/14] add global state for local storage prompt-to-name-files --- src/global-state.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/global-state.ts b/src/global-state.ts index c13c641b..63649819 100644 --- a/src/global-state.ts +++ b/src/global-state.ts @@ -869,6 +869,8 @@ export const isHelpPanelOpenAtom = atomWithStorage("help-panel", false) export const calendarLayoutAtom = atomWithStorage<"week" | "month">("calendar-layout", "week") +export const promptToNameFilesAtom = atomWithStorage("prompt-to-name-files", true) + // ----------------------------------------------------------------------------- // AI // ----------------------------------------------------------------------------- @@ -880,6 +882,3 @@ export const openaiKeyAtom = atomWithStorage(OPENAI_KEY_STORAGE_KEY, "") export const hasOpenAIKeyAtom = selectAtom(openaiKeyAtom, (key) => key !== "") export const voiceAssistantEnabledAtom = atomWithStorage("voice_assistant_enabled", false) - - -export const promptToNameFilesAtom = atomWithStorage('prompt-to-name-files', true); \ No newline at end of file From c9ea9ab6fa42397286ea8c1f06e0a5bf52c4ad6a Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Sun, 15 Feb 2026 12:27:13 +0500 Subject: [PATCH 04/14] feat: add editable fiename component to rename files easily --- src/components/editable-filename.tsx | 158 +++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/components/editable-filename.tsx diff --git a/src/components/editable-filename.tsx b/src/components/editable-filename.tsx new file mode 100644 index 00000000..64fc84c1 --- /dev/null +++ b/src/components/editable-filename.tsx @@ -0,0 +1,158 @@ +import { useState, useRef, useEffect, useCallback, forwardRef, useImperativeHandle } from "react" +import { useHotkeys } from "react-hotkeys-hook" +import { TextInput } from "./text-input" +import { Tooltip } from "./tooltip" +import { Keys } from "./keys" +import { cx } from "../utils/cx" + +type EditableFilenameProps = { + noteId: string + isSignedOut: boolean + onRename: (newName: string) => boolean | Promise +} + +export interface EditableFilenameHandle { + startRename: () => void +} + +export const EditableFilename = forwardRef( + ({ noteId, isSignedOut, onRename }, ref) => { + const [isRenaming, setIsRenaming] = useState(false) + const [renameValue, setRenameValue] = useState(noteId) + const inputRef = useRef(null) + + // Sync internal state with noteId prop + useEffect(() => { + setRenameValue(noteId) + setIsRenaming(false) + }, [noteId]) + + const startRename = useCallback(() => { + if (isSignedOut || isRenaming) return + setIsRenaming(true) + // Focus and select the text for immediate editing + requestAnimationFrame(() => inputRef.current?.select()) + }, [isSignedOut, isRenaming]) + + // Expose startRename to parent via ref + useImperativeHandle( + ref, + () => ({ + startRename, + }), + [startRename], + ) + + // Global F2 Shortcut logic + useHotkeys( + "f2", + (e) => { + e.preventDefault() + startRename() + }, + { enabled: !isSignedOut && !isRenaming }, + ) + + const handleFinish = async () => { + const trimmed = renameValue.trim() + // If empty or unchanged, just close + if (!trimmed || trimmed === noteId) { + setIsRenaming(false) + setRenameValue(noteId) + return + } + + const success = await onRename(trimmed) + if (success) { + setIsRenaming(false) + } else { + // Keep input open and focus if rename failed (e.g. duplicate name) + inputRef.current?.focus() + } + } + + const handleCancel = () => { + setRenameValue(noteId) + setIsRenaming(false) + } + + if (isRenaming) { + return ( +
+ setRenameValue(e.target.value)} + onBlur={handleFinish} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault() + handleFinish() + } + if (e.key === "Escape") { + e.preventDefault() + handleCancel() + } + }} + className="h-7 min-w-0 flex-grow text-sm font-bold sm:max-w-[300px]" + /> + .md +
+ ) + } + + return ( + + { + e.preventDefault() + startRename() + }} + onTouchStart={(e) => { + // Standard mobile double-tap detection + if (e.detail === 2) { + e.preventDefault() + startRename() + } + }} + onKeyDown={(e) => { + // Allow keyboard activation via Enter or Space + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + startRename() + } + }} + > + {noteId} + .md + + } + /> + {!isSignedOut && ( + +
+ Rename file +
+ +
+
+
+ )} +
+ ) + }, +) + +EditableFilename.displayName = "EditableFilename" From 2c181ceac4a39714318b4b72368a687621dfb3b9 Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Sun, 15 Feb 2026 12:33:23 +0500 Subject: [PATCH 05/14] implement new editable filename component and sync it witth old renaming functionality --- Co-authored-by: Cole Bemis Co-authored-by: Muhammad-Aqib-Bashir --- src/routes/_appRoot.notes_.$.tsx | 180 +++++++++++++++++++++---------- 1 file changed, 125 insertions(+), 55 deletions(-) diff --git a/src/routes/_appRoot.notes_.$.tsx b/src/routes/_appRoot.notes_.$.tsx index 2ef7dbcd..ec1f5713 100644 --- a/src/routes/_appRoot.notes_.$.tsx +++ b/src/routes/_appRoot.notes_.$.tsx @@ -46,6 +46,7 @@ import { PageLayout } from "../components/page-layout" import { PillButton } from "../components/pill-button" import { SegmentedControl } from "../components/segmented-control" import { ShareDialog } from "../components/share-dialog" +import { TextInput } from "../components/text-input" import { Tooltip } from "../components/tooltip" import { Tool, voiceConversationMachineAtom } from "../components/voice-conversation" import { @@ -54,6 +55,8 @@ import { githubRepoAtom, globalStateMachineAtom, isSignedOutAtom, + markdownFilesAtom, + promptToNameFilesAtom, vimModeAtom, weeklyTemplateAtom, } from "../global-state" @@ -66,10 +69,11 @@ import { cx } from "../utils/cx" import { formatDate, formatWeek, isValidDateString, isValidWeekString } from "../utils/date" import { updateFrontmatterValue } from "../utils/frontmatter" import { clearNoteDraft, getNoteDraft, setNoteDraft } from "../utils/note-draft" -import { getInvalidNoteIdCharacters } from "../utils/note-id" +import { getInvalidNoteIdCharacters, isValidNoteId } from "../utils/note-id" import { parseNote } from "../utils/parse-note" import { pluralize } from "../utils/pluralize" import { notificationSound, playSound } from "../utils/sounds" +import { EditableFilename, EditableFilenameHandle } from "../components/editable-filename" type RouteSearch = { mode: "read" | "write" @@ -133,6 +137,8 @@ function NotePage() { const dailyTemplate = useAtomValue(dailyTemplateAtom) const weeklyTemplate = useAtomValue(weeklyTemplateAtom) const defaultFont = useAtomValue(defaultFontAtom) + const markdownFiles = useAtomValue(markdownFilesAtom) + const promptToNameFiles = useAtomValue(promptToNameFilesAtom) const { online } = useNetworkState() // Note data @@ -177,6 +183,8 @@ function NotePage() { const parsedWidthResult = widthSchema.safeParse(frontmatterWidth) const resolvedWidth = parsedWidthResult.success ? parsedWidthResult.data : "fixed" + const editableFilenameRef = React.useRef(null) + // Set the font React.useEffect(() => { document.documentElement.style.setProperty( @@ -198,6 +206,62 @@ function NotePage() { const renameNote = useRenameNote() const attachFile = useAttachFile() + const normalizeNoteId = React.useCallback((rawName: string) => { + return rawName.trim().replace(/\.md$/i, "").trim() + }, []) + + const attemptRename = React.useCallback( + (rawName: string): boolean => { + if (!noteId) return false + + const newNoteId = normalizeNoteId(rawName) + + if (!newNoteId || newNoteId === noteId) { + return true + } + + const result = renameNote({ + oldName: noteId, + newName: newNoteId, + content: editorValue, + }) + + if (!result.success) { + switch (result.reason) { + case "no-op": + return true + case "invalid": + { + const invalidCharacters = Array.from(new Set(getInvalidNoteIdCharacters(newNoteId))) + const invalidList = invalidCharacters.map((char) => `"${char}"`).join(", ") + const suffix = invalidList ? `: ${invalidList}` : "" + window.alert(`"${newNoteId}.md" contains invalid characters${suffix}`) + } + return false + case "duplicate": + window.alert(`"${newNoteId}.md" already exists.`) + return false + default: + result.reason satisfies never + } + return false + } + + clearNoteDraft({ githubRepo, noteId }) + clearNoteDraft({ githubRepo, noteId: newNoteId }) + + navigate({ + to: "/notes/$", + params: { _splat: newNoteId }, + search: (prev) => ({ ...prev, content: undefined }), + replace: true, + }) + + return true + }, + [noteId, normalizeNoteId, renameNote, editorValue, githubRepo, navigate], + ) + const handleSave = React.useCallback( (value: string) => { if (isSignedOut || !noteId) return @@ -205,6 +269,46 @@ function NotePage() { // New notes shouldn't be saved if the editor is empty if (!note && !value) return + if (!note && value && promptToNameFiles && !isDailyNote && !isWeeklyNote) { + const rawName = window.prompt("Name this file", noteId) + + if (rawName === null) return + + const newNoteId = normalizeNoteId(rawName) + + if (!newNoteId) { + return + } + + if (newNoteId !== noteId) { + if (!isValidNoteId(newNoteId)) { + const invalidCharacters = Array.from(new Set(getInvalidNoteIdCharacters(newNoteId))) + const invalidList = invalidCharacters.map((char) => `"${char}"`).join(", ") + const suffix = invalidList ? `: ${invalidList}` : "" + window.alert(`"${newNoteId}.md" contains invalid characters${suffix}`) + return + } + + if (markdownFiles[`${newNoteId}.md`]) { + window.alert(`"${newNoteId}.md" already exists.`) + return + } + + saveNote({ id: newNoteId, content: value }) + clearNoteDraft({ githubRepo, noteId }) + clearNoteDraft({ githubRepo, noteId: newNoteId }) + + navigate({ + to: "/notes/$", + params: { _splat: newNoteId }, + search: (prev) => ({ ...prev, content: undefined }), + replace: true, + }) + + return + } + } + // Only save if the content has changed if (value !== note?.content) { saveNote({ id: noteId, content: value }) @@ -212,7 +316,19 @@ function NotePage() { clearNoteDraft({ githubRepo, noteId }) }, - [isSignedOut, noteId, note, saveNote, githubRepo], + [ + isSignedOut, + noteId, + note, + promptToNameFiles, + isDailyNote, + isWeeklyNote, + normalizeNoteId, + markdownFiles, + saveNote, + githubRepo, + navigate, + ], ) const updateWidth = React.useCallback( @@ -231,57 +347,6 @@ function NotePage() { [noteId, editorValue, setEditorValue, handleSave], ) - const handleRename = React.useCallback(() => { - if (!noteId) return - - const oldNoteId = noteId - const newNoteIdRaw = window.prompt("Rename file", oldNoteId) - if (!newNoteIdRaw) return - - const newNoteIdTrimmed = newNoteIdRaw.trim() - if (!newNoteIdTrimmed) return - - const newNoteId = newNoteIdTrimmed.replace(/\.md$/i, "").trim() - if (!newNoteId || newNoteId === oldNoteId) return - - const result = renameNote({ - oldName: oldNoteId, - newName: newNoteId, - content: editorValue, - }) - - if (!result.success) { - switch (result.reason) { - case "no-op": - return - case "invalid": - { - const invalidCharacters = Array.from(new Set(getInvalidNoteIdCharacters(newNoteId))) - const invalidList = invalidCharacters.map((char) => `"${char}"`).join(", ") - const suffix = invalidList ? `: ${invalidList}` : "" - window.alert(`"${newNoteId}.md" contains invalid characters${suffix}`) - } - return - case "duplicate": - window.alert(`"${newNoteId}.md" already exists.`) - return - default: - result.reason satisfies never - } - return - } - - clearNoteDraft({ githubRepo, noteId: oldNoteId }) - clearNoteDraft({ githubRepo, noteId: newNoteId }) - - navigate({ - to: "/notes/$", - params: { _splat: newNoteId }, - search: (prev) => ({ ...prev, content: undefined }), - replace: true, - }) - }, [noteId, renameNote, editorValue, githubRepo, navigate]) - const switchToWriting = React.useCallback(() => { navigate({ search: (prev) => ({ ...prev, mode: "write" }), replace: true }) setTimeout(() => { @@ -509,7 +574,12 @@ function NotePage() { - {noteId}.md + {isDraft ? ( } disabled={isSignedOut} - onClick={handleRename} + onClick={() => editableFilenameRef.current?.startRename()} > Rename file From 81304fa92d7c255330872101f44610c3c1f5c290 Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Sun, 15 Feb 2026 12:44:59 +0500 Subject: [PATCH 06/14] refactor: remove unused TextInput import --- src/routes/_appRoot.notes_.$.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/_appRoot.notes_.$.tsx b/src/routes/_appRoot.notes_.$.tsx index ec1f5713..120bdf8e 100644 --- a/src/routes/_appRoot.notes_.$.tsx +++ b/src/routes/_appRoot.notes_.$.tsx @@ -46,7 +46,6 @@ import { PageLayout } from "../components/page-layout" import { PillButton } from "../components/pill-button" import { SegmentedControl } from "../components/segmented-control" import { ShareDialog } from "../components/share-dialog" -import { TextInput } from "../components/text-input" import { Tooltip } from "../components/tooltip" import { Tool, voiceConversationMachineAtom } from "../components/voice-conversation" import { @@ -183,6 +182,7 @@ function NotePage() { const parsedWidthResult = widthSchema.safeParse(frontmatterWidth) const resolvedWidth = parsedWidthResult.success ? parsedWidthResult.data : "fixed" + // Handle for programmatic control of filename editing const editableFilenameRef = React.useRef(null) // Set the font From db7327565288b0bcdcfe3bf306687568b16802a1 Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Fri, 13 Feb 2026 18:39:04 +0500 Subject: [PATCH 07/14] add option in settings to ask for file names on fist save --- src/global-state.ts | 3 +++ src/routes/_appRoot.settings.tsx | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/global-state.ts b/src/global-state.ts index 9028e300..c13c641b 100644 --- a/src/global-state.ts +++ b/src/global-state.ts @@ -880,3 +880,6 @@ export const openaiKeyAtom = atomWithStorage(OPENAI_KEY_STORAGE_KEY, "") export const hasOpenAIKeyAtom = selectAtom(openaiKeyAtom, (key) => key !== "") export const voiceAssistantEnabledAtom = atomWithStorage("voice_assistant_enabled", false) + + +export const promptToNameFilesAtom = atomWithStorage('prompt-to-name-files', true); \ No newline at end of file diff --git a/src/routes/_appRoot.settings.tsx b/src/routes/_appRoot.settings.tsx index 5e800871..93f7a496 100644 --- a/src/routes/_appRoot.settings.tsx +++ b/src/routes/_appRoot.settings.tsx @@ -19,6 +19,7 @@ import { isCloningRepoAtom, isRepoClonedAtom, isRepoNotClonedAtom, + promptToNameFilesAtom, vimModeAtom, voiceAssistantEnabledAtom, } from "../global-state" @@ -37,6 +38,7 @@ function RouteComponent() {
+ @@ -155,6 +157,30 @@ function GitHubSection() { ) } +function FileManagementSection() { + const [promptToName, setPromptToName] = useAtom(promptToNameFilesAtom) + + return ( + +
+
+ + +
+

+ If disabled, notes will be named automatically based on current time. +

+
+
+ ) +} + function AppearanceSection() { const [epaper, setEpaper] = useAtom(epaperAtom) From 759dd412af2ed690d0a9442a302b4cbfaebf318e Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Sun, 15 Feb 2026 12:25:28 +0500 Subject: [PATCH 08/14] feat(settings): change he text of file management option --- src/routes/_appRoot.settings.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/routes/_appRoot.settings.tsx b/src/routes/_appRoot.settings.tsx index 93f7a496..11aea92e 100644 --- a/src/routes/_appRoot.settings.tsx +++ b/src/routes/_appRoot.settings.tsx @@ -164,17 +164,13 @@ function FileManagementSection() {
- +

- If disabled, notes will be named automatically based on current time. + If disabled, files are automatically saved with current time.

From 97951f5d5f5e40ae5c3bf8806041ef7658a06837 Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Sun, 15 Feb 2026 12:26:23 +0500 Subject: [PATCH 09/14] add global state for local storage prompt-to-name-files --- src/global-state.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/global-state.ts b/src/global-state.ts index c13c641b..63649819 100644 --- a/src/global-state.ts +++ b/src/global-state.ts @@ -869,6 +869,8 @@ export const isHelpPanelOpenAtom = atomWithStorage("help-panel", false) export const calendarLayoutAtom = atomWithStorage<"week" | "month">("calendar-layout", "week") +export const promptToNameFilesAtom = atomWithStorage("prompt-to-name-files", true) + // ----------------------------------------------------------------------------- // AI // ----------------------------------------------------------------------------- @@ -880,6 +882,3 @@ export const openaiKeyAtom = atomWithStorage(OPENAI_KEY_STORAGE_KEY, "") export const hasOpenAIKeyAtom = selectAtom(openaiKeyAtom, (key) => key !== "") export const voiceAssistantEnabledAtom = atomWithStorage("voice_assistant_enabled", false) - - -export const promptToNameFilesAtom = atomWithStorage('prompt-to-name-files', true); \ No newline at end of file From b5031beea2fa7614602b44d787d982c1462195fe Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Sun, 15 Feb 2026 12:27:13 +0500 Subject: [PATCH 10/14] feat: add editable fiename component to rename files easily --- src/components/editable-filename.tsx | 158 +++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/components/editable-filename.tsx diff --git a/src/components/editable-filename.tsx b/src/components/editable-filename.tsx new file mode 100644 index 00000000..64fc84c1 --- /dev/null +++ b/src/components/editable-filename.tsx @@ -0,0 +1,158 @@ +import { useState, useRef, useEffect, useCallback, forwardRef, useImperativeHandle } from "react" +import { useHotkeys } from "react-hotkeys-hook" +import { TextInput } from "./text-input" +import { Tooltip } from "./tooltip" +import { Keys } from "./keys" +import { cx } from "../utils/cx" + +type EditableFilenameProps = { + noteId: string + isSignedOut: boolean + onRename: (newName: string) => boolean | Promise +} + +export interface EditableFilenameHandle { + startRename: () => void +} + +export const EditableFilename = forwardRef( + ({ noteId, isSignedOut, onRename }, ref) => { + const [isRenaming, setIsRenaming] = useState(false) + const [renameValue, setRenameValue] = useState(noteId) + const inputRef = useRef(null) + + // Sync internal state with noteId prop + useEffect(() => { + setRenameValue(noteId) + setIsRenaming(false) + }, [noteId]) + + const startRename = useCallback(() => { + if (isSignedOut || isRenaming) return + setIsRenaming(true) + // Focus and select the text for immediate editing + requestAnimationFrame(() => inputRef.current?.select()) + }, [isSignedOut, isRenaming]) + + // Expose startRename to parent via ref + useImperativeHandle( + ref, + () => ({ + startRename, + }), + [startRename], + ) + + // Global F2 Shortcut logic + useHotkeys( + "f2", + (e) => { + e.preventDefault() + startRename() + }, + { enabled: !isSignedOut && !isRenaming }, + ) + + const handleFinish = async () => { + const trimmed = renameValue.trim() + // If empty or unchanged, just close + if (!trimmed || trimmed === noteId) { + setIsRenaming(false) + setRenameValue(noteId) + return + } + + const success = await onRename(trimmed) + if (success) { + setIsRenaming(false) + } else { + // Keep input open and focus if rename failed (e.g. duplicate name) + inputRef.current?.focus() + } + } + + const handleCancel = () => { + setRenameValue(noteId) + setIsRenaming(false) + } + + if (isRenaming) { + return ( +
+ setRenameValue(e.target.value)} + onBlur={handleFinish} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault() + handleFinish() + } + if (e.key === "Escape") { + e.preventDefault() + handleCancel() + } + }} + className="h-7 min-w-0 flex-grow text-sm font-bold sm:max-w-[300px]" + /> + .md +
+ ) + } + + return ( + + { + e.preventDefault() + startRename() + }} + onTouchStart={(e) => { + // Standard mobile double-tap detection + if (e.detail === 2) { + e.preventDefault() + startRename() + } + }} + onKeyDown={(e) => { + // Allow keyboard activation via Enter or Space + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + startRename() + } + }} + > + {noteId} + .md + + } + /> + {!isSignedOut && ( + +
+ Rename file +
+ +
+
+
+ )} +
+ ) + }, +) + +EditableFilename.displayName = "EditableFilename" From dfa70bcdd88174c01ea45c945cb1b2561ae44c7e Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Sun, 15 Feb 2026 12:33:23 +0500 Subject: [PATCH 11/14] implement new editable filename component and sync it witth old renaming functionality --- Co-authored-by: Cole Bemis Co-authored-by: Muhammad-Aqib-Bashir --- src/routes/_appRoot.notes_.$.tsx | 180 +++++++++++++++++++++---------- 1 file changed, 125 insertions(+), 55 deletions(-) diff --git a/src/routes/_appRoot.notes_.$.tsx b/src/routes/_appRoot.notes_.$.tsx index 2ef7dbcd..ec1f5713 100644 --- a/src/routes/_appRoot.notes_.$.tsx +++ b/src/routes/_appRoot.notes_.$.tsx @@ -46,6 +46,7 @@ import { PageLayout } from "../components/page-layout" import { PillButton } from "../components/pill-button" import { SegmentedControl } from "../components/segmented-control" import { ShareDialog } from "../components/share-dialog" +import { TextInput } from "../components/text-input" import { Tooltip } from "../components/tooltip" import { Tool, voiceConversationMachineAtom } from "../components/voice-conversation" import { @@ -54,6 +55,8 @@ import { githubRepoAtom, globalStateMachineAtom, isSignedOutAtom, + markdownFilesAtom, + promptToNameFilesAtom, vimModeAtom, weeklyTemplateAtom, } from "../global-state" @@ -66,10 +69,11 @@ import { cx } from "../utils/cx" import { formatDate, formatWeek, isValidDateString, isValidWeekString } from "../utils/date" import { updateFrontmatterValue } from "../utils/frontmatter" import { clearNoteDraft, getNoteDraft, setNoteDraft } from "../utils/note-draft" -import { getInvalidNoteIdCharacters } from "../utils/note-id" +import { getInvalidNoteIdCharacters, isValidNoteId } from "../utils/note-id" import { parseNote } from "../utils/parse-note" import { pluralize } from "../utils/pluralize" import { notificationSound, playSound } from "../utils/sounds" +import { EditableFilename, EditableFilenameHandle } from "../components/editable-filename" type RouteSearch = { mode: "read" | "write" @@ -133,6 +137,8 @@ function NotePage() { const dailyTemplate = useAtomValue(dailyTemplateAtom) const weeklyTemplate = useAtomValue(weeklyTemplateAtom) const defaultFont = useAtomValue(defaultFontAtom) + const markdownFiles = useAtomValue(markdownFilesAtom) + const promptToNameFiles = useAtomValue(promptToNameFilesAtom) const { online } = useNetworkState() // Note data @@ -177,6 +183,8 @@ function NotePage() { const parsedWidthResult = widthSchema.safeParse(frontmatterWidth) const resolvedWidth = parsedWidthResult.success ? parsedWidthResult.data : "fixed" + const editableFilenameRef = React.useRef(null) + // Set the font React.useEffect(() => { document.documentElement.style.setProperty( @@ -198,6 +206,62 @@ function NotePage() { const renameNote = useRenameNote() const attachFile = useAttachFile() + const normalizeNoteId = React.useCallback((rawName: string) => { + return rawName.trim().replace(/\.md$/i, "").trim() + }, []) + + const attemptRename = React.useCallback( + (rawName: string): boolean => { + if (!noteId) return false + + const newNoteId = normalizeNoteId(rawName) + + if (!newNoteId || newNoteId === noteId) { + return true + } + + const result = renameNote({ + oldName: noteId, + newName: newNoteId, + content: editorValue, + }) + + if (!result.success) { + switch (result.reason) { + case "no-op": + return true + case "invalid": + { + const invalidCharacters = Array.from(new Set(getInvalidNoteIdCharacters(newNoteId))) + const invalidList = invalidCharacters.map((char) => `"${char}"`).join(", ") + const suffix = invalidList ? `: ${invalidList}` : "" + window.alert(`"${newNoteId}.md" contains invalid characters${suffix}`) + } + return false + case "duplicate": + window.alert(`"${newNoteId}.md" already exists.`) + return false + default: + result.reason satisfies never + } + return false + } + + clearNoteDraft({ githubRepo, noteId }) + clearNoteDraft({ githubRepo, noteId: newNoteId }) + + navigate({ + to: "/notes/$", + params: { _splat: newNoteId }, + search: (prev) => ({ ...prev, content: undefined }), + replace: true, + }) + + return true + }, + [noteId, normalizeNoteId, renameNote, editorValue, githubRepo, navigate], + ) + const handleSave = React.useCallback( (value: string) => { if (isSignedOut || !noteId) return @@ -205,6 +269,46 @@ function NotePage() { // New notes shouldn't be saved if the editor is empty if (!note && !value) return + if (!note && value && promptToNameFiles && !isDailyNote && !isWeeklyNote) { + const rawName = window.prompt("Name this file", noteId) + + if (rawName === null) return + + const newNoteId = normalizeNoteId(rawName) + + if (!newNoteId) { + return + } + + if (newNoteId !== noteId) { + if (!isValidNoteId(newNoteId)) { + const invalidCharacters = Array.from(new Set(getInvalidNoteIdCharacters(newNoteId))) + const invalidList = invalidCharacters.map((char) => `"${char}"`).join(", ") + const suffix = invalidList ? `: ${invalidList}` : "" + window.alert(`"${newNoteId}.md" contains invalid characters${suffix}`) + return + } + + if (markdownFiles[`${newNoteId}.md`]) { + window.alert(`"${newNoteId}.md" already exists.`) + return + } + + saveNote({ id: newNoteId, content: value }) + clearNoteDraft({ githubRepo, noteId }) + clearNoteDraft({ githubRepo, noteId: newNoteId }) + + navigate({ + to: "/notes/$", + params: { _splat: newNoteId }, + search: (prev) => ({ ...prev, content: undefined }), + replace: true, + }) + + return + } + } + // Only save if the content has changed if (value !== note?.content) { saveNote({ id: noteId, content: value }) @@ -212,7 +316,19 @@ function NotePage() { clearNoteDraft({ githubRepo, noteId }) }, - [isSignedOut, noteId, note, saveNote, githubRepo], + [ + isSignedOut, + noteId, + note, + promptToNameFiles, + isDailyNote, + isWeeklyNote, + normalizeNoteId, + markdownFiles, + saveNote, + githubRepo, + navigate, + ], ) const updateWidth = React.useCallback( @@ -231,57 +347,6 @@ function NotePage() { [noteId, editorValue, setEditorValue, handleSave], ) - const handleRename = React.useCallback(() => { - if (!noteId) return - - const oldNoteId = noteId - const newNoteIdRaw = window.prompt("Rename file", oldNoteId) - if (!newNoteIdRaw) return - - const newNoteIdTrimmed = newNoteIdRaw.trim() - if (!newNoteIdTrimmed) return - - const newNoteId = newNoteIdTrimmed.replace(/\.md$/i, "").trim() - if (!newNoteId || newNoteId === oldNoteId) return - - const result = renameNote({ - oldName: oldNoteId, - newName: newNoteId, - content: editorValue, - }) - - if (!result.success) { - switch (result.reason) { - case "no-op": - return - case "invalid": - { - const invalidCharacters = Array.from(new Set(getInvalidNoteIdCharacters(newNoteId))) - const invalidList = invalidCharacters.map((char) => `"${char}"`).join(", ") - const suffix = invalidList ? `: ${invalidList}` : "" - window.alert(`"${newNoteId}.md" contains invalid characters${suffix}`) - } - return - case "duplicate": - window.alert(`"${newNoteId}.md" already exists.`) - return - default: - result.reason satisfies never - } - return - } - - clearNoteDraft({ githubRepo, noteId: oldNoteId }) - clearNoteDraft({ githubRepo, noteId: newNoteId }) - - navigate({ - to: "/notes/$", - params: { _splat: newNoteId }, - search: (prev) => ({ ...prev, content: undefined }), - replace: true, - }) - }, [noteId, renameNote, editorValue, githubRepo, navigate]) - const switchToWriting = React.useCallback(() => { navigate({ search: (prev) => ({ ...prev, mode: "write" }), replace: true }) setTimeout(() => { @@ -509,7 +574,12 @@ function NotePage() { - {noteId}.md + {isDraft ? ( } disabled={isSignedOut} - onClick={handleRename} + onClick={() => editableFilenameRef.current?.startRename()} > Rename file From dd656cc71341a1e2189aa35a91a93ffb080fe111 Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Sun, 15 Feb 2026 12:44:59 +0500 Subject: [PATCH 12/14] refactor: remove unused TextInput import --- src/routes/_appRoot.notes_.$.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/_appRoot.notes_.$.tsx b/src/routes/_appRoot.notes_.$.tsx index ec1f5713..120bdf8e 100644 --- a/src/routes/_appRoot.notes_.$.tsx +++ b/src/routes/_appRoot.notes_.$.tsx @@ -46,7 +46,6 @@ import { PageLayout } from "../components/page-layout" import { PillButton } from "../components/pill-button" import { SegmentedControl } from "../components/segmented-control" import { ShareDialog } from "../components/share-dialog" -import { TextInput } from "../components/text-input" import { Tooltip } from "../components/tooltip" import { Tool, voiceConversationMachineAtom } from "../components/voice-conversation" import { @@ -183,6 +182,7 @@ function NotePage() { const parsedWidthResult = widthSchema.safeParse(frontmatterWidth) const resolvedWidth = parsedWidthResult.success ? parsedWidthResult.data : "fixed" + // Handle for programmatic control of filename editing const editableFilenameRef = React.useRef(null) // Set the font From 105116d8c4a671a3de5a541553a1069a9808aecb Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Tue, 17 Feb 2026 12:55:36 +0500 Subject: [PATCH 13/14] remove option in settings to ask for file name on first save --- src/global-state.ts | 2 -- src/routes/_appRoot.notes_.$.tsx | 56 +------------------------------- src/routes/_appRoot.settings.tsx | 22 ------------- 3 files changed, 1 insertion(+), 79 deletions(-) diff --git a/src/global-state.ts b/src/global-state.ts index 63649819..9028e300 100644 --- a/src/global-state.ts +++ b/src/global-state.ts @@ -869,8 +869,6 @@ export const isHelpPanelOpenAtom = atomWithStorage("help-panel", false) export const calendarLayoutAtom = atomWithStorage<"week" | "month">("calendar-layout", "week") -export const promptToNameFilesAtom = atomWithStorage("prompt-to-name-files", true) - // ----------------------------------------------------------------------------- // AI // ----------------------------------------------------------------------------- diff --git a/src/routes/_appRoot.notes_.$.tsx b/src/routes/_appRoot.notes_.$.tsx index 120bdf8e..bef57957 100644 --- a/src/routes/_appRoot.notes_.$.tsx +++ b/src/routes/_appRoot.notes_.$.tsx @@ -55,7 +55,6 @@ import { globalStateMachineAtom, isSignedOutAtom, markdownFilesAtom, - promptToNameFilesAtom, vimModeAtom, weeklyTemplateAtom, } from "../global-state" @@ -137,7 +136,6 @@ function NotePage() { const weeklyTemplate = useAtomValue(weeklyTemplateAtom) const defaultFont = useAtomValue(defaultFontAtom) const markdownFiles = useAtomValue(markdownFilesAtom) - const promptToNameFiles = useAtomValue(promptToNameFilesAtom) const { online } = useNetworkState() // Note data @@ -269,46 +267,6 @@ function NotePage() { // New notes shouldn't be saved if the editor is empty if (!note && !value) return - if (!note && value && promptToNameFiles && !isDailyNote && !isWeeklyNote) { - const rawName = window.prompt("Name this file", noteId) - - if (rawName === null) return - - const newNoteId = normalizeNoteId(rawName) - - if (!newNoteId) { - return - } - - if (newNoteId !== noteId) { - if (!isValidNoteId(newNoteId)) { - const invalidCharacters = Array.from(new Set(getInvalidNoteIdCharacters(newNoteId))) - const invalidList = invalidCharacters.map((char) => `"${char}"`).join(", ") - const suffix = invalidList ? `: ${invalidList}` : "" - window.alert(`"${newNoteId}.md" contains invalid characters${suffix}`) - return - } - - if (markdownFiles[`${newNoteId}.md`]) { - window.alert(`"${newNoteId}.md" already exists.`) - return - } - - saveNote({ id: newNoteId, content: value }) - clearNoteDraft({ githubRepo, noteId }) - clearNoteDraft({ githubRepo, noteId: newNoteId }) - - navigate({ - to: "/notes/$", - params: { _splat: newNoteId }, - search: (prev) => ({ ...prev, content: undefined }), - replace: true, - }) - - return - } - } - // Only save if the content has changed if (value !== note?.content) { saveNote({ id: noteId, content: value }) @@ -316,19 +274,7 @@ function NotePage() { clearNoteDraft({ githubRepo, noteId }) }, - [ - isSignedOut, - noteId, - note, - promptToNameFiles, - isDailyNote, - isWeeklyNote, - normalizeNoteId, - markdownFiles, - saveNote, - githubRepo, - navigate, - ], + [isSignedOut, noteId, note, saveNote, githubRepo], ) const updateWidth = React.useCallback( diff --git a/src/routes/_appRoot.settings.tsx b/src/routes/_appRoot.settings.tsx index 11aea92e..5e800871 100644 --- a/src/routes/_appRoot.settings.tsx +++ b/src/routes/_appRoot.settings.tsx @@ -19,7 +19,6 @@ import { isCloningRepoAtom, isRepoClonedAtom, isRepoNotClonedAtom, - promptToNameFilesAtom, vimModeAtom, voiceAssistantEnabledAtom, } from "../global-state" @@ -38,7 +37,6 @@ function RouteComponent() {
- @@ -157,26 +155,6 @@ function GitHubSection() { ) } -function FileManagementSection() { - const [promptToName, setPromptToName] = useAtom(promptToNameFilesAtom) - - return ( - -
-
- - -
-

- If disabled, files are automatically saved with current time. -

-
-
- ) -} - function AppearanceSection() { const [epaper, setEpaper] = useAtom(epaperAtom) From d87c4c5ff64e0332edc6650ca3c4638794e5d4fb Mon Sep 17 00:00:00 2001 From: Muhammad-Aqib-Bashir Date: Tue, 17 Feb 2026 13:24:01 +0500 Subject: [PATCH 14/14] fix accessibility and focus styles via keyboard "tab" --- src/components/editable-filename.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/editable-filename.tsx b/src/components/editable-filename.tsx index 64fc84c1..87342b58 100644 --- a/src/components/editable-filename.tsx +++ b/src/components/editable-filename.tsx @@ -95,7 +95,7 @@ export const EditableFilename = forwardRef .md
@@ -111,10 +111,9 @@ export const EditableFilename = forwardRef { e.preventDefault()