-
-
-
new item
+
+
+ New item
- ${isInTab() ? '' : ''}
+ ${isInTab() ? '' : ''}
- ${isInTab() ? '
esc to cancel
' : '
'}
-
+
${TYPE_OPTIONS.map((opt) => `
-
+
Esc back
`;
document.getElementById('back-btn')?.addEventListener('click', () => navigate('list'));
document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') navigate('list');
+ }, { once: true });
document.querySelectorAll
('[data-type]').forEach((btn) => {
btn.addEventListener('click', () => {
const type = btn.dataset.type as ItemType;
setState({ newType: type });
- if (type === 'login' || type === 'secure_note') {
- renderItemForm(app, 'add');
- } else {
- popOutToTab();
- }
+ renderItemForm(app, 'add');
});
});
}
diff --git a/extension/src/popup/components/item-list.ts b/extension/src/popup/components/item-list.ts
index b7058fe..ae15f9d 100644
--- a/extension/src/popup/components/item-list.ts
+++ b/extension/src/popup/components/item-list.ts
@@ -3,6 +3,13 @@
/// to the detail view.
import { getState, setState, sendMessage, navigate, escapeHtml, openVaultTab } from '../../shared/state';
+import { showToast } from '../../shared/toast';
+import {
+ GLYPH_VAULT_TAB,
+ GLYPH_DEVICES, GLYPH_LOCK,
+ 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 type { ItemId, ItemType, ManifestEntry, Item } from '../../shared/types';
/// Extract the display hostname from an icon_hint or fallback to the first tag.
@@ -12,30 +19,46 @@ function metaLine(e: ManifestEntry): string {
return '';
}
-/// Emoji icon per item type. Placeholder until we ship real SVG icons.
+/// Glyph icon per item type.
function typeIcon(t: ItemType): string {
switch (t) {
- case 'login': return 'π';
- case 'secure_note': return 'π';
- case 'identity': return 'πͺͺ';
- case 'card': return 'π³';
- case 'key': return 'π';
- case 'document': return 'π';
- case 'totp': return 'β±';
+ 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 buildRowsHtml(): string {
const state = getState();
const filtered = getFilteredEntries();
- return filtered.length > 0
- ? filtered.map(([id, e], i) => `
+ if (filtered.length > 0) {
+ return filtered.map(([id, e], i) => `
- ${typeIcon(e.type)} ${escapeHtml(e.title)}${e.attachment_summaries.length > 0 ? ' π' : ''}
+ ${typeIcon(e.type)} ${escapeHtml(e.title)}${e.attachment_summaries.length > 0 ? ' β' : ''}
${escapeHtml(metaLine(e))}
- `).join('')
- : 'no items
';
+ `).join('');
+ }
+ if (state.searchQuery) {
+ return `
+
+
β
+
No results for "${escapeHtml(state.searchQuery)}"
+
Try a shorter search term.
+
+ `;
+ }
+ return `
+
+
β
+
No items yet
+
Press + to add your first item.
+
+ `;
}
function updateItemList(): void {
@@ -66,7 +89,7 @@ export function renderItemList(app: HTMLElement): void {
+ new
sync
- ⤴
+ ${GLYPH_VAULT_TAB}
settings
lock
@@ -108,11 +131,14 @@ export function renderItemList(app: HTMLElement): void {
if (listResp.ok) {
const data = listResp.data as { items: Array<[ItemId, ManifestEntry]> };
setState({ entries: data.items, loading: false });
+ showToast('Synced', 'success');
return;
}
setState({ loading: false, error: listResp.error });
+ showToast(listResp.error ?? 'Sync failed', 'error');
} else {
setState({ loading: false, error: resp.error });
+ showToast(resp.error ?? 'Sync failed', 'error');
}
});
@@ -253,8 +279,8 @@ function handleListKeydown(e: KeyboardEvent): void {
// ----------------------------------------------------------------------
const SETTINGS_OPTIONS: Array<{ view: 'settings' | 'settings-vault'; icon: string; label: string }> = [
- { view: 'settings', icon: 'π₯', label: 'device settings' },
- { view: 'settings-vault', icon: 'π', label: 'vault settings' },
+ { view: 'settings', icon: GLYPH_DEVICES, label: 'device settings' },
+ { view: 'settings-vault', icon: GLYPH_LOCK, label: 'vault settings' },
];
function showSettingsPicker(anchor: HTMLElement): void {
diff --git a/extension/src/popup/components/settings.ts b/extension/src/popup/components/settings.ts
index 2cea82d..ab4b60c 100644
--- a/extension/src/popup/components/settings.ts
+++ b/extension/src/popup/components/settings.ts
@@ -2,7 +2,7 @@
import { sendMessage, navigate, escapeHtml } from '../../shared/state';
import type { DeviceSettings } from '../../shared/types';
-import { GLYPH_TRASH, GLYPH_DEVICES } from '../../shared/glyphs';
+import { GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SYNC } from '../../shared/glyphs';
import {
loadColorScheme, saveColorScheme, resetColorScheme,
DEFAULT_DIGIT_COLOR, DEFAULT_SYMBOL_COLOR,
@@ -63,7 +63,7 @@ export async function renderSettings(app: HTMLElement): Promise
{
${GLYPH_TRASH} trash
${GLYPH_DEVICES} devices
-
π€ Sync now
+
${GLYPH_SYNC} Sync now
@@ -189,3 +189,8 @@ async function renderDisplaySection(): Promise {
updateSwatch(swatch, DEFAULT_DIGIT_COLOR, DEFAULT_SYMBOL_COLOR);
});
}
+
+// DEV-B interface contract stub β will be replaced with real teardown logic at merge time
+export function teardownSettings(): void {
+ // no-op stub
+}
diff --git a/extension/src/popup/components/trash.ts b/extension/src/popup/components/trash.ts
index 63b5b2d..27c32a5 100644
--- a/extension/src/popup/components/trash.ts
+++ b/extension/src/popup/components/trash.ts
@@ -2,10 +2,19 @@
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../shared/state';
import type { ItemId, ManifestEntry, VaultSettings } from '../../shared/types';
+import {
+ GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_CARD,
+ GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT, GLYPH_TYPE_TOTP,
+} from '../../shared/glyphs';
const TYPE_ICONS: Record = {
- login: 'π', secure_note: 'π', identity: 'π€', card: 'π³',
- key: 'π', document: 'π', totp: 'β±οΈ',
+ login: GLYPH_TYPE_LOGIN,
+ secure_note: GLYPH_TYPE_SECURE_NOTE,
+ identity: GLYPH_TYPE_IDENTITY,
+ card: GLYPH_TYPE_CARD,
+ key: GLYPH_TYPE_KEY,
+ document: GLYPH_TYPE_DOCUMENT,
+ totp: GLYPH_TYPE_TOTP,
};
function relativeTime(unixSec: number): string {
@@ -64,7 +73,7 @@ export async function renderTrash(app: HTMLElement): Promise {
? `Trash is empty
`
: items.map(([id, entry]) => `
-
${TYPE_ICONS[entry.type] ?? 'π¦'}
+
${TYPE_ICONS[entry.type] ?? 'β»'}
${escapeHtml(entry.title)}
trashed ${relativeTime(entry.trashed_at ?? 0)}
diff --git a/extension/src/popup/components/types/document.ts b/extension/src/popup/components/types/document.ts
index ba4fdab..a60cf5e 100644
--- a/extension/src/popup/components/types/document.ts
+++ b/extension/src/popup/components/types/document.ts
@@ -4,7 +4,7 @@
import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state';
import { renderFormHeader } from '../form-header';
-import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
+import { REQUIRED_PILL_HTML, GLYPH_TYPE_DOCUMENT, GLYPH_PREVIEW } from '../../../shared/glyphs';
import type { Item, ItemId, ManifestEntry, Section, AttachmentRef } from '../../../shared/types';
import {
renderSectionsEditor, wireSectionsEditor,
@@ -76,7 +76,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
}
return `
-
π
+
${GLYPH_TYPE_DOCUMENT}
${escapeHtml(primaryRef.filename)}
${formatBytes(primaryRef.size)}
β change
@@ -283,13 +283,13 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise
${escapeHtml(item.title)}
-
π
+
${GLYPH_TYPE_DOCUMENT}
${escapeHtml(primaryRef.filename)}
${formatBytes(primaryRef.size)} Β· ${new Date(primaryRef.created * 1000).toISOString().slice(0, 10)}
β download
- ${isImageMime ? 'π preview' : ''}
+ ${isImageMime ? `${GLYPH_PREVIEW} preview` : ''}
diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css
index 09c3509..ee28b08 100644
--- a/extension/src/popup/styles.css
+++ b/extension/src/popup/styles.css
@@ -1608,3 +1608,94 @@ textarea {
margin-top: 8px;
background: var(--bg-input);
}
+
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 40px 20px;
+ text-align: center;
+}
+
+.empty-state__icon {
+ font-size: 28px;
+ color: var(--text-muted, #8b949e);
+ margin-bottom: 12px;
+ display: block;
+}
+
+.empty-state__title {
+ font-size: 13px;
+ font-weight: 600;
+ margin-bottom: 4px;
+}
+
+.empty-state__hint {
+ font-size: 11px;
+ color: var(--text-muted, #8b949e);
+}
+
+.type-card-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+ margin-bottom: 12px;
+}
+
+.type-card {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ padding: 10px 12px;
+ background: var(--bg-elevated, #161b22);
+ border: 1px solid var(--border-mid, #30363d);
+ border-radius: 6px;
+ cursor: pointer;
+ text-align: left;
+ transition: border-color 0.15s;
+}
+
+.type-card:hover { border-color: var(--gold-base, #a88a4a); }
+
+.type-card__icon { font-size: 20px; margin-bottom: 4px; }
+.type-card__label { font-size: 12px; font-weight: 600; }
+
+/* Toast notifications */
+.relicario-toast-container {
+ position: fixed;
+ bottom: 16px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ pointer-events: none;
+ z-index: 9999;
+}
+
+.vault-shell .relicario-toast-container {
+ left: auto;
+ right: 24px;
+ transform: none;
+}
+
+.relicario-toast {
+ padding: 8px 16px;
+ border-radius: 6px;
+ font-size: 12px;
+ opacity: 0;
+ transform: translateY(8px);
+ transition: opacity 0.2s, transform 0.2s;
+ pointer-events: none;
+}
+
+.relicario-toast--visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.relicario-toast--success { background: #1f4a24; color: #aff0b5; border: 1px solid #238636; }
+.relicario-toast--error { background: #4a1f1f; color: #f0afaf; border: 1px solid #ab2b20; }
+.relicario-toast--info { background: #1f2d4a; color: #afc8f0; border: 1px solid #1f6feb; }
+.type-card__desc { font-size: 10px; color: var(--text-muted, #8b949e); margin-top: 2px; }
diff --git a/extension/src/shared/__tests__/glyphs.test.ts b/extension/src/shared/__tests__/glyphs.test.ts
index 957c28c..312251c 100644
--- a/extension/src/shared/__tests__/glyphs.test.ts
+++ b/extension/src/shared/__tests__/glyphs.test.ts
@@ -41,3 +41,30 @@ describe('glyph constants', () => {
expect(GLYPH_NEXT).toBe('βΈ');
});
});
+
+describe('Stream A glyphs (vault tab + type icons)', () => {
+ it('exports GLYPH_VAULT_TAB as U+29C9', () => {
+ expect(glyphs.GLYPH_VAULT_TAB).toBe('β§');
+ });
+
+ it('exports per-type glyph constants', () => {
+ expect(glyphs.GLYPH_TYPE_LOGIN).toBe('β');
+ expect(glyphs.GLYPH_TYPE_SECURE_NOTE).toBe('β«');
+ expect(glyphs.GLYPH_TYPE_TOTP).toBe('β‘');
+ expect(glyphs.GLYPH_TYPE_CARD).toBe('β');
+ expect(glyphs.GLYPH_TYPE_IDENTITY).toBe('β¬');
+ expect(glyphs.GLYPH_TYPE_KEY).toBe('βΉ');
+ expect(glyphs.GLYPH_TYPE_DOCUMENT).toBe('β‘');
+ });
+
+ it('per-type glyphs are single codepoints (no emoji)', () => {
+ const typeGlyphs = [
+ glyphs.GLYPH_TYPE_LOGIN, glyphs.GLYPH_TYPE_SECURE_NOTE, glyphs.GLYPH_TYPE_TOTP,
+ glyphs.GLYPH_TYPE_CARD, glyphs.GLYPH_TYPE_IDENTITY, glyphs.GLYPH_TYPE_KEY,
+ glyphs.GLYPH_TYPE_DOCUMENT,
+ ];
+ for (const g of typeGlyphs) {
+ expect([...g].length).toBe(1);
+ }
+ });
+});
diff --git a/extension/src/shared/glyphs.ts b/extension/src/shared/glyphs.ts
index a69e3fb..71a23a7 100644
--- a/extension/src/shared/glyphs.ts
+++ b/extension/src/shared/glyphs.ts
@@ -17,6 +17,19 @@ export const GLYPH_DEVICES = 'β¬'; // sidebar devices nav
export const GLYPH_SETTINGS = 'β'; // sidebar settings nav
export const GLYPH_LOCK = 'β»'; // sidebar lock nav
export const GLYPH_NEXT = 'βΈ'; // forward / next button (matches βΎ/βΈ disclosure family)
+export const GLYPH_COPY = 'β'; // copy to clipboard
+export const GLYPH_SYNC = 'β
'; // sync / upload
+export const GLYPH_PREVIEW = 'β'; // preview / expand
+
+export const GLYPH_VAULT_TAB = 'β§'; // U+29C9 pop-out to fullscreen vault tab
+
+export const GLYPH_TYPE_LOGIN = 'β'; // login
+export const GLYPH_TYPE_SECURE_NOTE = 'β«'; // secure note
+export const GLYPH_TYPE_TOTP = 'β‘'; // totp / 2FA
+export const GLYPH_TYPE_CARD = 'β'; // card
+export const GLYPH_TYPE_IDENTITY = 'β¬'; // identity
+export const GLYPH_TYPE_KEY = 'βΉ'; // SSH / API key
+export const GLYPH_TYPE_DOCUMENT = 'β‘'; // document
/// Inline HTML snippet for the required-field pill. Use after a label's text:
/// ``
diff --git a/extension/src/shared/toast.ts b/extension/src/shared/toast.ts
new file mode 100644
index 0000000..943ea09
--- /dev/null
+++ b/extension/src/shared/toast.ts
@@ -0,0 +1,26 @@
+export function showToast(
+ message: string,
+ type: 'success' | 'error' | 'info' = 'info',
+ durationMs = 2500,
+): void {
+ let container = document.querySelector('.relicario-toast-container');
+ if (!container) {
+ container = document.createElement('div');
+ container.className = 'relicario-toast-container';
+ document.body.appendChild(container);
+ }
+
+ const toast = document.createElement('div');
+ toast.className = `relicario-toast relicario-toast--${type}`;
+ toast.textContent = message;
+ container.appendChild(toast);
+
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => toast.classList.add('relicario-toast--visible'));
+ });
+
+ setTimeout(() => {
+ toast.classList.remove('relicario-toast--visible');
+ toast.addEventListener('transitionend', () => toast.remove(), { once: true });
+ }, durationMs);
+}
diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css
index c1d1639..8d9f994 100644
--- a/extension/src/vault/vault.css
+++ b/extension/src/vault/vault.css
@@ -1396,6 +1396,281 @@ textarea {
color: #484f58;
}
+/* === 3-column shell === */
+.vault-shell {
+ display: flex;
+ height: 100vh;
+ overflow: hidden;
+ background: var(--bg-page, #0d1117);
+}
+
+.vault-sidebar {
+ width: 200px;
+ min-width: 200px;
+ display: flex;
+ flex-direction: column;
+ border-right: 1px solid var(--border, #30363d);
+ background: var(--bg-sidebar, #0d1117);
+ overflow-y: auto;
+ flex-shrink: 0;
+}
+
+.vault-list-pane {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ min-width: 0;
+}
+
+.vault-drawer {
+ width: 440px;
+ min-width: 440px;
+ max-width: 440px;
+ border-left: 1px solid var(--border, #30363d);
+ background: var(--bg-elevated, #161b22);
+ overflow-y: auto;
+ transform: translateX(100%);
+ transition: transform 0.2s ease;
+ flex-shrink: 0;
+}
+
+.vault-drawer--open {
+ transform: translateX(0);
+}
+
+.vault-list-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 16px;
+ cursor: pointer;
+ border-bottom: 1px solid var(--border-subtle, #21262d);
+ transition: background 0.1s;
+}
+
+.vault-list-row:hover { background: var(--bg-hover, #161b22); }
+
+.vault-list-row--selected {
+ background: var(--bg-selected, #1c2d41);
+ border-left: 2px solid var(--gold, #b8860b);
+}
+
+.vault-list-row__icon {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-elevated, #161b22);
+ border-radius: 6px;
+ border: 1px solid var(--border, #30363d);
+ font-size: 14px;
+ flex-shrink: 0;
+}
+
+.vault-list-row--selected .vault-list-row__icon { border-color: var(--gold, #b8860b); }
+
+.vault-list-row__text { flex: 1; min-width: 0; }
+
+.vault-list-row__title {
+ font-size: 13px;
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.vault-list-row__subtitle {
+ font-size: 11px;
+ color: var(--text-muted, #8b949e);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-top: 1px;
+}
+
+.vault-list-row__age {
+ font-size: 10px;
+ color: var(--text-dim, #6e7681);
+ flex-shrink: 0;
+}
+
+/* Bottom sheet */
+.vault-bottom-sheet-scrim {
+ position: absolute;
+ inset: 0 0 0 200px;
+ background: rgba(0,0,0,0.5);
+ opacity: 0;
+ transition: opacity 0.2s;
+ pointer-events: none;
+ z-index: 100;
+}
+
+.vault-bottom-sheet-scrim--visible {
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.vault-bottom-sheet {
+ position: absolute;
+ bottom: 0;
+ left: 200px;
+ right: 0;
+ background: var(--bg-elevated, #161b22);
+ border-top: 1px solid var(--border, #30363d);
+ border-radius: 12px 12px 0 0;
+ padding: 16px 24px 24px;
+ transform: translateY(100%);
+ transition: transform 0.25s ease;
+ z-index: 101;
+ max-height: 60vh;
+ overflow-y: auto;
+}
+
+.vault-bottom-sheet--open { transform: translateY(0); }
+
+.vault-bottom-sheet__handle {
+ width: 40px;
+ height: 4px;
+ background: var(--border, #30363d);
+ border-radius: 2px;
+ margin: 0 auto 16px;
+}
+
+.vault-bottom-sheet__title {
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 16px;
+ text-align: center;
+}
+
+.vault-type-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+ gap: 10px;
+}
+
+.vault-type-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 12px 8px;
+ background: var(--bg-page, #0d1117);
+ border: 1px solid var(--border, #30363d);
+ border-radius: 8px;
+ cursor: pointer;
+ transition: border-color 0.15s;
+ gap: 6px;
+}
+
+.vault-type-card:hover { border-color: var(--gold, #b8860b); }
+
+.vault-type-card__icon { font-size: 28px; }
+.vault-type-card__name { font-size: 11px; color: var(--text-muted, #8b949e); }
+
+/* Drawer header and body */
+.vault-drawer__header {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border, #30363d);
+ gap: 8px;
+}
+
+.vault-drawer__type-pill {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ padding: 2px 8px;
+ background: var(--bg-page, #0d1117);
+ border: 1px solid var(--border, #30363d);
+ border-radius: 4px;
+ color: var(--text-muted, #8b949e);
+}
+
+.vault-drawer__actions { display: flex; gap: 6px; margin-left: auto; }
+
+.vault-drawer__close {
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ font-size: 16px;
+ color: var(--text-muted, #8b949e);
+ padding: 4px 6px;
+}
+
+.vault-drawer__body { padding: 20px 20px 16px; }
+
+.vault-drawer__title { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
+.vault-drawer__subtitle { font-size: 12px; color: var(--text-muted, #8b949e); margin-bottom: 16px; }
+
+.vault-drawer__field-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+}
+
+.vault-drawer__field-grid > .vault-drawer__field--full { grid-column: 1 / -1; }
+
+.vault-drawer__field-label {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--text-muted, #8b949e);
+ margin-bottom: 2px;
+}
+
+.vault-drawer__field-value {
+ font-size: 13px;
+ word-break: break-all;
+}
+
+/* Category nav */
+.vault-category-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ padding: 6px 12px;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ color: inherit;
+ font-size: 13px;
+ text-align: left;
+}
+
+.vault-category-row:hover { background: var(--bg-hover, #161b22); }
+.vault-category-row--active { background: var(--bg-selected, #1c2d41); }
+.vault-category-row__icon { font-size: 14px; flex-shrink: 0; }
+.vault-category-row__label { flex: 1; }
+.vault-category-row__count { font-size: 11px; color: var(--text-muted, #8b949e); }
+
+/* === Responsive === */
+@media (max-width: 960px) {
+ .vault-drawer {
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 100%;
+ }
+}
+
+@media (max-width: 720px) {
+ .vault-sidebar {
+ width: 48px;
+ min-width: 48px;
+ }
+ .vault-sidebar__category-label,
+ .vault-sidebar__category-count,
+ .vault-sidebar__nav-label {
+ display: none;
+ }
+ .vault-sidebar__nav-item { justify-content: center; padding: 10px 0; }
+}
+
/* --- Lock screen (vault tab) --- */
.vault-lock-screen {
@@ -1719,3 +1994,41 @@ textarea {
background: linear-gradient(to top, rgba(17, 22, 30, 0.7), transparent);
pointer-events: none;
}
+
+/* Toast notifications */
+.relicario-toast-container {
+ position: fixed;
+ bottom: 16px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ pointer-events: none;
+ z-index: 9999;
+}
+
+.vault-shell .relicario-toast-container {
+ left: auto;
+ right: 24px;
+ transform: none;
+}
+
+.relicario-toast {
+ padding: 8px 16px;
+ border-radius: 6px;
+ font-size: 12px;
+ opacity: 0;
+ transform: translateY(8px);
+ transition: opacity 0.2s, transform 0.2s;
+ pointer-events: none;
+}
+
+.relicario-toast--visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.relicario-toast--success { background: #1f4a24; color: #aff0b5; border: 1px solid #238636; }
+.relicario-toast--error { background: #4a1f1f; color: #f0afaf; border: 1px solid #ab2b20; }
+.relicario-toast--info { background: #1f2d4a; color: #afc8f0; border: 1px solid #1f6feb; }
diff --git a/extension/src/vault/vault.ts b/extension/src/vault/vault.ts
index ba91bc6..69feb7c 100644
--- a/extension/src/vault/vault.ts
+++ b/extension/src/vault/vault.ts
@@ -10,18 +10,36 @@ import type {
} from '../shared/types';
import { registerHost } from '../shared/state';
import { lookupErrorCopy, type ErrorCta } from '../shared/error-copy';
-import { GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK } from '../shared/glyphs';
+import {
+ GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK,
+ 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 { renderItemForm } from '../popup/components/item-form';
import { renderTrash, teardown as teardownTrash } from '../popup/components/trash';
import { renderDevices, teardown as teardownDevices } from '../popup/components/devices';
-import { renderSettings } from '../popup/components/settings';
+import { renderSettings, teardownSettings } from '../popup/components/settings';
import { renderVaultSettings as renderVaultSettingsView } from '../popup/components/settings-vault';
import { renderFieldHistory, teardown as teardownFieldHistory } from '../popup/components/field-history';
import { renderBackupPanel, teardown as teardownBackup } from './components/backup-panel';
import { renderImportPanel, teardown as teardownImport } from './components/import-panel';
import { applyColorScheme } from '../shared/color-scheme';
+// ---------------------------------------------------------------------------
+// Bottom sheet type picker
+// ---------------------------------------------------------------------------
+
+const BOTTOM_SHEET_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' },
+];
+
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@@ -60,26 +78,35 @@ function renderErrorBlock(code: string | null | undefined): string {
function typeIcon(t: ItemType): string {
switch (t) {
- case 'login': return '\u{1F511}'; // key
- case 'secure_note': return '\u{1F4DD}'; // memo
- case 'identity': return '\u{1FAAA}'; // id card
- case 'card': return '\u{1F4B3}'; // credit card
- case 'key': return '\u{1F5DD}'; // old key
- case 'document': return '\u{1F4C4}'; // page facing up
- case 'totp': return 'β±'; // stopwatch
+ 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 {
- switch (t) {
- case 'login': return 'Logins';
- case 'secure_note': return 'Secure Notes';
- case 'identity': return 'Identities';
- case 'card': return 'Cards';
- case 'key': return 'Keys';
- case 'document': return 'Documents';
- case 'totp': return 'TOTP';
- }
+ const labels: Record = {
+ login: 'Login',
+ secure_note: 'Secure Note',
+ identity: 'Identity',
+ card: 'Card',
+ key: 'SSH / API Key',
+ document: 'Document',
+ totp: 'TOTP',
+ };
+ return labels[t];
+}
+
+function relativeTime(unixSec: number): string {
+ const diffS = Math.floor(Date.now() / 1000) - unixSec;
+ if (diffS < 60) return 'just now';
+ if (diffS < 3600) return `${Math.floor(diffS / 60)}m ago`;
+ if (diffS < 86400) return `${Math.floor(diffS / 3600)}h ago`;
+ return `${Math.floor(diffS / 86400)}d ago`;
}
// ---------------------------------------------------------------------------
@@ -138,6 +165,8 @@ interface VaultState {
selectedIndex: number;
searchQuery: string;
activeGroup: string | null;
+ drawerOpen: boolean;
+ bottomSheetOpen: boolean;
vaultSettings: VaultSettings | null;
generatorDefaults: GeneratorRequest | null;
error: string | null;
@@ -157,6 +186,8 @@ const state: VaultState = {
selectedIndex: 0,
searchQuery: '',
activeGroup: null,
+ drawerOpen: false,
+ bottomSheetOpen: false,
vaultSettings: null,
generatorDefaults: null,
error: null,
@@ -180,7 +211,8 @@ registerHost({
navigate: (view: string, extras?: any) => {
Object.assign(state, { view, error: null, loading: false, ...extras });
setHash(view as VaultView);
- renderSidebarList();
+ renderSidebarCategories();
+ renderListPane();
renderPane();
},
sendMessage,
@@ -249,39 +281,220 @@ function renderLockScreen(app: HTMLElement): void {
}
// ---------------------------------------------------------------------------
-// Shell (sidebar + pane)
+// Shell (3-column: sidebar + list pane + drawer)
// ---------------------------------------------------------------------------
function renderShell(app: HTMLElement): void {
- // Only create the shell structure if it's not present yet
- if (!app.querySelector('.vault-sidebar')) {
+ if (!app.querySelector('.vault-shell')) {
app.innerHTML = `
-