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:
parent
23b3d07071
commit
7d81250ebd
26
build.sh
26
build.sh
|
|
@ -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
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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')}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue