fix: rename node not found, A11y warnings cleanup

- fix renameId cleared before RenameNode API call (submitRename)
- add role, tabindex, keydown handlers to all interactive divs
- associate labels with inputs (wrap in label + .label-text)
- remove autofocus, unused CSS selectors, old root build.sh
- update frontend-dist assets
This commit is contained in:
mirivlad 2026-06-03 08:55:38 +08:00
parent 23b3d07071
commit 7d81250ebd
9 changed files with 105 additions and 76 deletions

View File

@ -1,26 +0,0 @@
#!/bin/bash
set -e
# Load NVM for Node.js
export NVM_DIR="${NVM_DIR:-$HOME/.config/nvm}"
if [ -s "$NVM_DIR/nvm.sh" ]; then
. "$NVM_DIR/nvm.sh"
elif [ -s "$HOME/.nvm/nvm.sh" ]; then
. "$HOME/.nvm/nvm.sh"
fi
BUILD_DIR="build"
mkdir -p "$BUILD_DIR"
echo "==> Building frontend..."
cd frontend && npm run build && cd ..
cp -r frontend/dist/* cmd/verstak-gui/frontend-dist/
echo "==> Building GUI binary..."
go build -tags "webkit2_41 desktop production" -ldflags="-s -w" -o "$BUILD_DIR/verstak-gui-linux-amd64" ./cmd/verstak-gui/
echo "==> Building server binary..."
go build -ldflags="-s -w" -o "$BUILD_DIR/verstak-server-linux-amd64" ./cmd/verstak-server/
echo "==> Done. Binaries in $BUILD_DIR/:"
ls -lh "$BUILD_DIR/"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -16,8 +16,8 @@
background: #13131f; background: #13131f;
} }
</style> </style>
<script type="module" crossorigin src="/assets/main-BH7waEiY.js"></script> <script type="module" crossorigin src="/assets/main-CWWXp5bW.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-BzI_Zj56.css"> <link rel="stylesheet" crossorigin href="/assets/main-BBKDbfa7.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -89,6 +89,7 @@
let clipboard = { items: [], mode: 'copy' } let clipboard = { items: [], mode: 'copy' }
let selectedIds = [] let selectedIds = []
let dragIds = [] let dragIds = []
let dropRootValid = false
let showConfirm = false let showConfirm = false
let confirmTitle = '' let confirmTitle = ''
@ -565,11 +566,17 @@
return return
} }
showRename = false showRename = false
const id = renameId
renameId = '' renameId = ''
try { try {
await wailsCall('RenameNode', renameId, name) await wailsCall('RenameNode', id, name)
const parentId = currentFolderId || selectedNode.id if (selectedNode && selectedNode.id === id) {
await loadFolder(parentId) selectedNode = { ...selectedNode, title: name }
}
await reloadTreePreservingExpanded()
if (currentFolderId) {
await loadFolder(currentFolderId)
}
} catch (e) { } catch (e) {
error = String(e) error = String(e)
} }
@ -729,11 +736,11 @@
function handleDragOverRoot(e) { function handleDragOverRoot(e) {
e.preventDefault() e.preventDefault()
e.dataTransfer.dropEffect = 'move' e.dataTransfer.dropEffect = 'move'
e.currentTarget.classList.add('drop-valid') dropRootValid = true
} }
function handleDragLeaveRoot(e) { function handleDragLeaveRoot(e) {
e.currentTarget.classList.remove('drop-valid') dropRootValid = false
} }
// ===== Node operations from context menu ===== // ===== Node operations from context menu =====
@ -1172,6 +1179,15 @@
} }
syncLoading = false syncLoading = false
} }
function onKeyActivate(fn) {
return (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
fn()
}
}
}
</script> </script>
<div class="app"> <div class="app">
@ -1198,6 +1214,8 @@
</div> </div>
{#if workspaceTree.length > 0} {#if workspaceTree.length > 0}
<div class="workspace-tree-area" <div class="workspace-tree-area"
class:drop-valid={dropRootValid}
role="region" aria-label={t('nav.workspace')}
on:dragover|preventDefault={handleDragOverRoot} on:dragover|preventDefault={handleDragOverRoot}
on:dragleave={handleDragLeaveRoot} on:dragleave={handleDragLeaveRoot}
on:drop={handleDropRoot}> on:drop={handleDropRoot}>
@ -1252,7 +1270,7 @@
</header> </header>
{#if error} {#if error}
<div class="error-banner" on:click={() => error = ''}> <div class="error-banner" role="button" tabindex="0" on:click={() => error = ''} on:keydown={onKeyActivate(() => error = '')}>
{error} {error}
<button class="dismiss-btn" on:click|stopPropagation={() => error = ''} aria-label="Dismiss"> <button class="dismiss-btn" on:click|stopPropagation={() => error = ''} aria-label="Dismiss">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
@ -1315,7 +1333,7 @@
<div class="recent-section"> <div class="recent-section">
<h3>{t('overview.recentNotes')}</h3> <h3>{t('overview.recentNotes')}</h3>
{#each notes.slice(0, 5) as note} {#each notes.slice(0, 5) as note}
<div class="recent-note" on:click={() => openNote(note)}> <div class="recent-note" role="button" tabindex="0" on:click={() => openNote(note)} on:keydown={onKeyActivate(() => openNote(note))}>
<span>{note.title}</span><span class="recent-date">{formatDate(note.createdAt)}</span> <span>{note.title}</span><span class="recent-date">{formatDate(note.createdAt)}</span>
</div> </div>
{/each} {/each}
@ -1351,7 +1369,7 @@
{:else} {:else}
<div class="notes-list"> <div class="notes-list">
{#each notes as note} {#each notes as note}
<div class="note-card" on:click={() => openNote(note)}> <div class="note-card" role="button" tabindex="0" on:click={() => openNote(note)} on:keydown={onKeyActivate(() => openNote(note))}>
<div class="note-card-title">{note.title}</div> <div class="note-card-title">{note.title}</div>
<div class="note-card-date">{formatDate(note.createdAt)}</div> <div class="note-card-date">{formatDate(note.createdAt)}</div>
</div> </div>
@ -1620,15 +1638,15 @@
{/if} {/if}
{#if showCreateNode} {#if showCreateNode}
<div class="modal-overlay" on:click|self={cancelCreateNode}> <div class="modal-overlay" role="button" tabindex="0" on:click|self={cancelCreateNode} on:keydown={onKeyActivate(cancelCreateNode)}>
<div class="modal modal-create"> <div class="modal modal-create">
<h3>{t('nav.createNode')}</h3> <h3>{t('nav.createNode')}</h3>
{#if createInNode} {#if createInNode}
<div class="create-context">{t('nav.createInside')} «{createInNode.title}»</div> <div class="create-context">{t('nav.createInside')} «{createInNode.title}»</div>
{/if} {/if}
<div class="form-group"> <div class="form-group">
<label>{t('template.select')}</label> <span class="form-label">{t('template.select')}</span>
<div class="template-cards"> <div class="template-cards" role="group" aria-label={t('template.select')}>
<button class="template-card" class:selected={createWithTemplate === null} <button class="template-card" class:selected={createWithTemplate === null}
on:click={() => createWithTemplate = null}> on:click={() => createWithTemplate = null}>
<TemplateIcon kind="folder" size={24} /> <TemplateIcon kind="folder" size={24} />
@ -1650,9 +1668,10 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{t('common.name')}</label> <label><span class="label-text">{t('common.name')}</span>
<input type="text" placeholder={t('case.namePlaceholder')} bind:value={newNodeTitle} <input type="text" placeholder={t('case.namePlaceholder')} bind:value={newNodeTitle}
on:keydown={(e) => e.key === 'Enter' && submitCreateNode()} autofocus /> on:keydown={(e) => e.key === 'Enter' && submitCreateNode()} />
</label>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button class="btn btn-primary" on:click={submitCreateNode} <button class="btn btn-primary" on:click={submitCreateNode}
@ -1664,7 +1683,7 @@
{/if} {/if}
{#if contextMenu.visible} {#if contextMenu.visible}
<div class="context-menu-backdrop" on:click={closeContextMenu} on:contextmenu|preventDefault={closeContextMenu}> <div class="context-menu-backdrop" role="button" tabindex="0" on:click={closeContextMenu} on:contextmenu|preventDefault={closeContextMenu} on:keydown={onKeyActivate(closeContextMenu)}>
<div class="context-menu" style="left: {contextMenu.x}px; top: {contextMenu.y}px"> <div class="context-menu" style="left: {contextMenu.x}px; top: {contextMenu.y}px">
{#if contextMenu.node && ['folder','project','client','document','recipe'].includes(contextMenu.node.type)} {#if contextMenu.node && ['folder','project','client','document','recipe'].includes(contextMenu.node.type)}
<div class="context-menu-section">{t('common.create')}</div> <div class="context-menu-section">{t('common.create')}</div>
@ -1696,27 +1715,30 @@
{/if} {/if}
{#if showCreateAction} {#if showCreateAction}
<div class="modal-overlay" on:click|self={cancelCreateAction}> <div class="modal-overlay" role="button" tabindex="0" on:click|self={cancelCreateAction} on:keydown={onKeyActivate(cancelCreateAction)}>
<div class="modal"> <div class="modal">
<h3>{t('action.newAction')}</h3> <h3>{t('action.newAction')}</h3>
<div class="form-group"> <div class="form-group">
<label>{t('common.name')}</label> <label><span class="label-text">{t('common.name')}</span>
<input type="text" placeholder={t('action.namePlaceholder')} bind:value={newActionTitle} <input type="text" placeholder={t('action.namePlaceholder')} bind:value={newActionTitle}
on:keydown={(e) => e.key === 'Enter' && submitCreateAction()} autofocus /> on:keydown={(e) => e.key === 'Enter' && submitCreateAction()} />
</label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{t('common.type')}</label> <label><span class="label-text">{t('common.type')}</span>
<select bind:value={newActionKind}> <select bind:value={newActionKind}>
{#each actionKinds as k} {#each actionKinds as k}
<option value={k.id}>{k.label}</option> <option value={k.id}>{k.label}</option>
{/each} {/each}
</select> </select>
</label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{newActionKind === 'open_url' ? t('action.dataUrl') : newActionKind === 'open_folder' || newActionKind === 'open_file' ? t('action.dataPath') : t('action.dataCommand')}</label> <label><span class="label-text">{newActionKind === 'open_url' ? t('action.dataUrl') : newActionKind === 'open_folder' || newActionKind === 'open_file' ? t('action.dataPath') : t('action.dataCommand')}</span>
<input type="text" placeholder={newActionKind === 'open_url' ? t('action.urlPlaceholder') : newActionKind === 'open_folder' || newActionKind === 'open_file' ? t('action.pathPlaceholder') : t('action.commandPlaceholder')} <input type="text" placeholder={newActionKind === 'open_url' ? t('action.urlPlaceholder') : newActionKind === 'open_folder' || newActionKind === 'open_file' ? t('action.pathPlaceholder') : t('action.commandPlaceholder')}
bind:value={newActionData} bind:value={newActionData}
on:keydown={(e) => e.key === 'Enter' && submitCreateAction()} /> on:keydown={(e) => e.key === 'Enter' && submitCreateAction()} />
</label>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button class="btn btn-primary" on:click={submitCreateAction}>{t('common.create')}</button> <button class="btn btn-primary" on:click={submitCreateAction}>{t('common.create')}</button>
@ -1727,7 +1749,7 @@
{/if} {/if}
{#if showImportDialog && importSummary} {#if showImportDialog && importSummary}
<div class="modal-overlay" on:click|self={cancelImport}> <div class="modal-overlay" role="button" tabindex="0" on:click|self={cancelImport} on:keydown={onKeyActivate(cancelImport)}>
<div class="modal"> <div class="modal">
<h3>{t('file.importTitle')} «{selectedNode ? selectedNode.title : ''}»</h3> <h3>{t('file.importTitle')} «{selectedNode ? selectedNode.title : ''}»</h3>
<div class="import-summary"> <div class="import-summary">
@ -1751,13 +1773,14 @@
{/if} {/if}
{#if showRename} {#if showRename}
<div class="modal-overlay" on:click|self={cancelRename}> <div class="modal-overlay" role="button" tabindex="0" on:click|self={cancelRename} on:keydown={onKeyActivate(cancelRename)}>
<div class="modal"> <div class="modal">
<h3>{t('rename.title')}</h3> <h3>{t('rename.title')}</h3>
<div class="form-group"> <div class="form-group">
<label>{t('common.newName')}</label> <label><span class="label-text">{t('common.newName')}</span>
<input type="text" bind:value={renameValue} <input type="text" bind:value={renameValue}
on:keydown={onRenameKeydown} /> on:keydown={onRenameKeydown} />
</label>
</div> </div>
{#if renameError} {#if renameError}
<div class="rename-error">{renameError}</div> <div class="rename-error">{renameError}</div>
@ -1782,7 +1805,7 @@
{/if} {/if}
{#if showSettings} {#if showSettings}
<div class="modal-overlay" on:click|self={closeSettings}> <div class="modal-overlay" role="button" tabindex="0" on:click|self={closeSettings} on:keydown={onKeyActivate(closeSettings)}>
<div class="modal modal-sync"> <div class="modal modal-sync">
<h3>{t('sync.settings')}</h3> <h3>{t('sync.settings')}</h3>
{#if syncStatus} {#if syncStatus}
@ -1824,16 +1847,19 @@
</div> </div>
{:else} {:else}
<div class="form-group"> <div class="form-group">
<label>{t('sync.serverUrl')}</label> <label><span class="label-text">{t('sync.serverUrl')}</span>
<input type="text" placeholder={t('sync.serverUrlPlaceholder')} bind:value={syncServerUrl} /> <input type="text" placeholder={t('sync.serverUrlPlaceholder')} bind:value={syncServerUrl} />
</label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{t('sync.username')}</label> <label><span class="label-text">{t('sync.username')}</span>
<input type="text" placeholder={t('sync.usernamePlaceholder')} bind:value={syncUsername} /> <input type="text" placeholder={t('sync.usernamePlaceholder')} bind:value={syncUsername} />
</label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{t('sync.password')}</label> <label><span class="label-text">{t('sync.password')}</span>
<input type="password" placeholder={t('sync.passwordPlaceholder')} bind:value={syncPassword} /> <input type="password" placeholder={t('sync.passwordPlaceholder')} bind:value={syncPassword} />
</label>
</div> </div>
<div class="modal-actions" style="margin-top:12px"> <div class="modal-actions" style="margin-top:12px">
<button class="btn" on:click={testConnection} disabled={syncLoading || !syncServerUrl}>{t('sync.test')}</button> <button class="btn" on:click={testConnection} disabled={syncLoading || !syncServerUrl}>{t('sync.test')}</button>
@ -1843,8 +1869,9 @@
<div style="margin-top:16px;padding-top:16px;border-top:1px solid #2a2a3c"> <div style="margin-top:16px;padding-top:16px;border-top:1px solid #2a2a3c">
<div class="form-group"> <div class="form-group">
<label>{t('sync.autoSync')}</label> <label><span class="label-text">{t('sync.autoSync')}</span>
<input type="number" placeholder="0" bind:value={syncInterval} min="0" /> <input type="number" placeholder="0" bind:value={syncInterval} min="0" />
</label>
</div> </div>
<button class="btn" on:click={saveSyncInterval} disabled={syncLoading}>{t('sync.saveInterval')}</button> <button class="btn" on:click={saveSyncInterval} disabled={syncLoading}>{t('sync.saveInterval')}</button>
</div> </div>
@ -1884,16 +1911,6 @@
.nav-add-btn { background: none; border: none; color: #666; cursor: pointer; font-size: 16px; padding: 0 4px; font-family: inherit; line-height: 1; } .nav-add-btn { background: none; border: none; color: #666; cursor: pointer; font-size: 16px; padding: 0 4px; font-family: inherit; line-height: 1; }
.nav-add-btn:hover { color: #ccc; } .nav-add-btn:hover { color: #ccc; }
/* Tree items in sidebar */
.tree-item { display: flex; align-items: center; padding: 4px 8px 4px 0; cursor: default; font-size: 13px; color: #ccc; }
.tree-item:hover { background: #222233; }
.tree-item.selected { background: #2a2a4a; color: #fff; font-weight: 500; }
.tree-toggle { background: none; border: none; color: #666; cursor: pointer; padding: 2px 4px; font-size: 10px; width: 20px; text-align: center; flex-shrink: 0; font-family: inherit; line-height: 1; }
.tree-toggle:hover { color: #888; }
.tree-arrow { display: inline-block; }
.tree-spacer { display: inline-block; width: 12px; }
.tree-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 2px 4px; cursor: pointer; }
/* Context menu */ /* Context menu */
.context-menu-backdrop { position: fixed; inset: 0; z-index: 200; } .context-menu-backdrop { position: fixed; inset: 0; z-index: 200; }
.context-menu { position: fixed; background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; padding: 4px; min-width: 180px; box-shadow: 0 8px 24px rgba(0,0,0,0.4); } .context-menu { position: fixed; background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; padding: 4px; min-width: 180px; box-shadow: 0 8px 24px rgba(0,0,0,0.4); }
@ -1999,7 +2016,6 @@
.empty-state .empty-icon { margin-bottom: 12px; color: #444; } .empty-state .empty-icon { margin-bottom: 12px; color: #444; }
.empty-state .hint { font-size: 12px; color: #555; margin-top: 6px; } .empty-state .hint { font-size: 12px; color: #555; margin-top: 6px; }
.empty-state .empty-actions { display: flex; gap: 8px; justify-content: center; margin-top: 16px; } .empty-state .empty-actions { display: flex; gap: 8px; justify-content: center; margin-top: 16px; }
.empty-note { font-size: 12px; color: #444; margin-top: 16px; }
/* Welcome */ /* Welcome */
.welcome { padding: 48px 24px; text-align: center; } .welcome { padding: 48px 24px; text-align: center; }
@ -2007,16 +2023,13 @@
.welcome p { color: #666; font-size: 14px; } .welcome p { color: #666; font-size: 14px; }
.error-text { color: #ff8888; } .error-text { color: #ff8888; }
/* FAB */
.fab { position: fixed; bottom: 24px; right: 24px; width: 56px; height: 56px; border-radius: 50%; background: #6366f1; color: #fff; font-size: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4); }
.fab:hover { background: #4f46e5; }
/* Modal */ /* Modal */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 100; } .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 100; }
.modal { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 12px; padding: 24px; width: 400px; max-width: 90vw; } .modal { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 12px; padding: 24px; width: 400px; max-width: 90vw; }
.modal h3 { font-size: 18px; margin-bottom: 16px; } .modal h3 { font-size: 18px; margin-bottom: 16px; }
.form-group { margin-bottom: 12px; } .form-group { margin-bottom: 12px; }
.form-group label { display: block; font-size: 12px; color: #666; margin-bottom: 4px; } .form-group label { display: block; }
.form-group .label-text, .form-group .form-label { display: block; font-size: 12px; color: #666; margin-bottom: 4px; }
.form-group input, .form-group select { width: 100%; padding: 8px 12px; border: 1px solid #2a2a3c; background: #13131f; color: #e4e4ef; border-radius: 4px; font-size: 14px; font-family: inherit; } .form-group input, .form-group select { width: 100%; padding: 8px 12px; border: 1px solid #2a2a3c; background: #13131f; color: #e4e4ef; border-radius: 4px; font-size: 14px; font-family: inherit; }
.form-group select { appearance: none; -webkit-appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M2 4l4 4 4-4'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; padding-right: 32px; } .form-group select { appearance: none; -webkit-appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M2 4l4 4 4-4'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; padding-right: 32px; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: #6366f1; } .form-group input:focus, .form-group select:focus { outline: none; border-color: #6366f1; }

View File

@ -114,6 +114,13 @@
menuY = Math.min(e.clientY, window.innerHeight - 320) menuY = Math.min(e.clientY, window.innerHeight - 320)
menuOpen = true menuOpen = true
} }
function handleRowKeydown(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleClick(e)
}
}
</script> </script>
<svelte:window on:click={closeMenu}/> <svelte:window on:click={closeMenu}/>
@ -124,6 +131,7 @@
tabindex="0" tabindex="0"
draggable="true" draggable="true"
on:click={handleClick} on:click={handleClick}
on:keydown={handleRowKeydown}
on:contextmenu={oncontextmenu} on:contextmenu={oncontextmenu}
on:dragstart={handleDragStart} on:dragstart={handleDragStart}
on:dragover={handleDragOver} on:dragover={handleDragOver}
@ -194,7 +202,7 @@
{#if menuOpen} {#if menuOpen}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="menu-backdrop" on:click|stopPropagation={closeMenu} role="presentation"></div> <div class="menu-backdrop" on:click|stopPropagation={closeMenu} role="presentation"></div>
<div class="menu" style="left: {menuX}px; top: {menuY}px; position: fixed;" on:click|stopPropagation role="menu"> <div class="menu" style="left: {menuX}px; top: {menuY}px; position: fixed;" on:click|stopPropagation on:keydown={(e) => { if (e.key === 'Escape') { e.stopPropagation(); closeMenu() } }} role="menu" tabindex="-1">
<button class="menu-item" on:click={handleOpen} role="menuitem"> <button class="menu-item" on:click={handleOpen} role="menuitem">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
{t('common.open')} {t('common.open')}

View File

@ -201,11 +201,33 @@
if (shouldShowToggle(node) && onToggle) onToggle(node.id) if (shouldShowToggle(node) && onToggle) onToggle(node.id)
} }
function handleRowKeydown(e, node) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
if (onSelect) onSelect(node)
}
}
function handleIconClick(e, node) { function handleIconClick(e, node) {
e.stopPropagation() e.stopPropagation()
if (shouldShowToggle(node) && onToggle) onToggle(node.id) if (shouldShowToggle(node) && onToggle) onToggle(node.id)
} }
function handleIconKeydown(e, node) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
e.stopPropagation()
if (shouldShowToggle(node) && onToggle) onToggle(node.id)
}
}
function handleLabelKeydown(e, node) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
if (onSelect) onSelect(node)
}
}
function computeDropInfo(allNodes, draggedId, pMap) { function computeDropInfo(allNodes, draggedId, pMap) {
const info = {} const info = {}
function walk(list) { function walk(list) {
@ -232,11 +254,14 @@
class:drop-invalid={dragOverNodeId === node.id && !dropAllowed[node.id]} class:drop-invalid={dragOverNodeId === node.id && !dropAllowed[node.id]}
style="padding-left: {level * 16 + 4}px" style="padding-left: {level * 16 + 4}px"
draggable="true" draggable="true"
role="button"
tabindex="0"
on:dragstart={(e) => handleDragStart(e, node)} on:dragstart={(e) => handleDragStart(e, node)}
on:dragover={(e) => handleDragOver(e, node)} on:dragover={(e) => handleDragOver(e, node)}
on:dragleave={(e) => handleDragLeave(e, node)} on:dragleave={(e) => handleDragLeave(e, node)}
on:drop={(e) => handleDrop(e, node)} on:drop={(e) => handleDrop(e, node)}
on:click={(e) => handleRowClick(e, node)} on:click={(e) => handleRowClick(e, node)}
on:keydown={(e) => handleRowKeydown(e, node)}
on:dblclick={(e) => handleRowDblClick(e, node)} on:dblclick={(e) => handleRowDblClick(e, node)}
on:contextmenu|preventDefault={(e) => onContextMenu && onContextMenu(e, node)}> on:contextmenu|preventDefault={(e) => onContextMenu && onContextMenu(e, node)}>
{#if shouldShowToggle(node)} {#if shouldShowToggle(node)}
@ -246,10 +271,15 @@
{:else} {:else}
<span class="tree-toggle-placeholder"></span> <span class="tree-toggle-placeholder"></span>
{/if} {/if}
<span class="tree-icon" on:click={(e) => handleIconClick(e, node)} on:dblclick|stopPropagation> <span class="tree-icon" role="button" tabindex="-1"
on:click={(e) => handleIconClick(e, node)}
on:keydown={(e) => handleIconKeydown(e, node)}
on:dblclick|stopPropagation>
<TemplateIcon kind={iconKind(node)} size={16} /> <TemplateIcon kind={iconKind(node)} size={16} />
</span> </span>
<span class="tree-label" on:click|stopPropagation={() => onSelect && onSelect(node)}> <span class="tree-label" role="button" tabindex="-1"
on:click|stopPropagation={() => onSelect && onSelect(node)}
on:keydown={(e) => handleLabelKeydown(e, node)}>
{node.title} {node.title}
</span> </span>
</div> </div>

View File

@ -10,7 +10,7 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
</script> </script>
<div class="overlay" on:click|self={() => dispatch('cancel')} role="dialog" aria-modal="true" aria-label={title}> <div class="overlay" on:click|self={() => dispatch('cancel')} on:keydown={(e) => { if (e.key === 'Escape') { e.preventDefault(); dispatch('cancel') } }} role="presentation">
<div class="modal"> <div class="modal">
<h3>{title}</h3> <h3>{title}</h3>
<p class="message">{message}</p> <p class="message">{message}</p>

View File

@ -35,7 +35,7 @@
}) })
</script> </script>
<div class="overlay" on:click|self={() => dispatch('close')} role="dialog" aria-modal="true" aria-label={`Preview: ${item.name}`}> <div class="overlay" on:click|self={() => dispatch('close')} on:keydown={(e) => { if (e.key === 'Escape') { e.preventDefault(); dispatch('close') } }} role="presentation">
<div class="modal"> <div class="modal">
<header class="preview-header"> <header class="preview-header">
<div class="preview-title"> <div class="preview-title">