diff --git a/frontend/app.js b/frontend/app.js
index 627790d..0d1b3fa 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -43,8 +43,23 @@ const LOCAL_SETTINGS = {
hideUnderscoreFolders: { key: 'hideUnderscoreFolders', type: 'boolean', default: false },
tabInsertsTab: { key: 'tabInsertsTab', type: 'boolean', default: false },
sidebarPanelCollapsed: { key: 'sidebarPanelCollapsed', type: 'boolean', default: false },
+ // When true, the New Note name modal opens with a yyyymmddHHMMSS title pre-selected
+ // (Zettelkasten-style). Press Enter to accept, or type to replace. See issue #217.
+ autoFillNoteTitle: { key: 'autoFillNoteTitle', type: 'boolean', default: false },
// String settings
sortMode: { key: 'sortMode', type: 'string', default: 'a-z' },
+ // What clicking + (or the top-level New button) does. 'chooser' preserves the
+ // current dropdown-with-4-options behaviour; the other values dispatch straight
+ // to that creation flow. Shift+click always falls back to 'chooser'. See #217.
+ newButtonAction: {
+ key: 'newButtonAction', type: 'string', default: 'chooser',
+ valid: ['chooser', 'note', 'folder', 'template', 'drawing']
+ },
+ // Name of the last template that successfully created a note. Used to pre-select
+ // it the next time the "Create from Template" modal opens, so the common case of
+ // "I always use the same template" is one click less. Cleared / ignored silently
+ // if the template no longer exists.
+ lastUsedTemplate: { key: 'lastUsedTemplate', type: 'string', default: '' },
// Number settings with validation
sidebarWidth: { key: 'sidebarWidth', type: 'number', default: CONFIG.DEFAULT_SIDEBAR_WIDTH, min: 200, max: 600 },
editorWidth: { key: 'editorWidth', type: 'number', default: 50, min: 20, max: 80 },
@@ -408,6 +423,12 @@ function noteApp() {
availableTemplates: [],
selectedTemplate: '',
newTemplateNoteName: '',
+ // Persisted via LOCAL_SETTINGS; hydrated by loadLocalSettings() at app init.
+ // Inline defaults below mirror the LOCAL_SETTINGS defaults so the reactive
+ // proxy has the right type/shape before hydration runs. See issue #217.
+ newButtonAction: 'chooser',
+ autoFillNoteTitle: false,
+ lastUsedTemplate: '',
// New note / folder name modal (replaces window.prompt)
showCreateNameModal: false,
@@ -1742,6 +1763,11 @@ function noteApp() {
const data = await response.json();
+ // Remember for pre-selection next time openTemplateModal() runs (see #217).
+ // Captured BEFORE the reset below so the name is still available.
+ this.lastUsedTemplate = this.selectedTemplate;
+ try { localStorage.setItem('lastUsedTemplate', this.selectedTemplate); } catch (_) {}
+
// Close modal and reset state
this.showTemplateModal = false;
this.selectedTemplate = '';
@@ -4570,16 +4596,49 @@ function noteApp() {
// DROPDOWN MENU SYSTEM
// =====================================================
+ // Entry point for every "+" button and the top-level "New" button. Either opens
+ // the type-chooser dropdown (default behaviour) or dispatches straight to a
+ // single creation flow, depending on the user's `newButtonAction` setting.
+ //
+ // Shift+click ALWAYS forces the chooser regardless of configured default — a
+ // single discoverable escape hatch (mentioned in the settings description) that
+ // saves users from needing to open Settings just to reach the other modes once
+ // in a while. See issue #217.
+ //
+ // Folder context (dropdownTargetFolder) is set by the calling site BEFORE this
+ // method runs; each dispatched action reads it via inferredNewItemTargetFolder()
+ // and clears the dropdown state itself, so we don't call closeDropdown() here.
toggleNewDropdown(event) {
- this.showNewDropdown = true; // Always open (or keep open)
+ const wantsChooser =
+ (event && event.shiftKey) ||
+ !this.newButtonAction ||
+ this.newButtonAction === 'chooser';
+
+ if (wantsChooser) {
+ this._openNewDropdownChooser(event);
+ return;
+ }
+
+ switch (this.newButtonAction) {
+ case 'note': this.createNote(); break;
+ case 'folder': this.createFolder(); break;
+ case 'template': this.openTemplateModal(); break;
+ case 'drawing': this.createNewDrawing(); break;
+ default: this._openNewDropdownChooser(event);
+ }
+ },
+
+ // Open the type-chooser dropdown anchored next to the clicking element. Extracted
+ // verbatim from the original toggleNewDropdown body so existing positioning logic
+ // is preserved bit-for-bit; the new routing layer above is a thin wrapper.
+ _openNewDropdownChooser(event) {
+ this.showNewDropdown = true;
if (event && event.target) {
const rect = event.target.getBoundingClientRect();
- // Position dropdown next to the clicked element
- let top = rect.bottom + 4; // 4px spacing
+ let top = rect.bottom + 4;
let left = rect.left;
- // Keep dropdown on screen
const dropdownWidth = 200;
const dropdownHeight = 150;
if (left + dropdownWidth > window.innerWidth) {
@@ -4593,6 +4652,37 @@ function noteApp() {
}
},
+ // Open "Create from Template" modal. Shared by:
+ // - the type-chooser dropdown's "from template" button
+ // - the quick-dispatch path from toggleNewDropdown()
+ // so both paths consistently pre-select the last-used template (if it still
+ // exists) AND pre-fill the name when autoFillNoteTitle is on, matching the
+ // plain New Note popup behaviour. dropdownTargetFolder is intentionally NOT
+ // cleared — it's read at submit time by createNoteFromTemplate() via
+ // inferredNewItemTargetFolder(). See issue #217.
+ openTemplateModal() {
+ this.showNewDropdown = false;
+ this.mobileSidebarOpen = false;
+ const hasLastUsed = !!this.lastUsedTemplate &&
+ this.availableTemplates.some(t => t.name === this.lastUsedTemplate);
+ this.selectedTemplate = hasLastUsed ? this.lastUsedTemplate : '';
+ this.newTemplateNoteName = this.autoFillNoteTitle ? this._autoTitleTimestamp() : '';
+ this.showTemplateModal = true;
+ // If a template was pre-selected the modal can be submitted with Enter
+ // alone, so focus the name input and select its contents (so typing
+ // immediately replaces). When no template is pre-selected the user has
+ // to pick one first, so we don't steal focus from the dropdown.
+ if (hasLastUsed) {
+ this.$nextTick(() => {
+ const el = document.getElementById('template-note-name-input');
+ if (el) {
+ el.focus();
+ el.select();
+ }
+ });
+ }
+ },
+
closeDropdown() {
this.showNewDropdown = false;
this.dropdownTargetFolder = null; // Reset folder context
@@ -4653,7 +4743,12 @@ function noteApp() {
this.mobileSidebarOpen = false;
this.createNameModalKind = kind;
this.createNameModalTargetFolder = targetFolder;
- this.createNameModalInput = '';
+ // Auto-fill title with a yyyymmddHHMMSS timestamp when the user opted in
+ // (notes only; doesn't make sense for folders). The input gets .select()'d
+ // below, so typing anything immediately replaces it — Enter accepts it as
+ // a Zettelkasten-style filename. See issue #217.
+ this.createNameModalInput =
+ (kind === 'note' && this.autoFillNoteTitle) ? this._autoTitleTimestamp() : '';
this.showCreateNameModal = true;
this.$nextTick(() => {
const el = document.getElementById('create-name-modal-input');
@@ -4664,6 +4759,16 @@ function noteApp() {
});
},
+ // Zettelkasten convention: yyyymmddHHMMSS — compact, sorts chronologically,
+ // and contains no characters that need escaping on any filesystem. Local
+ // time deliberately (matches what users see on the clock).
+ _autoTitleTimestamp() {
+ const d = new Date();
+ const pad = (n) => String(n).padStart(2, '0');
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}` +
+ `${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
+ },
+
closeCreateNameModal() {
this.showCreateNameModal = false;
this.createNameModalInput = '';
diff --git a/frontend/index.html b/frontend/index.html
index a2b9f06..00d60a6 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2227,6 +2227,46 @@