shortcuts: board arrow-nav, enter→preview→edit, help screen polish, n/a/f/j hotkeys
Board arrow-key card navigation (public/js/app.js):
* ↑/↓/←/→ on the board (body focus, no overlay) move a single-card
focus cursor (.card-focused, accent2 outline) through the visible
card grid. Column count is computed live from the first row's
offsetTop so it tracks the CSS grid columns at any viewport width.
* First arrow press focuses the first card; subsequent presses move
in the grid. Focused card scrolls into view (smooth, nearest).
* Enter on a focused card → openDetail (preview). Focus cleared.
* Enter in detail view → closeDetail + openModal (editor). Card ref
is captured before closeDetail nulls state.viewingCard.
* Click anywhere → focus cleared. Re-renders restore focus via
window._restoreCardFocus() called from board.js renderBoard().
CSS (public/css/style.css):
* .card-focused: accent2 outline + glow, visually distinct from
.card-selected (accent, multi-select) and .preview-active.
* .shortcut-table kbd keycaps: JetBrains Mono font (not pixel),
10px, 3D raised look with gradient + bottom border + inner
highlight + shadow.
* Responsive shortcut grid: 3 cols > 900px, 2 cols > 640px, 1 col.
New bare-key shortcuts (public/js/app.js):
* n → openModal(null) (new card, same as the + button)
* a → activateBucket('all')
* f → activateBucket('favorites')
* j → open today's journal card (find by YYYY-MM-DD prefix in
state.cards), or create a new one (switch to journal view, open
empty editor which auto-fills the date title)
Ctrl+N quick capture (public/js/app.js):
* Now sends bucket_id: state.activeBucket on the POST (unless the
active view is a virtual filter like all/current/etc., in which
case bucket_id stays null). Cards captured while viewing a user
bucket land directly in that bucket.
Shortcuts help screen (public/index.php):
* max-w-4xl, 3-column grid layout.
* Complete shortcut inventory across all modules:
- Navigation: a/f/j/n/t/c/m, 1-9, Tab/Shift+Tab, /
- Global: Ctrl+K/N/Z, Esc, F1, ?, Del
- Editor: Ctrl+S, Ctrl+Shift+S, Ctrl+Enter, Tab, Alt+↑↓,
Shift+Alt+↑↓, Enter (list continue), [[ (link ac), # (tag ac),
?snippet
- Search: ↑↓ navigate, Enter open, Esc close
- Board+Detail: arrows focus, Enter→preview, Enter→editor,
←→ group nav, click/dbl/right/ctrl+click, drag edge/center
- Canvas: b/e/l/r/o/f/t tool shortcuts, ⌫ delete
- Mail: ⌫ delete selected, Shift+⌫ skip confirm
* kbd elements use normal font (JetBrains Mono) instead of pixel.
* Help overlay's old shortcuts section replaced with a one-liner
pointing to ? for the full reference.
* #shortcuts added to runner.js closeAll() list.
chore: bump version to 0.9.12
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c8835825e7
commit
7c113fd5ab
6 files changed
+322
-10
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
define('APP_NAME', 'hyprcrd');
|
||||
define('APP_VERSION', '0.9.11');
|
||||
define('APP_VERSION', '0.9.12');
|
||||
|
||||
// Database path — writable directory, outside webroot
|
||||
define('DB_PATH', __DIR__ . '/data/hyprcrd.db');
|
||||
|
||||
@@ -457,6 +457,11 @@ body::after {
|
||||
}
|
||||
.card-glow:hover { box-shadow: var(--ui-shadow-card), var(--card-stack-shadow), 0 0 16px -3px var(--glow-color, rgba(56, 189, 248, 0.15)); }
|
||||
.card-selected { outline: 2px solid var(--t-accent) !important; outline-offset: -2px; }
|
||||
.card-focused {
|
||||
outline: 2px solid var(--t-accent2) !important;
|
||||
outline-offset: -2px;
|
||||
box-shadow: 0 0 0 1px var(--t-accent2), 0 0 16px -4px color-mix(in srgb, var(--t-accent2) 40%, transparent) !important;
|
||||
}
|
||||
.card-select-check {
|
||||
position: absolute; top: 6px; left: 6px; width: 18px; height: 18px;
|
||||
border-radius: 50%; background: var(--t-accent); z-index: 6;
|
||||
@@ -804,6 +809,42 @@ body.anim-slow .board-card.card-pulse-new {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* Shortcut help table — table-fixed prevents column wobble when content
|
||||
length varies between sections. kbd styling matches typical keyboard-cap
|
||||
look (surface bg, border, mono font, tiny padding). */
|
||||
.shortcut-table { width:100%; table-layout:fixed; font-size:12px; border-collapse:collapse; }
|
||||
.shortcut-table td { padding:4px 0; vertical-align:middle; color:var(--t-text); }
|
||||
.shortcut-table td:first-child { width:48%; color:var(--t-dim); white-space:nowrap; font-size:11px; }
|
||||
/* Keycap look: 3D-ish raised surface with bottom shadow simulating
|
||||
depth, subtle gradient, rounded corners. Min-width so single-letter
|
||||
keys don't look squished. The "+" separator between combo keys sits
|
||||
outside <kbd> and inherits the dim color from the td. */
|
||||
.shortcut-table kbd {
|
||||
display:inline-block;
|
||||
min-width:20px; text-align:center;
|
||||
padding:3px 7px;
|
||||
background: linear-gradient(180deg, color-mix(in srgb, var(--t-raised) 90%, white 10%), var(--t-bg));
|
||||
border:1px solid var(--t-border);
|
||||
border-bottom-width:2px;
|
||||
border-radius:4px;
|
||||
font-family:var(--t-font-content, 'JetBrains Mono', monospace);
|
||||
font-size:10px;
|
||||
color:var(--t-bright);
|
||||
line-height:1.4;
|
||||
box-shadow: 0 1px 0 color-mix(in srgb, var(--t-border) 80%, black 20%),
|
||||
inset 0 1px 0 color-mix(in srgb, var(--t-raised) 60%, white 15%);
|
||||
}
|
||||
.shortcut-table .mouse {
|
||||
font-family:var(--t-font-pixel, 'Silkscreen', cursive);
|
||||
font-size:9px; color:var(--t-dim);
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
#shortcuts [style*="grid-template-columns"] { grid-template-columns: 1fr 1fr !important; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
#shortcuts [style*="grid-template-columns"] { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
|
||||
/* Tag chips */
|
||||
.tag-chip {
|
||||
display: inline-flex; align-items: center; padding: 1px 6px;
|
||||
|
||||
+111
-7
@@ -1322,10 +1322,120 @@ if (str_starts_with($route, '/api/')) {
|
||||
</div>
|
||||
<div id="footer-count" class="font-pixel text-[10px] text-t-dim whitespace-nowrap absolute left-1/2 -translate-x-1/2"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="shortcuts-btn" class="flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-dim hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-keyboard text-sm"></i><span class="font-pixel text-[10px]">Keys</span></button>
|
||||
<button id="help-btn" class="flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-dim hover:border-t-accent hover:text-t-accent transition-colors"><i class="ph ph-question text-sm"></i><span class="font-pixel text-[10px]">Help</span></button>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Shortcuts (?) -->
|
||||
<div class="overlay fixed inset-0 z-[95] flex justify-center items-start pt-12 pb-12 px-8" style="background:var(--t-overlay)" id="shortcuts">
|
||||
<div class="bg-t-surface border border-t-border shadow-2xl w-full max-w-4xl max-h-full overflow-y-auto">
|
||||
<div class="flex items-center gap-3 px-6 py-4 border-b border-t-border sticky top-0 bg-t-surface z-[1]">
|
||||
<i class="ph ph-keyboard text-t-accent text-base"></i>
|
||||
<h2 class="font-pixel text-sm text-t-accent">keyboard shortcuts</h2>
|
||||
<div class="flex-1"></div>
|
||||
<button id="shortcuts-close" class="flex items-center gap-1.5 px-2.5 py-1 text-xs border border-t-border text-t-muted hover:border-t-danger hover:text-t-danger transition-colors"><i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">Close</span></button>
|
||||
</div>
|
||||
<div class="p-6 text-sm text-t-text" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
|
||||
<!-- Column 1 -->
|
||||
<div class="space-y-5">
|
||||
<section>
|
||||
<h3 class="font-pixel text-[10px] text-t-accent mb-2 uppercase tracking-wider">navigation</h3>
|
||||
<table class="shortcut-table"><tbody>
|
||||
<tr><td><kbd>A</kbd></td><td>all cards</td></tr>
|
||||
<tr><td><kbd>F</kbd></td><td>favorites</td></tr>
|
||||
<tr><td><kbd>J</kbd></td><td>today's journal</td></tr>
|
||||
<tr><td><kbd>N</kbd></td><td>new card</td></tr>
|
||||
<tr><td><kbd>T</kbd></td><td>tag quick-pick</td></tr>
|
||||
<tr><td><kbd>C</kbd></td><td>calendar</td></tr>
|
||||
<tr><td><kbd>M</kbd></td><td>mail</td></tr>
|
||||
<tr><td><kbd>1</kbd> – <kbd>9</kbd></td><td>Nth user bucket</td></tr>
|
||||
<tr><td><kbd>⇥</kbd></td><td>next bucket</td></tr>
|
||||
<tr><td><kbd>⇧</kbd> <kbd>⇥</kbd></td><td>prev bucket</td></tr>
|
||||
<tr><td><kbd>/</kbd></td><td>search</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
<section>
|
||||
<h3 class="font-pixel text-[10px] text-t-accent mb-2 uppercase tracking-wider">global</h3>
|
||||
<table class="shortcut-table"><tbody>
|
||||
<tr><td><kbd>⌃</kbd> <kbd>K</kbd></td><td>search</td></tr>
|
||||
<tr><td><kbd>⌃</kbd> <kbd>N</kbd></td><td>quick capture (in bucket)</td></tr>
|
||||
<tr><td><kbd>⌃</kbd> <kbd>Z</kbd></td><td>undo</td></tr>
|
||||
<tr><td><kbd>Esc</kbd></td><td>close overlay</td></tr>
|
||||
<tr><td><kbd>F1</kbd></td><td>help</td></tr>
|
||||
<tr><td><kbd>?</kbd></td><td>this screen</td></tr>
|
||||
<tr><td><kbd>Del</kbd></td><td>delete selected</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
</div>
|
||||
<!-- Column 2 -->
|
||||
<div class="space-y-5">
|
||||
<section>
|
||||
<h3 class="font-pixel text-[10px] text-t-accent mb-2 uppercase tracking-wider">editor</h3>
|
||||
<table class="shortcut-table"><tbody>
|
||||
<tr><td><kbd>⌃</kbd> <kbd>S</kbd></td><td>save</td></tr>
|
||||
<tr><td><kbd>⌃</kbd> <kbd>⇧</kbd> <kbd>S</kbd></td><td>save + close</td></tr>
|
||||
<tr><td><kbd>⌃</kbd> <kbd>↵</kbd></td><td>save + close</td></tr>
|
||||
<tr><td><kbd>⇥</kbd></td><td>indent / title→body</td></tr>
|
||||
<tr><td><kbd>Alt</kbd> <kbd>↑</kbd> <kbd>↓</kbd></td><td>move line</td></tr>
|
||||
<tr><td><kbd>⇧</kbd> <kbd>Alt</kbd> <kbd>↑</kbd> <kbd>↓</kbd></td><td>duplicate line</td></tr>
|
||||
<tr><td><kbd>↵</kbd></td><td>auto-continue list</td></tr>
|
||||
<tr><td><kbd>[</kbd><kbd>[</kbd></td><td>link autocomplete</td></tr>
|
||||
<tr><td><kbd>#</kbd></td><td>tag autocomplete</td></tr>
|
||||
<tr><td><kbd>?</kbd><em>snippet</em></td><td>snippet expand</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
<section>
|
||||
<h3 class="font-pixel text-[10px] text-t-accent mb-2 uppercase tracking-wider">search</h3>
|
||||
<table class="shortcut-table"><tbody>
|
||||
<tr><td><kbd>↑</kbd> <kbd>↓</kbd></td><td>navigate results</td></tr>
|
||||
<tr><td><kbd>↵</kbd></td><td>open selected result</td></tr>
|
||||
<tr><td><kbd>Esc</kbd></td><td>close search</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
</div>
|
||||
<!-- Column 3 -->
|
||||
<div class="space-y-5">
|
||||
<section>
|
||||
<h3 class="font-pixel text-[10px] text-t-accent mb-2 uppercase tracking-wider">board + detail</h3>
|
||||
<table class="shortcut-table"><tbody>
|
||||
<tr><td><kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd></td><td>focus card</td></tr>
|
||||
<tr><td><kbd>↵</kbd></td><td>focused → preview</td></tr>
|
||||
<tr><td><kbd>↵</kbd> <span class="mouse">in preview</span></td><td>→ editor</td></tr>
|
||||
<tr><td><kbd>←</kbd> <kbd>→</kbd> <span class="mouse">in preview</span></td><td>group prev / next</td></tr>
|
||||
<tr><td><span class="mouse">click</span></td><td>preview / split</td></tr>
|
||||
<tr><td><span class="mouse">dbl-click</span></td><td>editor</td></tr>
|
||||
<tr><td><span class="mouse">right-click</span></td><td>context menu</td></tr>
|
||||
<tr><td><kbd>⌃</kbd> <span class="mouse">click</span></td><td>multi-select</td></tr>
|
||||
<tr><td><span class="mouse">drag → edge</span></td><td>reorder</td></tr>
|
||||
<tr><td><span class="mouse">drag → center</span></td><td>group</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
<section>
|
||||
<h3 class="font-pixel text-[10px] text-t-accent mb-2 uppercase tracking-wider">canvas (ascii editor)</h3>
|
||||
<table class="shortcut-table"><tbody>
|
||||
<tr><td><kbd>B</kbd></td><td>brush</td></tr>
|
||||
<tr><td><kbd>E</kbd></td><td>eraser</td></tr>
|
||||
<tr><td><kbd>L</kbd></td><td>line</td></tr>
|
||||
<tr><td><kbd>R</kbd></td><td>rectangle</td></tr>
|
||||
<tr><td><kbd>O</kbd></td><td>circle</td></tr>
|
||||
<tr><td><kbd>F</kbd></td><td>fill</td></tr>
|
||||
<tr><td><kbd>T</kbd></td><td>text cursor</td></tr>
|
||||
<tr><td><kbd>⌫</kbd></td><td>delete char</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
<section>
|
||||
<h3 class="font-pixel text-[10px] text-t-accent mb-2 uppercase tracking-wider">mail</h3>
|
||||
<table class="shortcut-table"><tbody>
|
||||
<tr><td><kbd>⌫</kbd></td><td>delete selected mails</td></tr>
|
||||
<tr><td><kbd>⇧</kbd> <kbd>⌫</kbd></td><td>delete (skip confirm)</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help -->
|
||||
<div class="overlay fixed inset-0 z-[95] flex justify-center items-start pt-12 pb-12 px-8" style="background:var(--t-overlay)" id="help">
|
||||
<div class="bg-t-surface border border-t-border shadow-2xl w-full max-w-3xl max-h-full overflow-y-auto">
|
||||
@@ -1351,13 +1461,7 @@ if (str_starts_with($route, '/api/')) {
|
||||
|
||||
<section>
|
||||
<h3 class="font-pixel text-xs text-t-accent mb-3 flex items-center gap-2"><i class="ph ph-keyboard text-sm"></i>shortcuts</h3>
|
||||
<table class="w-full text-xs">
|
||||
<tr><td class="py-1 text-t-dim w-40 font-pixel">Ctrl+K</td><td><i class="ph ph-magnifying-glass text-xs"></i> open / close search</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">Ctrl+N</td><td><i class="ph ph-lightning text-xs"></i> quick capture — type title, Enter to save</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">Ctrl+Z</td><td><i class="ph ph-arrow-counter-clockwise text-xs"></i> undo last action</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">F1</td><td><i class="ph ph-question text-xs"></i> open / close help</td></tr>
|
||||
<tr><td class="py-1 text-t-dim font-pixel">Escape</td><td><i class="ph ph-x text-xs"></i> close any overlay</td></tr>
|
||||
</table>
|
||||
<p class="text-xs text-t-muted mb-2">Press <kbd class="px-1 py-0.5 border border-t-border rounded text-[9px] font-pixel">?</kbd> for the full keyboard reference.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
+165
-1
@@ -112,8 +112,16 @@ function closeHelp() { helpOverlay?.classList.remove('open'); }
|
||||
document.getElementById('help-btn')?.addEventListener('click', openHelp);
|
||||
document.getElementById('help-close')?.addEventListener('click', closeHelp);
|
||||
helpOverlay?.addEventListener('click', e => { if (e.target === helpOverlay) closeHelp(); });
|
||||
|
||||
const shortcutsOverlay = document.getElementById('shortcuts');
|
||||
function openShortcuts() { shortcutsOverlay?.classList.add('open'); }
|
||||
function closeShortcuts() { shortcutsOverlay?.classList.remove('open'); }
|
||||
document.getElementById('shortcuts-btn')?.addEventListener('click', openShortcuts);
|
||||
document.getElementById('shortcuts-close')?.addEventListener('click', closeShortcuts);
|
||||
shortcutsOverlay?.addEventListener('click', e => { if (e.target === shortcutsOverlay) closeShortcuts(); });
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'F1') { e.preventDefault(); helpOverlay?.classList.contains('open') ? closeHelp() : openHelp(); }
|
||||
if (e.key === '?' && document.activeElement === document.body) { e.preventDefault(); shortcutsOverlay?.classList.contains('open') ? closeShortcuts() : openShortcuts(); }
|
||||
});
|
||||
|
||||
// ── Export / Import ──
|
||||
@@ -228,12 +236,48 @@ document.addEventListener('keydown', e => {
|
||||
else if (tasks?.classList.contains('open')) closeTasks();
|
||||
else if (document.getElementById('gallery')?.classList.contains('open')) closeGallery();
|
||||
else if (document.getElementById('calendar')?.classList.contains('open')) closeCalendar();
|
||||
else if (shortcutsOverlay?.classList.contains('open')) closeShortcuts();
|
||||
return;
|
||||
}
|
||||
if (e.key === 's' && (e.ctrlKey || e.metaKey) && modal.classList.contains('open')) { e.preventDefault(); e.shiftKey ? saveCard() : saveOnly(); return; }
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && modal.classList.contains('open')) { e.preventDefault(); saveCard(); return; }
|
||||
if (e.key === 'z' && (e.ctrlKey || e.metaKey) && !modal.classList.contains('open') && canUndo()) { e.preventDefault(); undo(); return; }
|
||||
if (e.key === '/' && ![modal, detail, search, tasks].some(el => el?.classList.contains('open')) && document.activeElement === document.body) { e.preventDefault(); openSearch(); }
|
||||
// Bare "n" → open editor for a new card (same as clicking the + button).
|
||||
if (e.key === 'n' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && document.activeElement === document.body) {
|
||||
const blockers = [modal, detail, search, tasks, document.getElementById('gallery'), document.getElementById('settings-overlay'), document.getElementById('help'), document.getElementById('email-overlay'), document.getElementById('calendar'), document.getElementById('shortcuts')];
|
||||
if (blockers.some(el => el?.classList.contains('open'))) return;
|
||||
e.preventDefault();
|
||||
openModal(null);
|
||||
return;
|
||||
}
|
||||
// Bare "a" → bucket all, "f" → bucket favorites. Same guards as the
|
||||
// other bare-key shortcuts.
|
||||
if ((e.key === 'a' || e.key === 'f') && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && document.activeElement === document.body) {
|
||||
const blockers = [modal, detail, search, tasks, document.getElementById('gallery'), document.getElementById('settings-overlay'), document.getElementById('help'), document.getElementById('email-overlay'), document.getElementById('calendar')];
|
||||
if (blockers.some(el => el?.classList.contains('open'))) return;
|
||||
e.preventDefault();
|
||||
activateBucket(e.key === 'a' ? 'all' : 'favorites');
|
||||
return;
|
||||
}
|
||||
// Bare "j" → open today's journal card (or create one if it doesn't
|
||||
// exist). Journal cards have a YYYY-MM-DD title prefix.
|
||||
if (e.key === 'j' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && document.activeElement === document.body) {
|
||||
const blockers = [modal, detail, search, tasks, document.getElementById('gallery'), document.getElementById('settings-overlay'), document.getElementById('help'), document.getElementById('email-overlay'), document.getElementById('calendar')];
|
||||
if (blockers.some(el => el?.classList.contains('open'))) return;
|
||||
e.preventDefault();
|
||||
const today = new Date().toISOString().substring(0, 10);
|
||||
const existing = state.cards.find(c => (c.title || '').startsWith(today));
|
||||
if (existing) {
|
||||
openModal(existing);
|
||||
} else {
|
||||
// Switch to journal view so openModal picks up the journal
|
||||
// context (auto-fills the date title).
|
||||
activateBucket('journal');
|
||||
openModal(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Bare "c" toggles the calendar from the board. Same guards as "/":
|
||||
// no modifier, focus on <body> (so it doesn't intercept text typing),
|
||||
// and no full-screen overlay already in front.
|
||||
@@ -255,6 +299,36 @@ document.addEventListener('keydown', e => {
|
||||
openTagQuickPick();
|
||||
return;
|
||||
}
|
||||
// Tab / Shift+Tab cycles through ALL bucket tiles in bar order
|
||||
// (specials + user buckets + super-buckets). Wraps around at both
|
||||
// ends. Same focus + overlay guards as the other bare-key shortcuts.
|
||||
if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey && document.activeElement === document.body) {
|
||||
const blockers = [modal, detail, search, tasks, document.getElementById('gallery'), document.getElementById('settings-overlay'), document.getElementById('help'), document.getElementById('email-overlay'), document.getElementById('calendar')];
|
||||
if (blockers.some(el => el?.classList.contains('open'))) return;
|
||||
const tiles = [...document.querySelectorAll('#bucket-bar .bucket-tab[data-bucket], #bucket-bar .bucket-tab[data-bucket-super]')];
|
||||
if (!tiles.length) return;
|
||||
// Find which tile is currently active
|
||||
const curIdx = tiles.findIndex(t => {
|
||||
const bid = t.dataset.bucket || t.dataset.bucketSuper;
|
||||
if (bid == state.activeBucket) return true;
|
||||
// Super-bucket: active if any child matches
|
||||
if (t.dataset.bucketSuper) {
|
||||
return state.buckets.some(b => b.parent_id == t.dataset.bucketSuper && b.id == state.activeBucket);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const dir = e.shiftKey ? -1 : 1;
|
||||
const nextIdx = (curIdx + dir + tiles.length) % tiles.length;
|
||||
const next = tiles[nextIdx];
|
||||
if (next) {
|
||||
e.preventDefault();
|
||||
const target = next.dataset.bucketSuper
|
||||
? (next.dataset.superShown || next.dataset.bucketSuper)
|
||||
: next.dataset.bucket;
|
||||
activateBucket(target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Bare "1"–"9" picks the Nth user bucket in the bar — fixed filters
|
||||
// (all/current/favorites/journal/none) are skipped, super-buckets
|
||||
// count as one slot and activate via activateBucket() so the
|
||||
@@ -287,8 +361,93 @@ document.addEventListener('keydown', e => {
|
||||
e.preventDefault();
|
||||
showDetailCard((e.key === 'ArrowRight' ? gc[(idx + 1) % gc.length] : gc[(idx - 1 + gc.length) % gc.length]).id);
|
||||
}
|
||||
|
||||
// Enter in detail view → close preview, then open editor
|
||||
if (e.key === 'Enter' && !e.ctrlKey && !e.metaKey && detail.classList.contains('open') && state.viewingCard) {
|
||||
e.preventDefault();
|
||||
const card = state.viewingCard;
|
||||
closeDetail();
|
||||
openModal(card);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Board arrow-key card navigation ──
|
||||
// Arrow keys select a card on the board (different from Ctrl+click
|
||||
// multi-select — this is a single-focus cursor). Enter on a focused
|
||||
// card opens the detail view. Escape clears the focus. The focus
|
||||
// indicator is a `.card-focused` outline (accent2, distinct from the
|
||||
// multi-select accent outline).
|
||||
const isArrow = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key);
|
||||
if ((isArrow || e.key === 'Enter') && document.activeElement === document.body) {
|
||||
const blockers = [modal, detail, search, tasks, document.getElementById('gallery'), document.getElementById('settings-overlay'), document.getElementById('help'), document.getElementById('email-overlay'), document.getElementById('calendar'), document.getElementById('shortcuts')];
|
||||
if (blockers.some(el => el?.classList.contains('open'))) return;
|
||||
|
||||
const board = document.getElementById('board');
|
||||
if (!board) return;
|
||||
const cards = [...board.querySelectorAll('.board-card[data-id]')];
|
||||
if (!cards.length) return;
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
const focused = board.querySelector('.card-focused');
|
||||
if (focused) {
|
||||
e.preventDefault();
|
||||
window._openDetail(focused.dataset.id);
|
||||
clearCardFocus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
const focused = board.querySelector('.card-focused');
|
||||
let curIdx = focused ? cards.indexOf(focused) : -1;
|
||||
|
||||
if (curIdx < 0) {
|
||||
// No focus yet → select the first card
|
||||
setCardFocus(cards[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute columns per row from the grid layout
|
||||
const firstTop = cards[0].offsetTop;
|
||||
let cols = cards.length;
|
||||
for (let i = 1; i < cards.length; i++) {
|
||||
if (cards[i].offsetTop !== firstTop) { cols = i; break; }
|
||||
}
|
||||
|
||||
let next = curIdx;
|
||||
if (e.key === 'ArrowRight') next = Math.min(curIdx + 1, cards.length - 1);
|
||||
else if (e.key === 'ArrowLeft') next = Math.max(curIdx - 1, 0);
|
||||
else if (e.key === 'ArrowDown') next = Math.min(curIdx + cols, cards.length - 1);
|
||||
else if (e.key === 'ArrowUp') next = Math.max(curIdx - cols, 0);
|
||||
|
||||
if (next !== curIdx) setCardFocus(cards[next]);
|
||||
}
|
||||
});
|
||||
|
||||
let focusedCardId = null;
|
||||
function setCardFocus(el) {
|
||||
clearCardFocus();
|
||||
if (!el) return;
|
||||
el.classList.add('card-focused');
|
||||
focusedCardId = el.dataset.id;
|
||||
el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
function clearCardFocus() {
|
||||
document.querySelectorAll('.card-focused').forEach(c => c.classList.remove('card-focused'));
|
||||
focusedCardId = null;
|
||||
}
|
||||
// Clear focus when clicking anywhere on the board (a card click opens
|
||||
// detail/editor and focus is no longer meaningful), or when any overlay
|
||||
// opens. Also re-apply after a board re-render if the focused card is
|
||||
// still in the DOM.
|
||||
document.addEventListener('click', () => { if (focusedCardId) clearCardFocus(); });
|
||||
window._restoreCardFocus = function () {
|
||||
if (!focusedCardId) return;
|
||||
const el = document.querySelector(`#board .board-card[data-id="${focusedCardId}"]`);
|
||||
if (el) el.classList.add('card-focused');
|
||||
else clearCardFocus();
|
||||
};
|
||||
|
||||
// ── Quick Capture ──
|
||||
|
||||
document.getElementById('quick-capture-input')?.addEventListener('keydown', async e => {
|
||||
@@ -318,7 +477,12 @@ document.getElementById('quick-capture-input')?.addEventListener('keydown', asyn
|
||||
if (!cardTitle) return;
|
||||
if (!body.includes('#inbox')) body = (body ? body + '\n\n' : '') + '#inbox';
|
||||
|
||||
await api('api/cards', { method: 'POST', body: { title: cardTitle, body, color: '#38bdf8', position: -1 } });
|
||||
// Place the card in the currently active bucket (if it's a real
|
||||
// user bucket). Special filters (all/current/favorites/journal/none)
|
||||
// don't map to a bucket_id — cards captured there stay unsorted.
|
||||
const VIRTUAL = ['all', 'current', 'favorites', 'journal', 'none'];
|
||||
const bucketId = VIRTUAL.includes(state.activeBucket) ? null : state.activeBucket;
|
||||
await api('api/cards', { method: 'POST', body: { title: cardTitle, body, color: '#38bdf8', position: -1, bucket_id: bucketId } });
|
||||
|
||||
e.target.value = '';
|
||||
document.getElementById('quick-capture').classList.add('hidden');
|
||||
|
||||
@@ -714,6 +714,9 @@ export function renderBoard() {
|
||||
});
|
||||
}
|
||||
|
||||
// Restore arrow-key card focus if the focused card is still rendered
|
||||
window._restoreCardFocus?.();
|
||||
|
||||
// Restore selection state
|
||||
selectedIds.forEach(id => {
|
||||
const el = board.querySelector(`[data-id="${id}"]`);
|
||||
|
||||
@@ -171,7 +171,7 @@ export function csrf() {
|
||||
}
|
||||
|
||||
export async function closeAll() {
|
||||
for (const id of ['settings-overlay', 'help', 'search', 'calendar', 'gallery', 'detail', 'modal', 'tasks', 'dialog']) {
|
||||
for (const id of ['settings-overlay', 'help', 'shortcuts', 'search', 'calendar', 'gallery', 'detail', 'modal', 'tasks', 'dialog']) {
|
||||
const el = document.getElementById(id);
|
||||
if (el?.classList.contains('open')) el.classList.remove('open');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user