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:
adlee-was-taken
2026-05-05 17:49:16 -04:00
parent cf66bd97b7
commit 29146439bb
4 changed files with 294 additions and 103 deletions

View File

@@ -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();
});
});