refactor(ext/vault): extract vault-shell.ts + introduce VaultController ctx (Plan C Phase 4)
Introduces vault-context.ts (VaultView/HashRoute/VaultState types, the VaultController contract, and the pure helpers escapeHtml/typeIcon/typeLabel/ getFilteredEntries). Extracts the shell concerns — render entry, lock screen, 3-column shell scaffolding, type picker panel, color-scheme apply, and the session_expired listener — into vault-shell.ts. vault.ts now assembles the ctx object and delegates shell rendering through it. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import * as path from 'path';
|
|||||||
|
|
||||||
describe('vault sidebar glyphs', () => {
|
describe('vault sidebar glyphs', () => {
|
||||||
const vaultSrc = fs.readFileSync(
|
const vaultSrc = fs.readFileSync(
|
||||||
path.resolve(__dirname, '../vault.ts'),
|
path.resolve(__dirname, '../vault-shell.ts'),
|
||||||
'utf-8',
|
'utf-8',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
122
extension/src/vault/vault-context.ts
Normal file
122
extension/src/vault/vault-context.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// Shared contract for the vault-tab modules. vault.ts owns the state
|
||||||
|
// singleton and assembles the VaultController; each vault-* module receives
|
||||||
|
// it as `ctx`. This module sits at the bottom of the dependency graph —
|
||||||
|
// it imports only from shared/, never from vault.ts or its sibling modules.
|
||||||
|
|
||||||
|
import type { Request, Response } from '../shared/messages';
|
||||||
|
import type {
|
||||||
|
ItemId, ItemType, ManifestEntry, Item, VaultSettings, GeneratorRequest,
|
||||||
|
} from '../shared/types';
|
||||||
|
import {
|
||||||
|
GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP,
|
||||||
|
GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT,
|
||||||
|
} from '../shared/glyphs';
|
||||||
|
|
||||||
|
export type VaultView =
|
||||||
|
| 'list' | 'detail' | 'add' | 'edit' | 'trash' | 'devices' | 'settings'
|
||||||
|
| 'settings-vault' | 'field-history' | 'history' | 'backup' | 'import';
|
||||||
|
|
||||||
|
export interface HashRoute {
|
||||||
|
view: VaultView;
|
||||||
|
id?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VaultState {
|
||||||
|
unlocked: boolean;
|
||||||
|
view: VaultView;
|
||||||
|
entries: Array<[ItemId, ManifestEntry]>;
|
||||||
|
selectedId: ItemId | null;
|
||||||
|
selectedItem: Item | null;
|
||||||
|
selectedIndex: number;
|
||||||
|
searchQuery: string;
|
||||||
|
activeGroup: string | null;
|
||||||
|
drawerOpen: boolean;
|
||||||
|
typePanelOpen: boolean;
|
||||||
|
vaultSettings: VaultSettings | null;
|
||||||
|
generatorDefaults: GeneratorRequest | null;
|
||||||
|
error: string | null;
|
||||||
|
loading: boolean;
|
||||||
|
newType: ItemType | null;
|
||||||
|
capturedTabId: number | null;
|
||||||
|
capturedUrl: string;
|
||||||
|
historyItemId: ItemId | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The controller passed to every vault-* module. vault.ts builds one instance
|
||||||
|
// and wires each hook to the function that currently lives in vault.ts (later
|
||||||
|
// Phase-4 tasks repoint individual hooks at the extracted module functions).
|
||||||
|
export interface VaultController {
|
||||||
|
readonly state: VaultState;
|
||||||
|
sendMessage(request: Request): Promise<Response>;
|
||||||
|
render(): void;
|
||||||
|
renderPane(): void;
|
||||||
|
renderListPane(): void;
|
||||||
|
renderSidebarCategories(): void;
|
||||||
|
renderDrawer(item: Item): void;
|
||||||
|
applyShellViewClass(): void;
|
||||||
|
setHash(view: VaultView, param?: string): void;
|
||||||
|
openDrawer(): void;
|
||||||
|
closeDrawer(): void;
|
||||||
|
selectItemForDrawer(id: string): Promise<void>;
|
||||||
|
openTypePanel(): void;
|
||||||
|
closeTypePanel(): void;
|
||||||
|
wireSidebar(): void;
|
||||||
|
loadManifest(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- pure helpers (no state, no DOM dependencies beyond the args) ---
|
||||||
|
|
||||||
|
export function escapeHtml(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function typeIcon(t: ItemType): string {
|
||||||
|
switch (t) {
|
||||||
|
case 'login': return GLYPH_TYPE_LOGIN;
|
||||||
|
case 'secure_note': return GLYPH_TYPE_SECURE_NOTE;
|
||||||
|
case 'identity': return GLYPH_TYPE_IDENTITY;
|
||||||
|
case 'card': return GLYPH_TYPE_CARD;
|
||||||
|
case 'key': return GLYPH_TYPE_KEY;
|
||||||
|
case 'document': return GLYPH_TYPE_DOCUMENT;
|
||||||
|
case 'totp': return GLYPH_TYPE_TOTP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function typeLabel(t: ItemType): string {
|
||||||
|
const labels: Record<ItemType, string> = {
|
||||||
|
login: 'Login',
|
||||||
|
secure_note: 'Secure Note',
|
||||||
|
identity: 'Identity',
|
||||||
|
card: 'Card',
|
||||||
|
key: 'SSH / API Key',
|
||||||
|
document: 'Document',
|
||||||
|
totp: 'TOTP',
|
||||||
|
};
|
||||||
|
return labels[t];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilteredEntries(
|
||||||
|
state: VaultState,
|
||||||
|
): Array<[ItemId, ManifestEntry]> {
|
||||||
|
let filtered = state.entries.filter(
|
||||||
|
([, e]) => e.trashed_at === undefined || e.trashed_at === null,
|
||||||
|
);
|
||||||
|
if (state.searchQuery) {
|
||||||
|
const q = state.searchQuery.toLowerCase();
|
||||||
|
filtered = filtered.filter(([, e]) => {
|
||||||
|
if (e.title.toLowerCase().includes(q)) return true;
|
||||||
|
if (e.icon_hint?.toLowerCase().includes(q)) return true;
|
||||||
|
if (e.group?.toLowerCase().includes(q)) return true;
|
||||||
|
if (e.tags.some((t) => t.toLowerCase().includes(q))) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
filtered.sort((a, b) => a[1].title.localeCompare(b[1].title));
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
255
extension/src/vault/vault-shell.ts
Normal file
255
extension/src/vault/vault-shell.ts
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
// Vault-tab shell: render entry point, lock screen, 3-column shell
|
||||||
|
// scaffolding, the right-side type-picker panel, color-scheme apply, and the
|
||||||
|
// session_expired listener. Each function receives the VaultController (`ctx`)
|
||||||
|
// and reaches sibling concerns through it; pure helpers come from
|
||||||
|
// vault-context. vault.ts owns the state singleton and assembles the ctx.
|
||||||
|
|
||||||
|
import type { ItemType } from '../shared/types';
|
||||||
|
import { lookupErrorCopy } from '../shared/error-copy';
|
||||||
|
import {
|
||||||
|
GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK, GLYPH_HISTORY,
|
||||||
|
} from '../shared/glyphs';
|
||||||
|
import { applyColorScheme } from '../shared/color-scheme';
|
||||||
|
import {
|
||||||
|
type VaultController, escapeHtml, typeIcon,
|
||||||
|
} from './vault-context';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Type picker (right side panel)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const PICKER_TYPES: Array<{ type: ItemType; label: string }> = [
|
||||||
|
{ type: 'login', label: 'Login' },
|
||||||
|
{ type: 'secure_note', label: 'Secure Note' },
|
||||||
|
{ type: 'totp', label: 'TOTP' },
|
||||||
|
{ type: 'card', label: 'Card' },
|
||||||
|
{ type: 'identity', label: 'Identity' },
|
||||||
|
{ type: 'key', label: 'SSH / API Key' },
|
||||||
|
{ type: 'document', label: 'Document' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Render entry point
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function render(ctx: VaultController): void {
|
||||||
|
const app = document.getElementById('vault-app');
|
||||||
|
if (!app) return;
|
||||||
|
|
||||||
|
if (!ctx.state.unlocked) {
|
||||||
|
renderLockScreen(ctx, app);
|
||||||
|
} else {
|
||||||
|
renderShell(ctx, app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Lock screen
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function renderErrorBlock(code: string | null | undefined): string {
|
||||||
|
if (!code) return '';
|
||||||
|
const copy = lookupErrorCopy(code);
|
||||||
|
const ctaHtml = copy.cta
|
||||||
|
? `<button class="btn btn-primary error-cta" data-cta="${escapeHtml(copy.cta.action ?? '')}">${escapeHtml(copy.cta.label)}</button>`
|
||||||
|
: '';
|
||||||
|
return `
|
||||||
|
<div class="error error-block">
|
||||||
|
<div class="error-title">${escapeHtml(copy.title)}</div>
|
||||||
|
<div class="error-body">${escapeHtml(copy.body)}</div>
|
||||||
|
${ctaHtml}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderLockScreen(ctx: VaultController, app: HTMLElement): void {
|
||||||
|
app.innerHTML = `
|
||||||
|
<div class="vault-lock-screen">
|
||||||
|
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
||||||
|
<span class="brand">Relicario</span>
|
||||||
|
<div class="vault-lock-screen__form">
|
||||||
|
<input type="password" id="vault-passphrase" placeholder="passphrase" autocomplete="off" />
|
||||||
|
<button class="btn btn-primary" id="vault-unlock-btn" style="width:100%;">unlock</button>
|
||||||
|
${renderErrorBlock(ctx.state.error)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const input = document.getElementById('vault-passphrase') as HTMLInputElement;
|
||||||
|
const btn = document.getElementById('vault-unlock-btn')!;
|
||||||
|
|
||||||
|
const doUnlock = async () => {
|
||||||
|
const passphrase = input.value;
|
||||||
|
if (!passphrase) return;
|
||||||
|
btn.textContent = 'unlocking...';
|
||||||
|
btn.setAttribute('disabled', 'true');
|
||||||
|
const resp = await ctx.sendMessage({ type: 'unlock', passphrase });
|
||||||
|
if (resp.ok) {
|
||||||
|
ctx.state.unlocked = true;
|
||||||
|
ctx.state.error = null;
|
||||||
|
await ctx.loadManifest();
|
||||||
|
render(ctx);
|
||||||
|
} else {
|
||||||
|
ctx.state.error = resp.error ?? 'unlock failed';
|
||||||
|
render(ctx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
btn.addEventListener('click', doUnlock);
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') doUnlock();
|
||||||
|
});
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shell (3-column: sidebar + list pane + drawer)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function renderShell(ctx: VaultController, app: HTMLElement): void {
|
||||||
|
if (!app.querySelector('.vault-shell')) {
|
||||||
|
app.innerHTML = `
|
||||||
|
<div class="vault-shell">
|
||||||
|
<div class="vault-sidebar">
|
||||||
|
<div class="vault-sidebar__header">
|
||||||
|
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
||||||
|
<span class="brand">Relicario</span>
|
||||||
|
</div>
|
||||||
|
<div class="vault-sidebar__search">
|
||||||
|
<input type="text" id="vault-search" placeholder="/ search…" />
|
||||||
|
</div>
|
||||||
|
<nav class="vault-sidebar__categories" id="vault-categories" aria-label="Item types"></nav>
|
||||||
|
<div class="vault-sidebar__nav">
|
||||||
|
<button class="vault-sidebar__nav-item vault-sidebar__nav-item--primary" data-nav="add" title="New item">+ new item</button>
|
||||||
|
<button class="vault-sidebar__nav-item" data-nav="trash" title="Trash">${GLYPH_TRASH} <span class="vault-sidebar__nav-label">trash</span></button>
|
||||||
|
<button class="vault-sidebar__nav-item" data-nav="devices" title="Devices">${GLYPH_DEVICES} <span class="vault-sidebar__nav-label">devices</span></button>
|
||||||
|
<button class="vault-sidebar__nav-item" data-nav="settings" title="Settings">${GLYPH_SETTINGS} <span class="vault-sidebar__nav-label">settings</span></button>
|
||||||
|
<button class="vault-sidebar__nav-item" data-nav="history" title="History">${GLYPH_HISTORY} <span class="vault-sidebar__nav-label">history</span></button>
|
||||||
|
<button class="vault-sidebar__nav-item" data-nav="lock" title="Lock">${GLYPH_LOCK} <span class="vault-sidebar__nav-label">lock</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vault-list-pane" id="vault-list-pane"></div>
|
||||||
|
<div class="vault-pane" id="vault-pane"></div>
|
||||||
|
<div class="vault-drawer" id="vault-drawer"></div>
|
||||||
|
<div class="vault-type-panel-scrim" id="vault-type-scrim"></div>
|
||||||
|
<aside class="vault-type-panel" id="vault-type-panel" aria-label="Choose item type"></aside>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
ctx.wireSidebar();
|
||||||
|
wireTypePanel(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyShellViewClass(ctx);
|
||||||
|
ctx.renderSidebarCategories();
|
||||||
|
if (ctx.state.view === 'list') {
|
||||||
|
ctx.renderListPane();
|
||||||
|
if (ctx.state.drawerOpen && ctx.state.selectedItem) {
|
||||||
|
ctx.renderDrawer(ctx.state.selectedItem);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.renderPane();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle which middle column is visible based on the current view.
|
||||||
|
// list view → list-pane (+ optional drawer); other views → vault-pane.
|
||||||
|
export function applyShellViewClass(ctx: VaultController): void {
|
||||||
|
const shell = document.querySelector('.vault-shell');
|
||||||
|
if (!shell) return;
|
||||||
|
shell.classList.toggle('vault-shell--list', ctx.state.view === 'list');
|
||||||
|
shell.classList.toggle('vault-shell--pane', ctx.state.view !== 'list');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Right-side type picker panel
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function wireTypePanel(ctx: VaultController): void {
|
||||||
|
document.getElementById('vault-type-scrim')?.addEventListener('click', () => closeTypePanel(ctx));
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape' && ctx.state.typePanelOpen) closeTypePanel(ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openTypePanel(ctx: VaultController): void {
|
||||||
|
const panel = document.getElementById('vault-type-panel');
|
||||||
|
const scrim = document.getElementById('vault-type-scrim');
|
||||||
|
if (!panel || !scrim) return;
|
||||||
|
|
||||||
|
panel.innerHTML = `
|
||||||
|
<div class="vault-type-panel__head">
|
||||||
|
<div class="vault-type-panel__title">New item</div>
|
||||||
|
<button class="vault-type-panel__close" id="vault-type-close" title="Close (Esc)" aria-label="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="vault-type-panel__hint">Choose a type</div>
|
||||||
|
<div class="vault-type-list" role="menu">
|
||||||
|
${PICKER_TYPES.map((t) => `
|
||||||
|
<button class="vault-type-item" data-type="${t.type}" role="menuitem">
|
||||||
|
<span class="vault-type-item__icon" aria-hidden="true">${typeIcon(t.type)}</span>
|
||||||
|
<span class="vault-type-item__name">${escapeHtml(t.label)}</span>
|
||||||
|
</button>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
panel.classList.add('vault-type-panel--open');
|
||||||
|
scrim.classList.add('vault-type-panel-scrim--visible');
|
||||||
|
ctx.state.typePanelOpen = true;
|
||||||
|
|
||||||
|
panel.querySelector('#vault-type-close')?.addEventListener('click', () => closeTypePanel(ctx));
|
||||||
|
|
||||||
|
panel.querySelectorAll<HTMLButtonElement>('[data-type]').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const type = btn.dataset.type as ItemType;
|
||||||
|
closeTypePanel(ctx);
|
||||||
|
// Use the host's navigate hook so view + hash + visibility all update
|
||||||
|
// together. This was the bug: bare setHash + renderPane left the
|
||||||
|
// shell stuck in list view with #vault-pane hidden.
|
||||||
|
ctx.state.newType = type;
|
||||||
|
ctx.state.selectedId = null;
|
||||||
|
ctx.state.selectedItem = null;
|
||||||
|
ctx.state.drawerOpen = false;
|
||||||
|
ctx.state.view = 'add';
|
||||||
|
ctx.setHash('add', type);
|
||||||
|
applyShellViewClass(ctx);
|
||||||
|
ctx.renderSidebarCategories();
|
||||||
|
ctx.renderPane();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus first item for keyboard users
|
||||||
|
(panel.querySelector('.vault-type-item') as HTMLElement | null)?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeTypePanel(ctx: VaultController): void {
|
||||||
|
document.getElementById('vault-type-panel')?.classList.remove('vault-type-panel--open');
|
||||||
|
document.getElementById('vault-type-scrim')?.classList.remove('vault-type-panel-scrim--visible');
|
||||||
|
ctx.state.typePanelOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Color scheme + session-expired wiring (bootstrap helpers)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function applyVaultColorScheme(): Promise<void> {
|
||||||
|
await applyColorScheme();
|
||||||
|
|
||||||
|
chrome.storage.onChanged.addListener((changes, area) => {
|
||||||
|
if (area === 'sync' && 'password_display_scheme' in changes) {
|
||||||
|
void applyColorScheme();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wireSessionExpiredListener(ctx: VaultController): void {
|
||||||
|
chrome.runtime.onMessage.addListener((msg) => {
|
||||||
|
if (msg.type === 'session_expired') {
|
||||||
|
ctx.state.unlocked = false;
|
||||||
|
ctx.state.selectedId = null;
|
||||||
|
ctx.state.selectedItem = null;
|
||||||
|
ctx.state.entries = [];
|
||||||
|
ctx.state.error = null;
|
||||||
|
render(ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -9,13 +9,8 @@ import type {
|
|||||||
ItemId, ItemType, ManifestEntry, Item, VaultSettings, GeneratorRequest,
|
ItemId, ItemType, ManifestEntry, Item, VaultSettings, GeneratorRequest,
|
||||||
} from '../shared/types';
|
} from '../shared/types';
|
||||||
import { registerHost } from '../shared/state';
|
import { registerHost } from '../shared/state';
|
||||||
import { lookupErrorCopy, type ErrorCta } from '../shared/error-copy';
|
import { type ErrorCta } from '../shared/error-copy';
|
||||||
import { relativeTime } from '../shared/relative-time';
|
import { relativeTime } from '../shared/relative-time';
|
||||||
import {
|
|
||||||
GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK, GLYPH_HISTORY,
|
|
||||||
GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP,
|
|
||||||
GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT,
|
|
||||||
} from '../shared/glyphs';
|
|
||||||
import { renderItemDetail } from '../popup/components/item-detail';
|
import { renderItemDetail } from '../popup/components/item-detail';
|
||||||
import { renderItemForm } from '../popup/components/item-form';
|
import { renderItemForm } from '../popup/components/item-form';
|
||||||
import { renderTrash, teardown as teardownTrash } from '../popup/components/trash';
|
import { renderTrash, teardown as teardownTrash } from '../popup/components/trash';
|
||||||
@@ -26,21 +21,15 @@ import { renderFieldHistory, teardown as teardownFieldHistory } from '../popup/c
|
|||||||
import { renderItemHistoryIndex, teardown as teardownHistoryIndex } from '../popup/components/item-history-index';
|
import { renderItemHistoryIndex, teardown as teardownHistoryIndex } from '../popup/components/item-history-index';
|
||||||
import { renderBackupPanel, teardown as teardownBackup } from './components/backup-panel';
|
import { renderBackupPanel, teardown as teardownBackup } from './components/backup-panel';
|
||||||
import { renderImportPanel, teardown as teardownImport } from './components/import-panel';
|
import { renderImportPanel, teardown as teardownImport } from './components/import-panel';
|
||||||
import { applyColorScheme } from '../shared/color-scheme';
|
import {
|
||||||
|
type VaultController, type VaultState, type VaultView, type HashRoute,
|
||||||
// ---------------------------------------------------------------------------
|
escapeHtml, typeIcon, typeLabel, getFilteredEntries,
|
||||||
// Type picker (right side panel)
|
} from './vault-context';
|
||||||
// ---------------------------------------------------------------------------
|
import {
|
||||||
|
render, applyShellViewClass,
|
||||||
const PICKER_TYPES: Array<{ type: ItemType; label: string }> = [
|
openTypePanel, closeTypePanel, applyVaultColorScheme,
|
||||||
{ type: 'login', label: 'Login' },
|
wireSessionExpiredListener,
|
||||||
{ type: 'secure_note', label: 'Secure Note' },
|
} from './vault-shell';
|
||||||
{ type: 'totp', label: 'TOTP' },
|
|
||||||
{ type: 'card', label: 'Card' },
|
|
||||||
{ type: 'identity', label: 'Identity' },
|
|
||||||
{ type: 'key', label: 'SSH / API Key' },
|
|
||||||
{ type: 'document', label: 'Document' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
@@ -68,74 +57,17 @@ function sendMessage(request: Request): Promise<Response> {
|
|||||||
state.selectedItem = null;
|
state.selectedItem = null;
|
||||||
state.entries = [];
|
state.entries = [];
|
||||||
state.error = 'Session expired — please unlock again.';
|
state.error = 'Session expired — please unlock again.';
|
||||||
render();
|
render(ctx);
|
||||||
}
|
}
|
||||||
resolve(response);
|
resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(str: string): string {
|
|
||||||
return str
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderErrorBlock(code: string | null | undefined): string {
|
|
||||||
if (!code) return '';
|
|
||||||
const copy = lookupErrorCopy(code);
|
|
||||||
const ctaHtml = copy.cta
|
|
||||||
? `<button class="btn btn-primary error-cta" data-cta="${escapeHtml(copy.cta.action ?? '')}">${escapeHtml(copy.cta.label)}</button>`
|
|
||||||
: '';
|
|
||||||
return `
|
|
||||||
<div class="error error-block">
|
|
||||||
<div class="error-title">${escapeHtml(copy.title)}</div>
|
|
||||||
<div class="error-body">${escapeHtml(copy.body)}</div>
|
|
||||||
${ctaHtml}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function typeIcon(t: ItemType): string {
|
|
||||||
switch (t) {
|
|
||||||
case 'login': return GLYPH_TYPE_LOGIN;
|
|
||||||
case 'secure_note': return GLYPH_TYPE_SECURE_NOTE;
|
|
||||||
case 'identity': return GLYPH_TYPE_IDENTITY;
|
|
||||||
case 'card': return GLYPH_TYPE_CARD;
|
|
||||||
case 'key': return GLYPH_TYPE_KEY;
|
|
||||||
case 'document': return GLYPH_TYPE_DOCUMENT;
|
|
||||||
case 'totp': return GLYPH_TYPE_TOTP;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function typeLabel(t: ItemType): string {
|
|
||||||
const labels: Record<ItemType, string> = {
|
|
||||||
login: 'Login',
|
|
||||||
secure_note: 'Secure Note',
|
|
||||||
identity: 'Identity',
|
|
||||||
card: 'Card',
|
|
||||||
key: 'SSH / API Key',
|
|
||||||
document: 'Document',
|
|
||||||
totp: 'TOTP',
|
|
||||||
};
|
|
||||||
return labels[t];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Hash routing
|
// Hash routing
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type VaultView = 'list' | 'detail' | 'add' | 'edit' | 'trash' | 'devices' | 'settings' | 'settings-vault' | 'field-history' | 'history' | 'backup' | 'import';
|
|
||||||
|
|
||||||
interface HashRoute {
|
|
||||||
view: VaultView;
|
|
||||||
id?: string;
|
|
||||||
type?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseHash(): HashRoute {
|
function parseHash(): HashRoute {
|
||||||
let raw = window.location.hash.replace(/^#\/?/, '');
|
let raw = window.location.hash.replace(/^#\/?/, '');
|
||||||
if (!raw) return { view: 'list' };
|
if (!raw) return { view: 'list' };
|
||||||
@@ -181,27 +113,6 @@ function setHash(view: VaultView, param?: string): void {
|
|||||||
// State
|
// State
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
interface VaultState {
|
|
||||||
unlocked: boolean;
|
|
||||||
view: VaultView;
|
|
||||||
entries: Array<[ItemId, ManifestEntry]>;
|
|
||||||
selectedId: ItemId | null;
|
|
||||||
selectedItem: Item | null;
|
|
||||||
selectedIndex: number;
|
|
||||||
searchQuery: string;
|
|
||||||
activeGroup: string | null;
|
|
||||||
drawerOpen: boolean;
|
|
||||||
typePanelOpen: boolean;
|
|
||||||
vaultSettings: VaultSettings | null;
|
|
||||||
generatorDefaults: GeneratorRequest | null;
|
|
||||||
error: string | null;
|
|
||||||
loading: boolean;
|
|
||||||
newType: ItemType | null;
|
|
||||||
capturedTabId: number | null;
|
|
||||||
capturedUrl: string;
|
|
||||||
historyItemId: ItemId | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state: VaultState = {
|
const state: VaultState = {
|
||||||
unlocked: false,
|
unlocked: false,
|
||||||
view: 'list',
|
view: 'list',
|
||||||
@@ -223,6 +134,29 @@ const state: VaultState = {
|
|||||||
historyItemId: null,
|
historyItemId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Controller — carries state + cross-module re-render hooks
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const ctx: VaultController = {
|
||||||
|
state,
|
||||||
|
sendMessage,
|
||||||
|
render: () => render(ctx),
|
||||||
|
renderPane: () => renderPane(),
|
||||||
|
renderListPane: () => renderListPane(),
|
||||||
|
renderSidebarCategories: () => renderSidebarCategories(),
|
||||||
|
renderDrawer: (item) => renderDrawer(item),
|
||||||
|
applyShellViewClass: () => applyShellViewClass(ctx),
|
||||||
|
setHash,
|
||||||
|
openDrawer: () => openDrawer(),
|
||||||
|
closeDrawer: () => closeDrawer(),
|
||||||
|
selectItemForDrawer: (id) => selectItemForDrawer(id),
|
||||||
|
openTypePanel: () => openTypePanel(ctx),
|
||||||
|
closeTypePanel: () => closeTypePanel(ctx),
|
||||||
|
wireSidebar: () => wireSidebar(),
|
||||||
|
loadManifest: () => loadManifest(),
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Register as shared state host
|
// Register as shared state host
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -236,7 +170,7 @@ registerHost({
|
|||||||
navigate: (view, extras) => {
|
navigate: (view, extras) => {
|
||||||
Object.assign(state, { view, error: null, loading: false, ...extras });
|
Object.assign(state, { view, error: null, loading: false, ...extras });
|
||||||
setHash(view as VaultView);
|
setHash(view as VaultView);
|
||||||
applyShellViewClass();
|
applyShellViewClass(ctx);
|
||||||
renderSidebarCategories();
|
renderSidebarCategories();
|
||||||
if (state.view === 'list') renderListPane();
|
if (state.view === 'list') renderListPane();
|
||||||
renderPane();
|
renderPane();
|
||||||
@@ -248,190 +182,6 @@ registerHost({
|
|||||||
openVaultTab: () => {},
|
openVaultTab: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Render entry point
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function render(): void {
|
|
||||||
const app = document.getElementById('vault-app');
|
|
||||||
if (!app) return;
|
|
||||||
|
|
||||||
if (!state.unlocked) {
|
|
||||||
renderLockScreen(app);
|
|
||||||
} else {
|
|
||||||
renderShell(app);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Lock screen
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function renderLockScreen(app: HTMLElement): void {
|
|
||||||
app.innerHTML = `
|
|
||||||
<div class="vault-lock-screen">
|
|
||||||
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
|
||||||
<span class="brand">Relicario</span>
|
|
||||||
<div class="vault-lock-screen__form">
|
|
||||||
<input type="password" id="vault-passphrase" placeholder="passphrase" autocomplete="off" />
|
|
||||||
<button class="btn btn-primary" id="vault-unlock-btn" style="width:100%;">unlock</button>
|
|
||||||
${renderErrorBlock(state.error)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const input = document.getElementById('vault-passphrase') as HTMLInputElement;
|
|
||||||
const btn = document.getElementById('vault-unlock-btn')!;
|
|
||||||
|
|
||||||
const doUnlock = async () => {
|
|
||||||
const passphrase = input.value;
|
|
||||||
if (!passphrase) return;
|
|
||||||
btn.textContent = 'unlocking...';
|
|
||||||
btn.setAttribute('disabled', 'true');
|
|
||||||
const resp = await sendMessage({ type: 'unlock', passphrase });
|
|
||||||
if (resp.ok) {
|
|
||||||
state.unlocked = true;
|
|
||||||
state.error = null;
|
|
||||||
await loadManifest();
|
|
||||||
render();
|
|
||||||
} else {
|
|
||||||
state.error = resp.error ?? 'unlock failed';
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
btn.addEventListener('click', doUnlock);
|
|
||||||
input.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Enter') doUnlock();
|
|
||||||
});
|
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Shell (3-column: sidebar + list pane + drawer)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function renderShell(app: HTMLElement): void {
|
|
||||||
if (!app.querySelector('.vault-shell')) {
|
|
||||||
app.innerHTML = `
|
|
||||||
<div class="vault-shell">
|
|
||||||
<div class="vault-sidebar">
|
|
||||||
<div class="vault-sidebar__header">
|
|
||||||
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
|
||||||
<span class="brand">Relicario</span>
|
|
||||||
</div>
|
|
||||||
<div class="vault-sidebar__search">
|
|
||||||
<input type="text" id="vault-search" placeholder="/ search…" />
|
|
||||||
</div>
|
|
||||||
<nav class="vault-sidebar__categories" id="vault-categories" aria-label="Item types"></nav>
|
|
||||||
<div class="vault-sidebar__nav">
|
|
||||||
<button class="vault-sidebar__nav-item vault-sidebar__nav-item--primary" data-nav="add" title="New item">+ new item</button>
|
|
||||||
<button class="vault-sidebar__nav-item" data-nav="trash" title="Trash">${GLYPH_TRASH} <span class="vault-sidebar__nav-label">trash</span></button>
|
|
||||||
<button class="vault-sidebar__nav-item" data-nav="devices" title="Devices">${GLYPH_DEVICES} <span class="vault-sidebar__nav-label">devices</span></button>
|
|
||||||
<button class="vault-sidebar__nav-item" data-nav="settings" title="Settings">${GLYPH_SETTINGS} <span class="vault-sidebar__nav-label">settings</span></button>
|
|
||||||
<button class="vault-sidebar__nav-item" data-nav="history" title="History">${GLYPH_HISTORY} <span class="vault-sidebar__nav-label">history</span></button>
|
|
||||||
<button class="vault-sidebar__nav-item" data-nav="lock" title="Lock">${GLYPH_LOCK} <span class="vault-sidebar__nav-label">lock</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="vault-list-pane" id="vault-list-pane"></div>
|
|
||||||
<div class="vault-pane" id="vault-pane"></div>
|
|
||||||
<div class="vault-drawer" id="vault-drawer"></div>
|
|
||||||
<div class="vault-type-panel-scrim" id="vault-type-scrim"></div>
|
|
||||||
<aside class="vault-type-panel" id="vault-type-panel" aria-label="Choose item type"></aside>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
wireSidebar();
|
|
||||||
wireTypePanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
applyShellViewClass();
|
|
||||||
renderSidebarCategories();
|
|
||||||
if (state.view === 'list') {
|
|
||||||
renderListPane();
|
|
||||||
if (state.drawerOpen && state.selectedItem) {
|
|
||||||
renderDrawer(state.selectedItem);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
renderPane();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle which middle column is visible based on the current view.
|
|
||||||
// list view → list-pane (+ optional drawer); other views → vault-pane.
|
|
||||||
function applyShellViewClass(): void {
|
|
||||||
const shell = document.querySelector('.vault-shell');
|
|
||||||
if (!shell) return;
|
|
||||||
shell.classList.toggle('vault-shell--list', state.view === 'list');
|
|
||||||
shell.classList.toggle('vault-shell--pane', state.view !== 'list');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Right-side type picker panel
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function wireTypePanel(): void {
|
|
||||||
document.getElementById('vault-type-scrim')?.addEventListener('click', closeTypePanel);
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape' && state.typePanelOpen) closeTypePanel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function openTypePanel(): void {
|
|
||||||
const panel = document.getElementById('vault-type-panel');
|
|
||||||
const scrim = document.getElementById('vault-type-scrim');
|
|
||||||
if (!panel || !scrim) return;
|
|
||||||
|
|
||||||
panel.innerHTML = `
|
|
||||||
<div class="vault-type-panel__head">
|
|
||||||
<div class="vault-type-panel__title">New item</div>
|
|
||||||
<button class="vault-type-panel__close" id="vault-type-close" title="Close (Esc)" aria-label="Close">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="vault-type-panel__hint">Choose a type</div>
|
|
||||||
<div class="vault-type-list" role="menu">
|
|
||||||
${PICKER_TYPES.map((t) => `
|
|
||||||
<button class="vault-type-item" data-type="${t.type}" role="menuitem">
|
|
||||||
<span class="vault-type-item__icon" aria-hidden="true">${typeIcon(t.type)}</span>
|
|
||||||
<span class="vault-type-item__name">${escapeHtml(t.label)}</span>
|
|
||||||
</button>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
panel.classList.add('vault-type-panel--open');
|
|
||||||
scrim.classList.add('vault-type-panel-scrim--visible');
|
|
||||||
state.typePanelOpen = true;
|
|
||||||
|
|
||||||
panel.querySelector('#vault-type-close')?.addEventListener('click', closeTypePanel);
|
|
||||||
|
|
||||||
panel.querySelectorAll<HTMLButtonElement>('[data-type]').forEach((btn) => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const type = btn.dataset.type as ItemType;
|
|
||||||
closeTypePanel();
|
|
||||||
// Use the host's navigate hook so view + hash + visibility all update
|
|
||||||
// together. This was the bug: bare setHash + renderPane left the
|
|
||||||
// shell stuck in list view with #vault-pane hidden.
|
|
||||||
state.newType = type;
|
|
||||||
state.selectedId = null;
|
|
||||||
state.selectedItem = null;
|
|
||||||
state.drawerOpen = false;
|
|
||||||
state.view = 'add';
|
|
||||||
setHash('add', type);
|
|
||||||
applyShellViewClass();
|
|
||||||
renderSidebarCategories();
|
|
||||||
renderPane();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Focus first item for keyboard users
|
|
||||||
(panel.querySelector('.vault-type-item') as HTMLElement | null)?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeTypePanel(): void {
|
|
||||||
document.getElementById('vault-type-panel')?.classList.remove('vault-type-panel--open');
|
|
||||||
document.getElementById('vault-type-scrim')?.classList.remove('vault-type-panel-scrim--visible');
|
|
||||||
state.typePanelOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Drawer (implemented in Task 10)
|
// Drawer (implemented in Task 10)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -581,7 +331,7 @@ function wireSidebar(): void {
|
|||||||
state.selectedId = null;
|
state.selectedId = null;
|
||||||
state.selectedItem = null;
|
state.selectedItem = null;
|
||||||
state.entries = [];
|
state.entries = [];
|
||||||
render();
|
render(ctx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (nav === 'add') {
|
if (nav === 'add') {
|
||||||
@@ -590,7 +340,7 @@ function wireSidebar(): void {
|
|||||||
state.newType = null;
|
state.newType = null;
|
||||||
state.drawerOpen = false;
|
state.drawerOpen = false;
|
||||||
closeDrawer();
|
closeDrawer();
|
||||||
openTypePanel();
|
openTypePanel(ctx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (nav === 'trash' || nav === 'devices' || nav === 'settings' || nav === 'history') {
|
if (nav === 'trash' || nav === 'devices' || nav === 'settings' || nav === 'history') {
|
||||||
@@ -600,7 +350,7 @@ function wireSidebar(): void {
|
|||||||
state.drawerOpen = false;
|
state.drawerOpen = false;
|
||||||
state.view = nav;
|
state.view = nav;
|
||||||
setHash(nav);
|
setHash(nav);
|
||||||
applyShellViewClass();
|
applyShellViewClass(ctx);
|
||||||
renderPane();
|
renderPane();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -633,29 +383,11 @@ function isEditableTarget(target: EventTarget | null): boolean {
|
|||||||
// Sidebar category nav
|
// Sidebar category nav
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function getFilteredEntries(): Array<[ItemId, ManifestEntry]> {
|
|
||||||
let filtered = state.entries.filter(
|
|
||||||
([, e]) => e.trashed_at === undefined || e.trashed_at === null,
|
|
||||||
);
|
|
||||||
if (state.searchQuery) {
|
|
||||||
const q = state.searchQuery.toLowerCase();
|
|
||||||
filtered = filtered.filter(([, e]) => {
|
|
||||||
if (e.title.toLowerCase().includes(q)) return true;
|
|
||||||
if (e.icon_hint?.toLowerCase().includes(q)) return true;
|
|
||||||
if (e.group?.toLowerCase().includes(q)) return true;
|
|
||||||
if (e.tags.some((t) => t.toLowerCase().includes(q))) return true;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
filtered.sort((a, b) => a[1].title.localeCompare(b[1].title));
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSidebarCategories(): void {
|
function renderSidebarCategories(): void {
|
||||||
const container = document.getElementById('vault-categories');
|
const container = document.getElementById('vault-categories');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const filtered = getFilteredEntries();
|
const filtered = getFilteredEntries(state);
|
||||||
const typeOrder: ItemType[] = ['login', 'secure_note', 'identity', 'card', 'key', 'document', 'totp'];
|
const typeOrder: ItemType[] = ['login', 'secure_note', 'identity', 'card', 'key', 'document', 'totp'];
|
||||||
|
|
||||||
const allCount = filtered.length;
|
const allCount = filtered.length;
|
||||||
@@ -693,7 +425,7 @@ function renderSidebarCategories(): void {
|
|||||||
state.selectedItem = null;
|
state.selectedItem = null;
|
||||||
state.view = 'list';
|
state.view = 'list';
|
||||||
setHash('list');
|
setHash('list');
|
||||||
applyShellViewClass();
|
applyShellViewClass(ctx);
|
||||||
renderSidebarCategories();
|
renderSidebarCategories();
|
||||||
renderListPane();
|
renderListPane();
|
||||||
closeDrawer();
|
closeDrawer();
|
||||||
@@ -710,7 +442,7 @@ function renderListPane(): void {
|
|||||||
if (!pane) return;
|
if (!pane) return;
|
||||||
|
|
||||||
const group = state.activeGroup as ItemType | null;
|
const group = state.activeGroup as ItemType | null;
|
||||||
let items = getFilteredEntries();
|
let items = getFilteredEntries(state);
|
||||||
if (group) items = items.filter(([, e]) => e.type === group);
|
if (group) items = items.filter(([, e]) => e.type === group);
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
@@ -835,7 +567,7 @@ function renderPane(): void {
|
|||||||
const route = parseHash();
|
const route = parseHash();
|
||||||
// Keep state.view in sync with hash for components that read it
|
// Keep state.view in sync with hash for components that read it
|
||||||
state.view = route.view;
|
state.view = route.view;
|
||||||
applyShellViewClass();
|
applyShellViewClass(ctx);
|
||||||
|
|
||||||
pane.className = 'vault-pane';
|
pane.className = 'vault-pane';
|
||||||
|
|
||||||
@@ -930,13 +662,7 @@ async function loadManifest(): Promise<void> {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await applyColorScheme();
|
await applyVaultColorScheme();
|
||||||
|
|
||||||
chrome.storage.onChanged.addListener((changes, area) => {
|
|
||||||
if (area === 'sync' && 'password_display_scheme' in changes) {
|
|
||||||
void applyColorScheme();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delegated handler for .error-cta buttons — set up once on the stable root.
|
// Delegated handler for .error-cta buttons — set up once on the stable root.
|
||||||
const app = document.getElementById('vault-app')!;
|
const app = document.getElementById('vault-app')!;
|
||||||
@@ -970,19 +696,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render();
|
render(ctx);
|
||||||
|
|
||||||
// Session expired listener
|
wireSessionExpiredListener(ctx);
|
||||||
chrome.runtime.onMessage.addListener((msg) => {
|
|
||||||
if (msg.type === 'session_expired') {
|
|
||||||
state.unlocked = false;
|
|
||||||
state.selectedId = null;
|
|
||||||
state.selectedItem = null;
|
|
||||||
state.entries = [];
|
|
||||||
state.error = null;
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hash change listener
|
// Hash change listener
|
||||||
window.addEventListener('hashchange', () => {
|
window.addEventListener('hashchange', () => {
|
||||||
@@ -990,7 +706,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
const route = parseHash();
|
const route = parseHash();
|
||||||
state.view = route.view;
|
state.view = route.view;
|
||||||
applyShellViewClass();
|
applyShellViewClass(ctx);
|
||||||
|
|
||||||
// If navigating to a detail/edit view for an item we already have loaded
|
// If navigating to a detail/edit view for an item we already have loaded
|
||||||
if ((route.view === 'detail' || route.view === 'edit') && route.id) {
|
if ((route.view === 'detail' || route.view === 'edit') && route.id) {
|
||||||
|
|||||||
Reference in New Issue
Block a user