feat(modal): keyboard-friendly tagging — fandom dialogs, Tab-accept, jump hotkey, cheatsheet
Operator-requested modal/tagging keyboard improvements: - A2/A3: fandom dialogs autofocus their autocomplete on open; in the character- creation FandomPicker, picking a fandom (keyboard Enter or click) confirms in one step. FandomSetDialog stays autofocus-only (its Save can trigger a merge). - B5: Tab accepts the highlighted autocomplete row (standard convention). - C9: T or / jumps focus to the tag input from anywhere in the modal. - C8: ? toggles a keyboard cheatsheet (corner hint advertises it; Esc closes the cheatsheet first, then the viewer). Builds on the same-batch regression fixes (kebab #711, ESC-after-accept #700, autocomplete scroll-into-view). B6 (keep focus after add) is covered — the input retains focus after adding a tag, and Esc now works after accepting a suggestion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,25 @@
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</button>
|
||||
|
||||
<!-- C8: keyboard cheatsheet. '?' toggles; the corner hint advertises it. -->
|
||||
<button
|
||||
class="fc-viewer__help-hint" aria-label="Keyboard shortcuts (press ?)"
|
||||
@click="showHelp = !showHelp"
|
||||
>?</button>
|
||||
<div v-if="showHelp" class="fc-viewer__help" @click.self="showHelp = false">
|
||||
<div class="fc-viewer__help-card" role="dialog" aria-label="Keyboard shortcuts">
|
||||
<h3>Keyboard shortcuts</h3>
|
||||
<dl>
|
||||
<div><dt>← / →</dt><dd>Previous / next image</dd></div>
|
||||
<div><dt>T or /</dt><dd>Jump to the tag input</dd></div>
|
||||
<div><dt>↑ / ↓</dt><dd>Move through tag suggestions</dd></div>
|
||||
<div><dt>Enter / Tab</dt><dd>Accept the highlighted tag</dd></div>
|
||||
<div><dt>Esc</dt><dd>Close a dialog, then the viewer</dd></div>
|
||||
<div><dt>?</dt><dd>Toggle this help</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-chip
|
||||
v-if="integrityBadge"
|
||||
:color="integrityBadge.color"
|
||||
@@ -63,7 +82,7 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useModalStore } from '../../stores/modal.js'
|
||||
import { arrowNavAllowed } from '../../utils/textEntry.js'
|
||||
import { arrowNavAllowed, isTextEntry } from '../../utils/textEntry.js'
|
||||
import ImageCanvas from './ImageCanvas.vue'
|
||||
import VideoCanvas from './VideoCanvas.vue'
|
||||
import TagPanel from './TagPanel.vue'
|
||||
@@ -74,6 +93,7 @@ const emit = defineEmits(['close'])
|
||||
|
||||
const modal = useModalStore()
|
||||
const rootEl = ref(null)
|
||||
const showHelp = ref(false)
|
||||
|
||||
const isVideo = computed(() =>
|
||||
modal.current?.mime && modal.current.mime.startsWith('video/')
|
||||
@@ -96,6 +116,12 @@ let prevBodyOverflow = null
|
||||
// that. Filtered via isTextEntry so tag/comment inputs still get their
|
||||
// own keystrokes.
|
||||
function onKeyDown(ev) {
|
||||
if (ev.key === 'Escape' && showHelp.value) {
|
||||
// Close the cheatsheet first; a second Esc closes the modal.
|
||||
ev.preventDefault()
|
||||
showHelp.value = false
|
||||
return
|
||||
}
|
||||
if (ev.key === 'Escape') {
|
||||
// Escape closes the modal even from inside a text input — the universal
|
||||
// "get me out of here" expectation; the autofocused tag field would
|
||||
@@ -123,6 +149,14 @@ function onKeyDown(ev) {
|
||||
if (!arrowNavAllowed(ev.target)) return
|
||||
ev.preventDefault()
|
||||
modal.goNext()
|
||||
} else if ((ev.key === '/' || ev.key === 't') && !isTextEntry(ev.target)) {
|
||||
// C9 (2026-06-07): jump focus to the tag input from anywhere in the modal.
|
||||
const input = document.querySelector('.fc-tag-autocomplete input')
|
||||
if (input) { ev.preventDefault(); input.focus() }
|
||||
} else if (ev.key === '?' && !isTextEntry(ev.target)) {
|
||||
// C8: toggle the keyboard cheatsheet.
|
||||
ev.preventDefault()
|
||||
showHelp.value = !showHelp.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +212,43 @@ function nextFrame() {
|
||||
}
|
||||
.fc-viewer__nav:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
.fc-viewer__close { top: 16px; right: 16px; transform: none; }
|
||||
.fc-viewer__help-hint {
|
||||
position: absolute; top: 16px; right: 64px; z-index: 2;
|
||||
width: 32px; height: 32px; border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
font-weight: 700; cursor: pointer; opacity: 0.6;
|
||||
}
|
||||
.fc-viewer__help-hint:hover { opacity: 1; }
|
||||
.fc-viewer__help {
|
||||
position: absolute; inset: 0; z-index: 4;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.fc-viewer__help-card {
|
||||
background: rgb(var(--v-theme-surface));
|
||||
border: 1px solid rgb(var(--v-theme-surface-light));
|
||||
border-radius: 10px; padding: 20px 24px; min-width: 320px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.fc-viewer__help-card h3 {
|
||||
font-family: 'Fraunces', Georgia, serif;
|
||||
font-size: 18px; margin-bottom: 12px;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
.fc-viewer__help-card dl { display: flex; flex-direction: column; gap: 8px; }
|
||||
.fc-viewer__help-card dl > div {
|
||||
display: flex; gap: 16px; align-items: baseline;
|
||||
}
|
||||
.fc-viewer__help-card dt {
|
||||
flex: 0 0 96px; text-align: right;
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 12px;
|
||||
color: rgb(var(--v-theme-accent));
|
||||
}
|
||||
.fc-viewer__help-card dd {
|
||||
flex: 1; font-size: 14px;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
.fc-viewer__integrity {
|
||||
position: absolute; top: 72px; right: 16px; z-index: 3;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user