experiments in a post-browser web
10
fork

Configure Feed

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

hjkl and arrows in groups views

+135 -5
+4 -3
TODO.md
··· 1 1 # Roadmap 2 2 3 - ## v0.4 - Minimum viable web workbench 3 + ## v? - Minimum viable web workbench 4 4 5 5 - [ ] Design philosophy write-up w/ driving principles and characteristics 6 6 - [ ] Multi-protocol architecture ··· 16 16 17 17 - [ ] Notes in datastore 18 18 - [ ] Editor app (in/out datastore) 19 + - [ ] Vim mode 20 + - [ ] List editor 19 21 20 22 ## v? - Extensibility 21 23 ··· 28 30 - [ ] annotate all data with source creator/editor (sys vs extension) 29 31 - [ ] figure out in-extension settings vs jamming in global settings 30 32 - [ ] syncable settings (extension decides) 33 + - [ ] App cmds (eg Editor -> cmd to edit note X) 31 34 32 35 Web extensions 33 36 - [ ] WebExtension integration for priority only, on some platforms 34 - 35 - - [ ] App cmds (eg Editor -> cmd to edit note X) 36 37 37 38 ## v? Portability 38 39
+19
app/groups/home.css
··· 93 93 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 94 94 } 95 95 96 + .card.selected { 97 + background: #333; 98 + outline: 2px solid #0066cc; 99 + outline-offset: -2px; 100 + } 101 + 102 + .card.selected:hover { 103 + background: #383838; 104 + } 105 + 96 106 /* Group card */ 97 107 .group-card .color-dot { 98 108 width: 12px; ··· 184 194 .card:hover { 185 195 background: #fafafa; 186 196 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 197 + } 198 + 199 + .card.selected { 200 + background: #e8f0fe; 201 + outline: 2px solid #0066cc; 202 + } 203 + 204 + .card.selected:hover { 205 + background: #dbe7fc; 187 206 } 188 207 189 208 .card-title {
+110 -1
app/groups/home.js
··· 29 29 tags: [], 30 30 currentTag: null, 31 31 addresses: [], 32 - untaggedCount: 0 32 + untaggedCount: 0, 33 + selectedIndex: 0 33 34 }; 34 35 35 36 // Handle ESC - cooperative escape handling with window manager ··· 45 46 return { handled: false }; 46 47 }); 47 48 49 + /** 50 + * Get all cards in the current view 51 + */ 52 + const getCards = () => { 53 + return Array.from(document.querySelectorAll('.cards .card')); 54 + }; 55 + 56 + /** 57 + * Update visual selection on cards 58 + */ 59 + const updateSelection = () => { 60 + const cards = getCards(); 61 + cards.forEach((card, i) => { 62 + card.classList.toggle('selected', i === state.selectedIndex); 63 + }); 64 + 65 + // Scroll selected card into view 66 + const selected = cards[state.selectedIndex]; 67 + if (selected) { 68 + selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); 69 + } 70 + }; 71 + 72 + /** 73 + * Activate the currently selected card 74 + */ 75 + const activateSelected = () => { 76 + const cards = getCards(); 77 + const selected = cards[state.selectedIndex]; 78 + if (selected) { 79 + selected.click(); 80 + } 81 + }; 82 + 83 + /** 84 + * Get number of columns in the grid based on card positions 85 + */ 86 + const getGridColumns = (cards) => { 87 + if (cards.length < 2) return 1; 88 + const firstTop = cards[0].getBoundingClientRect().top; 89 + for (let i = 1; i < cards.length; i++) { 90 + if (cards[i].getBoundingClientRect().top !== firstTop) { 91 + return i; 92 + } 93 + } 94 + return cards.length; // All on one row 95 + }; 96 + 97 + /** 98 + * Handle keyboard navigation (vim-style hjkl for grid movement) 99 + */ 100 + const handleKeydown = (e) => { 101 + const cards = getCards(); 102 + if (cards.length === 0) return; 103 + 104 + const cols = getGridColumns(cards); 105 + 106 + switch (e.key) { 107 + case 'j': 108 + case 'ArrowDown': 109 + e.preventDefault(); 110 + if (state.selectedIndex + cols < cards.length) { 111 + state.selectedIndex += cols; 112 + updateSelection(); 113 + } 114 + break; 115 + case 'k': 116 + case 'ArrowUp': 117 + e.preventDefault(); 118 + if (state.selectedIndex - cols >= 0) { 119 + state.selectedIndex -= cols; 120 + updateSelection(); 121 + } 122 + break; 123 + case 'h': 124 + case 'ArrowLeft': 125 + e.preventDefault(); 126 + if (state.selectedIndex > 0) { 127 + state.selectedIndex--; 128 + updateSelection(); 129 + } 130 + break; 131 + case 'l': 132 + case 'ArrowRight': 133 + e.preventDefault(); 134 + if (state.selectedIndex < cards.length - 1) { 135 + state.selectedIndex++; 136 + updateSelection(); 137 + } 138 + break; 139 + case 'Enter': 140 + e.preventDefault(); 141 + activateSelected(); 142 + break; 143 + } 144 + }; 145 + 48 146 const init = async () => { 49 147 debug && console.log('Groups init'); 50 148 ··· 54 152 // Set up event listeners 55 153 document.querySelector('.new-group-btn').addEventListener('click', createNewGroup); 56 154 document.querySelector('.back-btn').addEventListener('click', showGroups); 155 + 156 + // Keyboard navigation 157 + document.addEventListener('keydown', handleKeydown); 57 158 58 159 // Show groups view 59 160 showGroups(); ··· 147 248 const card = createGroupCard(tag); 148 249 container.appendChild(card); 149 250 }); 251 + 252 + // Reset selection 253 + state.selectedIndex = 0; 254 + updateSelection(); 150 255 }; 151 256 152 257 /** ··· 186 291 const card = createAddressCard(address); 187 292 container.appendChild(card); 188 293 }); 294 + 295 + // Reset selection 296 + state.selectedIndex = 0; 297 + updateSelection(); 189 298 }; 190 299 191 300 /**
+2 -1
app/groups/index.js
··· 19 19 const params = { 20 20 key: address, 21 21 height, 22 - width 22 + width, 23 + escapeMode: 'navigate' // Allow internal navigation before closing 23 24 }; 24 25 25 26 // Use the window creation API