experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(tags): embed editor in detail pane for inline note editing

+235 -40
+53 -12
extensions/tags/home.css
··· 430 430 display: none; 431 431 } 432 432 433 - /* Detail content section (for text items) */ 434 - .detail-content-section { 433 + /* Embedded editor section (for text items) */ 434 + .detail-editor-section { 435 435 margin-bottom: 20px; 436 + display: flex; 437 + flex-direction: column; 438 + } 439 + 440 + .detail-editor-header { 441 + display: flex; 442 + align-items: center; 443 + justify-content: space-between; 444 + margin-bottom: 8px; 445 + } 446 + 447 + .detail-editor-header .detail-label { 448 + margin-bottom: 0; 436 449 } 437 450 438 - .detail-content-text { 451 + .detail-save-status { 452 + font-size: 11px; 453 + font-weight: 500; 454 + padding: 2px 8px; 455 + border-radius: 3px; 456 + user-select: none; 457 + transition: color 0.2s, background 0.2s; 458 + } 459 + 460 + .detail-save-status-saved { 461 + color: var(--base0B); 462 + } 463 + 464 + .detail-save-status-saving { 465 + color: var(--base0A); 466 + } 467 + 468 + .detail-save-status-unsaved { 469 + color: var(--base08); 439 470 background: var(--base01); 440 - border: 1px solid var(--base02); 471 + } 472 + 473 + .detail-editor-container { 441 474 border-radius: 8px; 442 - padding: 14px; 443 - font-size: 14px; 444 - color: var(--base05); 445 - white-space: pre-wrap; 446 - word-break: break-word; 447 - max-height: 300px; 448 - overflow-y: auto; 449 - line-height: 1.6; 475 + overflow: hidden; 476 + min-height: 200px; 477 + max-height: 50vh; 478 + display: flex; 479 + flex-direction: column; 480 + } 481 + 482 + .detail-editor-container .cm-editor { 483 + height: 100%; 484 + min-height: 200px; 485 + max-height: 50vh; 486 + border-radius: 8px; 487 + } 488 + 489 + .detail-editor-container .cm-scroller { 490 + overflow: auto; 450 491 } 451 492 452 493 /* Detail sections (tags) */
+30 -4
extensions/tags/home.html
··· 18 18 "lit-element": "peek://node_modules/lit-element/lit-element.js", 19 19 "lit-element/": "peek://node_modules/lit-element/", 20 20 "@lit/reactive-element": "peek://node_modules/@lit/reactive-element/reactive-element.js", 21 - "@lit/reactive-element/": "peek://node_modules/@lit/reactive-element/" 21 + "@lit/reactive-element/": "peek://node_modules/@lit/reactive-element/", 22 + "@codemirror/state": "peek://node_modules/@codemirror/state/dist/index.js", 23 + "@codemirror/view": "peek://node_modules/@codemirror/view/dist/index.js", 24 + "@codemirror/commands": "peek://node_modules/@codemirror/commands/dist/index.js", 25 + "@codemirror/language": "peek://node_modules/@codemirror/language/dist/index.js", 26 + "@codemirror/autocomplete": "peek://node_modules/@codemirror/autocomplete/dist/index.js", 27 + "@codemirror/lang-markdown": "peek://node_modules/@codemirror/lang-markdown/dist/index.js", 28 + "@codemirror/lang-html": "peek://node_modules/@codemirror/lang-html/dist/index.js", 29 + "@codemirror/lang-css": "peek://node_modules/@codemirror/lang-css/dist/index.js", 30 + "@codemirror/lang-javascript": "peek://node_modules/@codemirror/lang-javascript/dist/index.js", 31 + "@codemirror/theme-one-dark": "peek://node_modules/@codemirror/theme-one-dark/dist/index.js", 32 + "@codemirror/search": "peek://node_modules/@codemirror/search/dist/index.js", 33 + "@replit/codemirror-vim": "peek://node_modules/@replit/codemirror-vim/dist/index.js", 34 + "@lezer/common": "peek://node_modules/@lezer/common/dist/index.js", 35 + "@lezer/highlight": "peek://node_modules/@lezer/highlight/dist/index.js", 36 + "@lezer/lr": "peek://node_modules/@lezer/lr/dist/index.js", 37 + "@lezer/markdown": "peek://node_modules/@lezer/markdown/dist/index.js", 38 + "@lezer/html": "peek://node_modules/@lezer/html/dist/index.js", 39 + "@lezer/css": "peek://node_modules/@lezer/css/dist/index.js", 40 + "@lezer/javascript": "peek://node_modules/@lezer/javascript/dist/index.js", 41 + "crelt": "peek://node_modules/crelt/index.js", 42 + "style-mod": "peek://node_modules/style-mod/src/style-mod.js", 43 + "w3c-keyname": "peek://node_modules/w3c-keyname/index.js", 44 + "@marijn/find-cluster-break": "peek://node_modules/@marijn/find-cluster-break/src/index.js" 22 45 } 23 46 } 24 47 </script> ··· 110 133 111 134 <div class="detail-actions"></div> 112 135 113 - <div class="detail-content-section" style="display: none;"> 114 - <label class="detail-label">Content</label> 115 - <div class="detail-content-text"></div> 136 + <div class="detail-editor-section" style="display: none;"> 137 + <div class="detail-editor-header"> 138 + <label class="detail-label">Content</label> 139 + <span class="detail-save-status"></span> 140 + </div> 141 + <div class="detail-editor-container"></div> 116 142 </div> 117 143 118 144 <div class="detail-section">
+152 -24
extensions/tags/home.js
··· 8 8 * - Content search across items (URLs, titles, text) with tag combo filtering 9 9 * - Selected tags pinned to top of sidebar with visual distinction 10 10 * - Inline detail view (no modal/dialog) with proper ESC navigation 11 + * - Embedded CodeMirror editor for inline text editing 11 12 */ 13 + 14 + import * as CodeMirror from 'peek://extensions/editor/codemirror.js'; 12 15 13 16 const api = window.app; 14 17 const debug = api?.debug; ··· 85 88 86 89 // Expose state for debugging 87 90 window._tagsState = state; 91 + 92 + // Embedded editor state 93 + let embeddedEditor = null; // CodeMirror EditorView instance 94 + let editorSaveTimer = null; 95 + let editorSaveStatus = 'saved'; // 'saved' | 'saving' | 'unsaved' 96 + const EDITOR_AUTOSAVE_DELAY_MS = 1500; 88 97 89 98 // DOM elements 90 99 let searchInput; ··· 183 192 184 193 // If in detail view, navigate back to list 185 194 if (state.currentView === VIEW_DETAIL) { 195 + // If editor has unsaved changes, flush them before leaving 196 + if (embeddedEditor && editorSaveStatus === 'unsaved') { 197 + const content = CodeMirror.getContent(embeddedEditor); 198 + performEditorAutosave(content); 199 + } 186 200 // Use setTimeout to ensure handler returns before DOM work starts 187 201 setTimeout(() => { 188 202 showList(); ··· 232 246 if (state.currentView === VIEW_LIST) { 233 247 render(); 234 248 } else if (state.currentView === VIEW_DETAIL && state.editingItem) { 235 - // Refresh detail view with updated data 249 + // Refresh detail view with updated data, but don't re-create the editor 250 + // if the user is actively editing (the editor manages its own content) 236 251 const updatedItem = state.items.find(i => i.id === state.editingItem.id); 237 252 if (updatedItem) { 238 253 state.editingItem = updatedItem; 239 - showDetailView(updatedItem); 254 + // Only refresh tags, not the editor content 255 + const tags = state.itemTags.get(updatedItem.id) || []; 256 + renderCurrentTags(tags); 257 + renderAvailableTags(tags); 240 258 } else { 241 259 // Item was deleted, go back to list 242 260 showList(); ··· 338 356 */ 339 357 const handleKeydown = (e) => { 340 358 // If in detail view, don't process grid navigation 359 + // (especially important when embedded editor is focused) 341 360 if (state.currentView === VIEW_DETAIL) { 342 361 return; 343 362 } ··· 459 478 const showList = () => { 460 479 state.currentView = VIEW_LIST; 461 480 state.editingItem = null; 481 + 482 + // Clean up embedded editor before switching views 483 + destroyEmbeddedEditor(); 462 484 463 485 cardsContainer.style.display = ''; 464 486 detailView.style.display = 'none'; ··· 853 875 }; 854 876 855 877 /** 878 + * Update the save status indicator in the detail editor 879 + */ 880 + const updateEditorSaveStatus = () => { 881 + const statusEl = document.querySelector('.detail-save-status'); 882 + if (!statusEl) return; 883 + 884 + statusEl.className = `detail-save-status detail-save-status-${editorSaveStatus}`; 885 + switch (editorSaveStatus) { 886 + case 'saved': 887 + statusEl.textContent = 'Saved'; 888 + break; 889 + case 'saving': 890 + statusEl.textContent = 'Saving...'; 891 + break; 892 + case 'unsaved': 893 + statusEl.textContent = 'Unsaved'; 894 + break; 895 + } 896 + }; 897 + 898 + /** 899 + * Perform autosave of the embedded editor content to the datastore 900 + */ 901 + const performEditorAutosave = async (content) => { 902 + if (!state.editingItem || !api?.datastore?.updateItem) return; 903 + 904 + editorSaveStatus = 'saving'; 905 + updateEditorSaveStatus(); 906 + 907 + try { 908 + const result = await api.datastore.updateItem(state.editingItem.id, { content }); 909 + if (result.success) { 910 + editorSaveStatus = 'saved'; 911 + debug && console.log('[tags] Editor autosaved item:', state.editingItem.id); 912 + 913 + // Update local item state so list view reflects changes 914 + state.editingItem.content = content; 915 + const idx = state.items.findIndex(i => i.id === state.editingItem.id); 916 + if (idx >= 0) { 917 + state.items[idx].content = content; 918 + } 919 + 920 + // Publish change event for other extensions 921 + if (api?.publish) { 922 + api.publish('editor:changed', { action: 'update', itemId: state.editingItem.id }, api.scopes.GLOBAL); 923 + } 924 + } else { 925 + console.error('[tags] Editor autosave failed:', result.error); 926 + editorSaveStatus = 'unsaved'; 927 + } 928 + } catch (err) { 929 + console.error('[tags] Editor autosave error:', err); 930 + editorSaveStatus = 'unsaved'; 931 + } 932 + 933 + updateEditorSaveStatus(); 934 + }; 935 + 936 + /** 937 + * Schedule an autosave with debouncing for the embedded editor 938 + */ 939 + const scheduleEditorAutosave = (content) => { 940 + editorSaveStatus = 'unsaved'; 941 + updateEditorSaveStatus(); 942 + 943 + if (editorSaveTimer) { 944 + clearTimeout(editorSaveTimer); 945 + } 946 + 947 + editorSaveTimer = setTimeout(() => { 948 + performEditorAutosave(content); 949 + }, EDITOR_AUTOSAVE_DELAY_MS); 950 + }; 951 + 952 + /** 953 + * Create the embedded CodeMirror editor for a text item 954 + */ 955 + const createEmbeddedEditor = (item) => { 956 + destroyEmbeddedEditor(); 957 + 958 + const container = document.querySelector('.detail-editor-container'); 959 + if (!container) return; 960 + 961 + // Clear the container 962 + container.innerHTML = ''; 963 + 964 + embeddedEditor = CodeMirror.createEditor({ 965 + parent: container, 966 + content: item.content || '', 967 + vimMode: false, 968 + showLineNumbers: true, 969 + onChange: (content) => { 970 + scheduleEditorAutosave(content); 971 + }, 972 + }); 973 + 974 + editorSaveStatus = 'saved'; 975 + updateEditorSaveStatus(); 976 + 977 + // Focus the editor after a short delay to let layout settle 978 + setTimeout(() => { 979 + if (embeddedEditor) CodeMirror.focus(embeddedEditor); 980 + }, 100); 981 + }; 982 + 983 + /** 984 + * Destroy the embedded CodeMirror editor instance and clean up 985 + */ 986 + const destroyEmbeddedEditor = () => { 987 + if (editorSaveTimer) { 988 + clearTimeout(editorSaveTimer); 989 + editorSaveTimer = null; 990 + } 991 + 992 + if (embeddedEditor) { 993 + CodeMirror.destroy(embeddedEditor); 994 + embeddedEditor = null; 995 + } 996 + 997 + editorSaveStatus = 'saved'; 998 + }; 999 + 1000 + /** 856 1001 * Show the inline detail view for an item (replaces card grid) 857 1002 */ 858 1003 const showDetailView = (item) => { ··· 891 1036 actionsEl.appendChild(openPageBtn); 892 1037 } 893 1038 1039 + // Show embedded editor for text items 1040 + const editorSection = detailView.querySelector('.detail-editor-section'); 894 1041 if (itemType === 'text') { 895 - const editBtn = document.createElement('peek-button'); 896 - editBtn.variant = 'ghost'; 897 - editBtn.size = 'sm'; 898 - editBtn.innerHTML = ` 899 - <svg slot="prefix" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 900 - <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> 901 - <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> 902 - </svg> 903 - Edit in Editor 904 - `; 905 - editBtn.addEventListener('click', () => { 906 - api.publish('editor:open', { itemId: item.id }, api.scopes.GLOBAL); 907 - }); 908 - actionsEl.appendChild(editBtn); 909 - } 910 - 911 - // Show content section for text items 912 - const contentSection = detailView.querySelector('.detail-content-section'); 913 - if (itemType === 'text' && item.content) { 914 - contentSection.style.display = ''; 915 - detailView.querySelector('.detail-content-text').textContent = item.content; 1042 + editorSection.style.display = ''; 1043 + createEmbeddedEditor(item); 916 1044 } else { 917 - contentSection.style.display = 'none'; 1045 + editorSection.style.display = 'none'; 918 1046 } 919 1047 920 1048 // Render current tags