fix(ext): glyph + vault polish — distinct identity glyph, sticky-bar form-actions hidden
GLYPH_TYPE_IDENTITY changed from ⌬ to ◍ so it's visually distinct from GLYPH_DEVICES (also ⌬). Adds a CSS rule asserting [hidden] over the .form-actions display:flex so the fullscreen sticky save bar can hide the inner action row by attribute. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -27,10 +27,10 @@ import { renderImportPanel, teardown as teardownImport } from './components/impo
|
||||
import { applyColorScheme } from '../shared/color-scheme';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bottom sheet type picker
|
||||
// Type picker (right side panel)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BOTTOM_SHEET_TYPES: Array<{ type: ItemType; label: string }> = [
|
||||
const PICKER_TYPES: Array<{ type: ItemType; label: string }> = [
|
||||
{ type: 'login', label: 'Login' },
|
||||
{ type: 'secure_note', label: 'Secure Note' },
|
||||
{ type: 'totp', label: 'TOTP' },
|
||||
@@ -47,6 +47,27 @@ const BOTTOM_SHEET_TYPES: Array<{ type: ItemType; label: string }> = [
|
||||
function sendMessage(request: Request): Promise<Response> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage(request, (response: Response) => {
|
||||
// MV3 service workers are evicted after ~30s idle, which wipes the
|
||||
// in-memory session/manifest/gitHost. The fullscreen tab stays open
|
||||
// and has no signal that the SW restarted — the next RPC just comes
|
||||
// back `vault_locked`. Treat that as "session lost" and force the
|
||||
// lock screen so the user can re-enter their passphrase. Skip for
|
||||
// is_unlocked / unlock themselves to avoid loops on cold start.
|
||||
if (
|
||||
response &&
|
||||
!response.ok &&
|
||||
response.error === 'vault_locked' &&
|
||||
request.type !== 'is_unlocked' &&
|
||||
request.type !== 'unlock' &&
|
||||
state.unlocked
|
||||
) {
|
||||
state.unlocked = false;
|
||||
state.selectedId = null;
|
||||
state.selectedItem = null;
|
||||
state.entries = [];
|
||||
state.error = 'Session expired — please unlock again.';
|
||||
render();
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
@@ -166,7 +187,7 @@ interface VaultState {
|
||||
searchQuery: string;
|
||||
activeGroup: string | null;
|
||||
drawerOpen: boolean;
|
||||
bottomSheetOpen: boolean;
|
||||
typePanelOpen: boolean;
|
||||
vaultSettings: VaultSettings | null;
|
||||
generatorDefaults: GeneratorRequest | null;
|
||||
error: string | null;
|
||||
@@ -187,7 +208,7 @@ const state: VaultState = {
|
||||
searchQuery: '',
|
||||
activeGroup: null,
|
||||
drawerOpen: false,
|
||||
bottomSheetOpen: false,
|
||||
typePanelOpen: false,
|
||||
vaultSettings: null,
|
||||
generatorDefaults: null,
|
||||
error: null,
|
||||
@@ -211,8 +232,9 @@ registerHost({
|
||||
navigate: (view: string, extras?: any) => {
|
||||
Object.assign(state, { view, error: null, loading: false, ...extras });
|
||||
setHash(view as VaultView);
|
||||
applyShellViewClass();
|
||||
renderSidebarCategories();
|
||||
renderListPane();
|
||||
if (state.view === 'list') renderListPane();
|
||||
renderPane();
|
||||
},
|
||||
sendMessage,
|
||||
@@ -290,7 +312,7 @@ function renderShell(app: HTMLElement): void {
|
||||
<div class="vault-shell">
|
||||
<div class="vault-sidebar">
|
||||
<div class="vault-sidebar__header">
|
||||
<img class="brand-logo" src="icons/relicario-logo-16.svg" alt="">
|
||||
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
||||
<span class="brand">Relicario</span>
|
||||
</div>
|
||||
<div class="vault-sidebar__search">
|
||||
@@ -298,7 +320,7 @@ function renderShell(app: HTMLElement): void {
|
||||
</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" data-nav="add" title="New item">+ new item</button>
|
||||
<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>
|
||||
@@ -306,69 +328,102 @@ function renderShell(app: HTMLElement): void {
|
||||
</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-bottom-sheet-scrim" id="vault-sheet-scrim"></div>
|
||||
<div class="vault-bottom-sheet" id="vault-bottom-sheet"></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();
|
||||
wireBottomSheet();
|
||||
wireTypePanel();
|
||||
}
|
||||
|
||||
applyShellViewClass();
|
||||
renderSidebarCategories();
|
||||
renderListPane();
|
||||
if (state.drawerOpen && state.selectedItem) {
|
||||
renderDrawer(state.selectedItem);
|
||||
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');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bottom sheet (wired in Task 11)
|
||||
// Right-side type picker panel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function wireBottomSheet(): void {
|
||||
document.getElementById('vault-sheet-scrim')?.addEventListener('click', closeBottomSheet);
|
||||
function wireTypePanel(): void {
|
||||
document.getElementById('vault-type-scrim')?.addEventListener('click', closeTypePanel);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && state.bottomSheetOpen) closeBottomSheet();
|
||||
if (e.key === 'Escape' && state.typePanelOpen) closeTypePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function openBottomSheet(): void {
|
||||
const sheet = document.getElementById('vault-bottom-sheet');
|
||||
const scrim = document.getElementById('vault-sheet-scrim');
|
||||
if (!sheet || !scrim) return;
|
||||
function openTypePanel(): void {
|
||||
const panel = document.getElementById('vault-type-panel');
|
||||
const scrim = document.getElementById('vault-type-scrim');
|
||||
if (!panel || !scrim) return;
|
||||
|
||||
sheet.innerHTML = `
|
||||
<div class="vault-bottom-sheet__handle"></div>
|
||||
<div class="vault-bottom-sheet__title">New item — choose type</div>
|
||||
<div class="vault-type-grid">
|
||||
${BOTTOM_SHEET_TYPES.map((t) => `
|
||||
<button class="vault-type-card" data-type="${t.type}">
|
||||
<span class="vault-type-card__icon" aria-hidden="true">${typeIcon(t.type)}</span>
|
||||
<span class="vault-type-card__name">${escapeHtml(t.label)}</span>
|
||||
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>
|
||||
`;
|
||||
|
||||
sheet.classList.add('vault-bottom-sheet--open');
|
||||
scrim.classList.add('vault-bottom-sheet-scrim--visible');
|
||||
state.bottomSheetOpen = true;
|
||||
panel.classList.add('vault-type-panel--open');
|
||||
scrim.classList.add('vault-type-panel-scrim--visible');
|
||||
state.typePanelOpen = true;
|
||||
|
||||
sheet.querySelectorAll<HTMLButtonElement>('[data-type]').forEach((btn) => {
|
||||
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;
|
||||
closeBottomSheet();
|
||||
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 closeBottomSheet(): void {
|
||||
document.getElementById('vault-bottom-sheet')?.classList.remove('vault-bottom-sheet--open');
|
||||
document.getElementById('vault-sheet-scrim')?.classList.remove('vault-bottom-sheet-scrim--visible');
|
||||
state.bottomSheetOpen = false;
|
||||
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;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -527,14 +582,19 @@ function wireSidebar(): void {
|
||||
state.selectedId = null;
|
||||
state.selectedItem = null;
|
||||
state.newType = null;
|
||||
openBottomSheet();
|
||||
state.drawerOpen = false;
|
||||
closeDrawer();
|
||||
openTypePanel();
|
||||
return;
|
||||
}
|
||||
if (nav === 'trash' || nav === 'devices' || nav === 'settings') {
|
||||
state.selectedId = null;
|
||||
state.selectedItem = null;
|
||||
state.newType = null;
|
||||
state.drawerOpen = false;
|
||||
state.view = nav;
|
||||
setHash(nav);
|
||||
applyShellViewClass();
|
||||
renderPane();
|
||||
return;
|
||||
}
|
||||
@@ -605,7 +665,8 @@ function renderSidebarCategories(): void {
|
||||
|
||||
for (const t of typeOrder) {
|
||||
const count = filtered.filter(([, e]) => e.type === t).length;
|
||||
if (count === 0 && allCount > 0) continue;
|
||||
// Always show Login (staple type); hide other types when empty.
|
||||
if (count === 0 && t !== 'login') continue;
|
||||
const isActive = state.activeGroup === t;
|
||||
html += `
|
||||
<button class="vault-category-row ${isActive ? 'vault-category-row--active' : ''}" data-group="${t}">
|
||||
@@ -624,6 +685,9 @@ function renderSidebarCategories(): void {
|
||||
state.drawerOpen = false;
|
||||
state.selectedId = null;
|
||||
state.selectedItem = null;
|
||||
state.view = 'list';
|
||||
setHash('list');
|
||||
applyShellViewClass();
|
||||
renderSidebarCategories();
|
||||
renderListPane();
|
||||
closeDrawer();
|
||||
@@ -764,6 +828,7 @@ function renderPane(): void {
|
||||
const route = parseHash();
|
||||
// Keep state.view in sync with hash for components that read it
|
||||
state.view = route.view;
|
||||
applyShellViewClass();
|
||||
|
||||
pane.className = 'vault-pane';
|
||||
|
||||
@@ -914,13 +979,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (!state.unlocked) return;
|
||||
|
||||
const route = parseHash();
|
||||
state.view = route.view;
|
||||
applyShellViewClass();
|
||||
|
||||
// If navigating to a detail/edit view for an item we already have loaded
|
||||
if ((route.view === 'detail' || route.view === 'edit') && route.id) {
|
||||
if (state.selectedId === route.id && state.selectedItem) {
|
||||
renderPane();
|
||||
renderSidebarCategories();
|
||||
renderListPane();
|
||||
if (state.view === 'list') renderListPane();
|
||||
return;
|
||||
}
|
||||
// Need to fetch the item
|
||||
@@ -932,7 +999,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
state.selectedId = null;
|
||||
state.selectedItem = null;
|
||||
renderSidebarCategories();
|
||||
renderListPane();
|
||||
if (state.view === 'list') renderListPane();
|
||||
renderPane();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user