feat(ext): shared state host — decouple components from popup.ts
Introduce shared/state.ts as a service-locator so popup components (item-detail, item-form, trash, devices, settings, etc.) work in both the popup and vault tab bundles. Both entry points register themselves as the host; components import from shared/state instead of popup.ts. Vault.ts now delegates to the real popup components, removing ~300 lines of placeholder renderers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,21 @@
|
||||
/// Vault tab entry point — full "desktop-like" sidebar + pane layout.
|
||||
///
|
||||
/// This is a standalone entry point with its own state and renderers.
|
||||
/// Task 4 will wire shared popup components; for now all pane renderers
|
||||
/// are placeholder implementations.
|
||||
/// Registers as the shared state host so popup components (item-detail,
|
||||
/// item-form, trash, devices, settings, etc.) render natively in the
|
||||
/// vault tab's pane area.
|
||||
|
||||
import type { Request, Response } from '../shared/messages';
|
||||
import type {
|
||||
ItemId, ItemType, ManifestEntry, Item, VaultSettings,
|
||||
ItemId, ItemType, ManifestEntry, Item, VaultSettings, GeneratorRequest,
|
||||
} from '../shared/types';
|
||||
import { registerHost } from '../shared/state';
|
||||
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 { renderVaultSettings as renderVaultSettingsView } from '../popup/components/settings-vault';
|
||||
import { renderFieldHistory, teardown as teardownFieldHistory } from '../popup/components/field-history';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -58,7 +66,7 @@ function typeLabel(t: ItemType): string {
|
||||
// Hash routing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type VaultView = 'list' | 'detail' | 'add' | 'edit' | 'trash' | 'devices' | 'settings';
|
||||
type VaultView = 'list' | 'detail' | 'add' | 'edit' | 'trash' | 'devices' | 'settings' | 'settings-vault' | 'field-history';
|
||||
|
||||
interface HashRoute {
|
||||
view: VaultView;
|
||||
@@ -82,6 +90,8 @@ function parseHash(): HashRoute {
|
||||
case 'trash':
|
||||
case 'devices':
|
||||
case 'settings':
|
||||
case 'settings-vault':
|
||||
case 'field-history':
|
||||
return { view };
|
||||
default:
|
||||
return { view: 'list' };
|
||||
@@ -99,26 +109,65 @@ function setHash(view: VaultView, param?: string): void {
|
||||
|
||||
interface VaultState {
|
||||
unlocked: boolean;
|
||||
view: VaultView;
|
||||
entries: Array<[ItemId, ManifestEntry]>;
|
||||
selectedId: ItemId | null;
|
||||
selectedItem: Item | null;
|
||||
selectedIndex: number;
|
||||
searchQuery: string;
|
||||
activeGroup: string | null;
|
||||
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 = {
|
||||
unlocked: false,
|
||||
view: 'list',
|
||||
entries: [],
|
||||
selectedId: null,
|
||||
selectedItem: null,
|
||||
selectedIndex: 0,
|
||||
searchQuery: '',
|
||||
activeGroup: null,
|
||||
vaultSettings: null,
|
||||
generatorDefaults: null,
|
||||
error: null,
|
||||
loading: false,
|
||||
newType: null,
|
||||
capturedTabId: null,
|
||||
capturedUrl: '',
|
||||
historyItemId: null,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register as shared state host
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
registerHost({
|
||||
getState: () => state,
|
||||
setState: (partial: any) => {
|
||||
Object.assign(state, partial);
|
||||
renderPane();
|
||||
},
|
||||
navigate: (view: string, extras?: any) => {
|
||||
Object.assign(state, { view, error: null, loading: false, ...extras });
|
||||
setHash(view as VaultView);
|
||||
renderSidebarList();
|
||||
renderPane();
|
||||
},
|
||||
sendMessage,
|
||||
escapeHtml,
|
||||
popOutToTab: () => {},
|
||||
isInTab: () => true,
|
||||
openVaultTab: () => {},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Render entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -240,6 +289,7 @@ function wireSidebar(): void {
|
||||
if (nav === 'add') {
|
||||
state.selectedId = null;
|
||||
state.selectedItem = null;
|
||||
state.newType = null;
|
||||
setHash('add');
|
||||
renderPane();
|
||||
return;
|
||||
@@ -247,6 +297,7 @@ function wireSidebar(): void {
|
||||
if (nav === 'trash' || nav === 'devices' || nav === 'settings') {
|
||||
state.selectedId = null;
|
||||
state.selectedItem = null;
|
||||
state.newType = null;
|
||||
setHash(nav);
|
||||
renderPane();
|
||||
return;
|
||||
@@ -360,409 +411,66 @@ async function selectItem(id: ItemId): Promise<void> {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pane rendering
|
||||
// Pane rendering — delegates to shared popup components
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function teardownPaneComponents(): void {
|
||||
teardownTrash();
|
||||
teardownDevices();
|
||||
teardownFieldHistory();
|
||||
}
|
||||
|
||||
function renderPane(): void {
|
||||
const pane = document.getElementById('vault-pane');
|
||||
if (!pane) return;
|
||||
|
||||
teardownPaneComponents();
|
||||
|
||||
const route = parseHash();
|
||||
// Keep state.view in sync with hash for components that read it
|
||||
state.view = route.view;
|
||||
|
||||
pane.className = 'vault-pane';
|
||||
|
||||
switch (route.view) {
|
||||
case 'detail':
|
||||
renderDetailPane(pane);
|
||||
if (state.selectedItem) {
|
||||
renderItemDetail(pane);
|
||||
} else {
|
||||
pane.className = 'vault-pane vault-pane--empty';
|
||||
pane.innerHTML = 'select an item';
|
||||
}
|
||||
break;
|
||||
case 'add':
|
||||
renderAddPane(pane, route.type);
|
||||
// Sync newType from hash for the item-form component
|
||||
state.newType = (route.type as ItemType) ?? null;
|
||||
renderItemForm(pane, 'add');
|
||||
break;
|
||||
case 'edit':
|
||||
renderEditPane(pane);
|
||||
renderItemForm(pane, 'edit');
|
||||
break;
|
||||
case 'trash':
|
||||
renderTrashPane(pane);
|
||||
renderTrash(pane);
|
||||
break;
|
||||
case 'devices':
|
||||
renderDevicesPane(pane);
|
||||
renderDevices(pane);
|
||||
break;
|
||||
case 'settings':
|
||||
renderSettingsPane(pane);
|
||||
renderSettings(pane);
|
||||
break;
|
||||
case 'settings-vault':
|
||||
renderVaultSettingsView(pane);
|
||||
break;
|
||||
case 'field-history':
|
||||
renderFieldHistory(pane);
|
||||
break;
|
||||
default:
|
||||
renderEmptyPane(pane);
|
||||
pane.className = 'vault-pane vault-pane--empty';
|
||||
pane.innerHTML = 'select an item';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function renderEmptyPane(pane: HTMLElement): void {
|
||||
pane.className = 'vault-pane vault-pane--empty';
|
||||
pane.innerHTML = 'select an item';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Detail pane (placeholder — Task 4 wires real popup components)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderDetailPane(pane: HTMLElement): void {
|
||||
pane.className = 'vault-pane';
|
||||
const item = state.selectedItem;
|
||||
if (!item) {
|
||||
renderEmptyPane(pane);
|
||||
return;
|
||||
}
|
||||
|
||||
let fieldsHtml = '';
|
||||
|
||||
// Core fields based on type
|
||||
switch (item.core.type) {
|
||||
case 'login': {
|
||||
const c = item.core;
|
||||
if (c.username) fieldsHtml += fieldRow('username', c.username);
|
||||
if (c.password) fieldsHtml += fieldRow('password', '••••••••', true);
|
||||
if (c.url) fieldsHtml += fieldRow('url', c.url);
|
||||
break;
|
||||
}
|
||||
case 'secure_note': {
|
||||
fieldsHtml += fieldRow('body', item.core.body);
|
||||
break;
|
||||
}
|
||||
case 'identity': {
|
||||
const c = item.core;
|
||||
if (c.full_name) fieldsHtml += fieldRow('name', c.full_name);
|
||||
if (c.email) fieldsHtml += fieldRow('email', c.email);
|
||||
if (c.phone) fieldsHtml += fieldRow('phone', c.phone);
|
||||
if (c.address) fieldsHtml += fieldRow('address', c.address);
|
||||
break;
|
||||
}
|
||||
case 'card': {
|
||||
const c = item.core;
|
||||
if (c.number) fieldsHtml += fieldRow('number', '•••• ' + c.number.slice(-4));
|
||||
if (c.holder) fieldsHtml += fieldRow('holder', c.holder);
|
||||
if (c.expiry) fieldsHtml += fieldRow('expiry', `${c.expiry.month}/${c.expiry.year}`);
|
||||
break;
|
||||
}
|
||||
case 'key': {
|
||||
const c = item.core;
|
||||
if (c.label) fieldsHtml += fieldRow('label', c.label);
|
||||
if (c.algorithm) fieldsHtml += fieldRow('algorithm', c.algorithm);
|
||||
fieldsHtml += fieldRow('key', '••••••••', true);
|
||||
break;
|
||||
}
|
||||
case 'document': {
|
||||
const c = item.core;
|
||||
fieldsHtml += fieldRow('filename', c.filename);
|
||||
fieldsHtml += fieldRow('mime', c.mime_type);
|
||||
break;
|
||||
}
|
||||
case 'totp': {
|
||||
const c = item.core;
|
||||
if (c.issuer) fieldsHtml += fieldRow('issuer', c.issuer);
|
||||
if (c.label) fieldsHtml += fieldRow('label', c.label);
|
||||
fieldsHtml += fieldRow('digits', String(c.config.digits));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom sections
|
||||
if (item.sections.length > 0) {
|
||||
for (const section of item.sections) {
|
||||
const sectionName = section.name || '(unnamed section)';
|
||||
fieldsHtml += `<div class="section-header">${escapeHtml(sectionName)}</div>`;
|
||||
for (const field of section.fields) {
|
||||
const val = field.value.kind === 'month_year'
|
||||
? `${(field.value.value as { month: number; year: number }).month}/${(field.value.value as { month: number; year: number }).year}`
|
||||
: String(field.value.value);
|
||||
const hidden = field.hidden_by_default || field.kind === 'password' || field.kind === 'concealed';
|
||||
fieldsHtml += fieldRow(field.label, hidden ? '••••••••' : val, hidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notes
|
||||
if (item.notes) {
|
||||
fieldsHtml += `<div class="section-header">notes</div>`;
|
||||
fieldsHtml += `<div class="field-row"><div class="field-row__label"></div><div class="field-row__value"><pre>${escapeHtml(item.notes)}</pre></div></div>`;
|
||||
}
|
||||
|
||||
const modified = new Date(item.modified * 1000).toLocaleDateString();
|
||||
|
||||
pane.innerHTML = `
|
||||
<div class="detail-header" style="padding:0 0 12px; border-bottom:1px solid #21262d; margin-bottom:16px;">
|
||||
<div>
|
||||
<span style="font-size:18px; margin-right:8px;">${typeIcon(item.type)}</span>
|
||||
<span class="detail-title">${escapeHtml(item.title)}</span>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<button class="btn" id="pane-edit-btn">edit</button>
|
||||
<button class="btn btn-danger" id="pane-delete-btn">delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sig-block sig-block--gold">
|
||||
${fieldsHtml}
|
||||
</div>
|
||||
<div class="muted" style="margin-top:16px;">modified ${escapeHtml(modified)}</div>
|
||||
${item.tags.length > 0 ? `<div class="muted" style="margin-top:4px;">tags: ${item.tags.map(t => escapeHtml(t)).join(', ')}</div>` : ''}
|
||||
`;
|
||||
|
||||
document.getElementById('pane-edit-btn')?.addEventListener('click', () => {
|
||||
setHash('edit', state.selectedId!);
|
||||
renderPane();
|
||||
});
|
||||
|
||||
document.getElementById('pane-delete-btn')?.addEventListener('click', async () => {
|
||||
if (!state.selectedId) return;
|
||||
const resp = await sendMessage({ type: 'delete_item', id: state.selectedId });
|
||||
if (resp.ok) {
|
||||
state.selectedId = null;
|
||||
state.selectedItem = null;
|
||||
await loadManifest();
|
||||
setHash('list');
|
||||
render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fieldRow(label: string, value: string, concealed = false): string {
|
||||
return `
|
||||
<div class="field-row">
|
||||
<div class="field-row__label">${escapeHtml(label)}</div>
|
||||
<div class="field-row__value${concealed ? '' : ''}">${escapeHtml(value)}</div>
|
||||
<div class="field-row__actions">
|
||||
<button onclick="navigator.clipboard.writeText(this.closest('.field-row').querySelector('.field-row__value').textContent.trim())" title="copy">copy</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Add pane (placeholder)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderAddPane(pane: HTMLElement, itemType?: string): void {
|
||||
pane.className = 'vault-pane';
|
||||
|
||||
if (!itemType) {
|
||||
// Show type picker
|
||||
const types: Array<{ type: ItemType; icon: string; label: string }> = [
|
||||
{ type: 'login', icon: '\u{1F511}', label: 'Login' },
|
||||
{ type: 'secure_note', icon: '\u{1F4DD}', label: 'Secure Note' },
|
||||
{ type: 'identity', icon: '\u{1FAAA}', label: 'Identity' },
|
||||
{ type: 'card', icon: '\u{1F4B3}', label: 'Card' },
|
||||
{ type: 'key', icon: '\u{1F5DD}', label: 'Key' },
|
||||
{ type: 'document', icon: '\u{1F4C4}', label: 'Document' },
|
||||
{ type: 'totp', icon: '⏱', label: 'TOTP' },
|
||||
];
|
||||
pane.innerHTML = `
|
||||
<h3 style="margin-bottom:16px; font-size:15px;">new item</h3>
|
||||
<div class="type-select-list">
|
||||
${types.map(t => `
|
||||
<button class="type-select-row" data-type="${t.type}">
|
||||
<span class="type-select-icon">${t.icon}</span>
|
||||
${escapeHtml(t.label)}
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
pane.querySelectorAll('.type-select-row').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const t = (btn as HTMLElement).dataset.type!;
|
||||
setHash('add', t);
|
||||
renderPane();
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Placeholder form — Task 4 will wire real popup components
|
||||
pane.innerHTML = `
|
||||
<h3 style="margin-bottom:16px; font-size:15px;">
|
||||
${typeIcon(itemType as ItemType)} new ${escapeHtml(itemType)}
|
||||
</h3>
|
||||
<p class="muted" style="margin-bottom:16px;">
|
||||
Full form will be wired in Task 4 (shared state host).
|
||||
</p>
|
||||
<div class="form-actions">
|
||||
<button class="btn" id="pane-back-btn">back</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('pane-back-btn')?.addEventListener('click', () => {
|
||||
setHash('list');
|
||||
renderPane();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Edit pane (placeholder)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderEditPane(pane: HTMLElement): void {
|
||||
pane.className = 'vault-pane';
|
||||
const item = state.selectedItem;
|
||||
if (!item) {
|
||||
renderEmptyPane(pane);
|
||||
return;
|
||||
}
|
||||
|
||||
pane.innerHTML = `
|
||||
<h3 style="margin-bottom:16px; font-size:15px;">
|
||||
${typeIcon(item.type)} edit: ${escapeHtml(item.title)}
|
||||
</h3>
|
||||
<p class="muted" style="margin-bottom:16px;">
|
||||
Full edit form will be wired in Task 4 (shared state host).
|
||||
</p>
|
||||
<div class="form-actions">
|
||||
<button class="btn" id="pane-cancel-btn">cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('pane-cancel-btn')?.addEventListener('click', () => {
|
||||
setHash('detail', state.selectedId!);
|
||||
renderPane();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Trash pane (placeholder)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderTrashPane(pane: HTMLElement): void {
|
||||
pane.className = 'vault-pane';
|
||||
|
||||
const trashedEntries = state.entries.filter(
|
||||
([, e]) => e.trashed_at !== undefined && e.trashed_at !== null,
|
||||
);
|
||||
|
||||
pane.innerHTML = `
|
||||
<div class="trash-header">
|
||||
<button class="btn" id="pane-trash-back">←</button>
|
||||
<h3 style="font-size:15px;">\u{1F5D1} trash</h3>
|
||||
</div>
|
||||
${trashedEntries.length === 0
|
||||
? '<div class="empty">trash is empty</div>'
|
||||
: trashedEntries.map(([id, e]) => `
|
||||
<div class="trash-row">
|
||||
<span class="trash-row__icon">${typeIcon(e.type)}</span>
|
||||
<div class="trash-row__info">
|
||||
<span class="trash-row__title">${escapeHtml(e.title)}</span>
|
||||
<span class="trash-row__meta">${e.type}</span>
|
||||
</div>
|
||||
<button class="trash-row__restore" data-id="${escapeHtml(id)}">restore</button>
|
||||
</div>
|
||||
`).join('')
|
||||
}
|
||||
`;
|
||||
|
||||
document.getElementById('pane-trash-back')?.addEventListener('click', () => {
|
||||
setHash('list');
|
||||
renderPane();
|
||||
});
|
||||
|
||||
pane.querySelectorAll('.trash-row__restore').forEach((btn) => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = (btn as HTMLElement).dataset.id!;
|
||||
const resp = await sendMessage({ type: 'restore_item', id });
|
||||
if (resp.ok) {
|
||||
await loadManifest();
|
||||
renderSidebarList();
|
||||
renderTrashPane(pane);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Devices pane (placeholder)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderDevicesPane(pane: HTMLElement): void {
|
||||
pane.className = 'vault-pane';
|
||||
|
||||
pane.innerHTML = `
|
||||
<div class="devices-header">
|
||||
<button class="btn" id="pane-devices-back">←</button>
|
||||
<h3 style="font-size:15px;">\u{1F4F1} devices</h3>
|
||||
</div>
|
||||
<p class="muted">loading devices...</p>
|
||||
`;
|
||||
|
||||
document.getElementById('pane-devices-back')?.addEventListener('click', () => {
|
||||
setHash('list');
|
||||
renderPane();
|
||||
});
|
||||
|
||||
// Fetch and render devices
|
||||
sendMessage({ type: 'list_devices' }).then((resp) => {
|
||||
if (!resp.ok) return;
|
||||
const data = resp.data as { devices: Array<{ name: string; public_key: string; added_at: number }> };
|
||||
const devicesContainer = pane.querySelector('.muted');
|
||||
if (!devicesContainer) return;
|
||||
|
||||
if (data.devices.length === 0) {
|
||||
devicesContainer.outerHTML = '<div class="empty">no devices registered</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
devicesContainer.outerHTML = data.devices.map((d) => `
|
||||
<div class="device-row">
|
||||
<div class="device-row__info">
|
||||
<span class="device-row__name">${escapeHtml(d.name)}</span>
|
||||
<span class="device-row__meta">added ${new Date(d.added_at * 1000).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings pane (placeholder)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderSettingsPane(pane: HTMLElement): void {
|
||||
pane.className = 'vault-pane';
|
||||
|
||||
pane.innerHTML = `
|
||||
<div class="settings-header">
|
||||
<button class="btn" id="pane-settings-back">←</button>
|
||||
<h3 style="font-size:15px;">⚙ vault settings</h3>
|
||||
</div>
|
||||
<p class="muted" style="margin-bottom:16px;">
|
||||
Full settings view will be wired in Task 4 (shared state host).
|
||||
</p>
|
||||
`;
|
||||
|
||||
if (state.vaultSettings) {
|
||||
const vs = state.vaultSettings;
|
||||
const trashRetention = vs.trash_retention.kind === 'forever'
|
||||
? 'forever'
|
||||
: `${(vs.trash_retention as { kind: 'days'; value: number }).value} days`;
|
||||
const historyRetention = vs.field_history_retention.kind === 'forever'
|
||||
? 'forever'
|
||||
: vs.field_history_retention.kind === 'last_n'
|
||||
? `last ${(vs.field_history_retention as { kind: 'last_n'; value: number }).value}`
|
||||
: `${(vs.field_history_retention as { kind: 'days'; value: number }).value} days`;
|
||||
|
||||
pane.innerHTML += `
|
||||
<div class="settings-section">
|
||||
<div class="settings-section__title">retention</div>
|
||||
<div class="settings-row">
|
||||
<span class="settings-row__label">trash</span>
|
||||
<span>${escapeHtml(trashRetention)}</span>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<span class="settings-row__label">history</span>
|
||||
<span>${escapeHtml(historyRetention)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
document.getElementById('pane-settings-back')?.addEventListener('click', () => {
|
||||
setHash('list');
|
||||
renderPane();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data loading
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -778,6 +486,7 @@ async function loadManifest(): Promise<void> {
|
||||
if (vsResp.ok) {
|
||||
const data = vsResp.data as { settings: VaultSettings };
|
||||
state.vaultSettings = data.settings;
|
||||
state.generatorDefaults = data.settings.generator_defaults;
|
||||
}
|
||||
|
||||
// Handle deep link from hash
|
||||
|
||||
Reference in New Issue
Block a user