From 6c8ebb3548abd8f9d592cfd79821f0144ec3d27c Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Mon, 27 Apr 2026 15:53:53 -0400 Subject: [PATCH] feat(ext/vault): scaffold vault.html tab with sidebar+pane layout and hash routing Co-Authored-By: Claude Opus 4.6 --- extension/manifest.json | 5 + extension/src/service-worker/index.ts | 6 + extension/src/vault/vault.css | 1308 +++++++++++++++++++++++++ extension/src/vault/vault.html | 12 + extension/src/vault/vault.ts | 848 ++++++++++++++++ extension/webpack.config.js | 3 + 6 files changed, 2182 insertions(+) create mode 100644 extension/src/vault/vault.css create mode 100644 extension/src/vault/vault.html create mode 100644 extension/src/vault/vault.ts diff --git a/extension/manifest.json b/extension/manifest.json index 7eb1630..f9b8da8 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -30,5 +30,10 @@ "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" }, + "commands": { + "open-vault": { + "description": "Open relicario vault" + } + }, "web_accessible_resources": [] } diff --git a/extension/src/service-worker/index.ts b/extension/src/service-worker/index.ts index 84bb225..e3dec89 100644 --- a/extension/src/service-worker/index.ts +++ b/extension/src/service-worker/index.ts @@ -64,6 +64,12 @@ chrome.storage.local.get('session_timeout').then((r) => { } }).catch(() => {}); +chrome.commands.onCommand.addListener((command) => { + if (command === 'open-vault') { + chrome.tabs.create({ url: chrome.runtime.getURL('vault.html') }); + } +}); + chrome.runtime.onMessage.addListener( (request: Request, sender: chrome.runtime.MessageSender, sendResponse: (r: Response) => void) => { (async () => { diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css new file mode 100644 index 0000000..6184035 --- /dev/null +++ b/extension/src/vault/vault.css @@ -0,0 +1,1308 @@ +/* relicario vault — terminal dark theme (tab layout) */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: #0d1117; + color: #c9d1d9; + font-family: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', 'SF Mono', Menlo, monospace; + font-size: 13px; + line-height: 1.5; + margin: 0; + height: 100vh; + overflow: hidden; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 4px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: #30363d; + border-radius: 2px; +} + +/* Typography */ +.brand { + font-size: 16px; + font-weight: 700; + color: #d2ab43; + letter-spacing: 1px; +} + +.brand-logo { + display: block; + width: 48px; + height: 48px; + margin: 0 auto 8px; +} + +.label { + font-size: 11px; + font-weight: 500; + color: #8b949e; + text-transform: lowercase; + letter-spacing: 0.02em; + margin-bottom: 4px; +} + +.label .req { + color: #aa812a; + margin-left: 2px; + font-weight: 600; +} + +.secondary { + color: #8b949e; +} + +.muted { + color: #484f58; + font-size: 11px; +} + +.error { + color: #ab2b20; + font-size: 12px; + margin-top: 8px; +} + +/* Buttons */ +.btn { + display: inline-block; + padding: 6px 14px; + font-family: inherit; + font-size: 12px; + border: 1px solid #30363d; + border-radius: 4px; + background: #21262d; + color: #c9d1d9; + cursor: pointer; + transition: background 0.15s; +} + +.btn:hover { + background: #30363d; +} + +.btn:focus { + outline: 1px solid #d2ab43; + outline-offset: 1px; +} + +.btn-primary { + background: #7c5719; + border-color: #7c5719; + color: #fff; +} + +.btn-primary:hover { + background: #aa812a; +} + +.btn-danger { + background: #791111; + border-color: #791111; + color: #fff; +} + +.btn-danger:hover { + background: #ab2b20; +} + +/* Inputs */ +input, textarea, select { + width: 100%; + padding: 8px 10px; + font-family: inherit; + font-size: 13px; + background: #161b22; + border: 1px solid #30363d; + border-radius: 4px; + color: #c9d1d9; + outline: none; + transition: border-color 0.15s; +} + +input:focus, textarea:focus, select:focus { + border-color: #d2ab43; +} + +input::placeholder, textarea::placeholder { + color: #484f58; +} + +textarea { + resize: vertical; + min-height: 60px; +} + +/* Layout */ +.pad { + padding: 16px; +} + +/* Group tabs */ +.group-tabs { + display: flex; + gap: 2px; + padding: 6px 12px; + background: #0d1117; + border-bottom: 1px solid #21262d; + overflow-x: auto; +} + +.group-tab { + padding: 4px 10px; + font-size: 11px; + border: none; + border-radius: 3px; + background: transparent; + color: #8b949e; + cursor: pointer; + white-space: nowrap; + font-family: inherit; +} + +.group-tab:hover { + color: #c9d1d9; + background: #161b22; +} + +.group-tab.active { + color: #d2ab43; + background: #161b22; +} + +/* Detail view */ +.detail-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + border-bottom: 1px solid #21262d; +} + +.detail-title { + font-size: 15px; + font-weight: 600; + color: #c9d1d9; +} + +.field { + padding: 10px 12px; + border-bottom: 1px solid #21262d; +} + +.field-value { + font-size: 13px; + color: #c9d1d9; + word-break: break-all; + user-select: all; +} + +/* TOTP */ +.totp-code { + font-size: 22px; + font-weight: 700; + color: #3fb950; + letter-spacing: 4px; +} + +.totp-bar { + height: 3px; + background: #21262d; + border-radius: 2px; + margin-top: 6px; + overflow: hidden; +} + +.totp-bar-fill { + height: 100%; + background: #3fb950; + border-radius: 2px; + transition: width 1s linear; +} + +/* Wizard */ +.wizard-step { + padding: 16px; +} + +.wizard-step h3 { + font-size: 14px; + font-weight: 600; + margin-bottom: 12px; + color: #c9d1d9; +} + +.progress-bar { + display: flex; + gap: 4px; + padding: 12px 16px 0; +} + +.progress-bar .step { + flex: 1; + height: 3px; + background: #21262d; + border-radius: 2px; +} + +.progress-bar .step.done { + background: #d2ab43; +} + +.progress-bar .step.current { + background: #aa812a; +} + +/* Spinner */ +.spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid #30363d; + border-top-color: #d2ab43; + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Confirm overlay */ +.confirm-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.confirm-box { + background: #161b22; + border: 1px solid #30363d; + border-radius: 6px; + padding: 20px; + max-width: 280px; + text-align: center; +} + +.confirm-box p { + margin-bottom: 16px; + font-size: 13px; +} + +.confirm-box .btn + .btn { + margin-left: 8px; +} + +/* Empty state */ +.empty { + text-align: center; + padding: 40px 16px; + color: #484f58; + font-size: 13px; +} + +/* Form layout */ +.form-group { + margin-bottom: 12px; +} + +.form-group .label { + display: block; + margin-bottom: 4px; +} + +.form-actions { + display: flex; + gap: 8px; + margin-top: 16px; +} + +.inline-row { + display: flex; + gap: 8px; + align-items: center; +} + +.inline-row input { + flex: 1; +} + +/* Toggle (for host type) */ +.toggle-group { + display: flex; + gap: 0; + border: 1px solid #30363d; + border-radius: 4px; + overflow: hidden; +} + +.toggle-group button { + flex: 1; + padding: 6px 12px; + font-family: inherit; + font-size: 12px; + border: none; + background: #21262d; + color: #8b949e; + cursor: pointer; +} + +.toggle-group button.active { + background: #7c5719; + color: #fff; +} + +/* File upload area */ +.file-drop { + border: 2px dashed #30363d; + border-radius: 6px; + padding: 24px; + text-align: center; + cursor: pointer; + transition: border-color 0.15s; +} + +.file-drop:hover { + border-color: #d2ab43; +} + +.file-drop.has-file { + border-color: #3fb950; + border-style: solid; +} + +/* --- field-row + signature-block helpers --- */ + +.field-row { + display: grid; + grid-template-columns: 90px 1fr auto; + gap: 8px 10px; + align-items: baseline; + padding: 4px 0; + font-size: 12px; +} + +.field-row__label { color: #8b949e; } +.field-row__value { color: #c9d1d9; word-break: break-word; } +.field-row__value.monospace { font-family: "SF Mono", "JetBrains Mono", monospace; } +.field-row__value pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-family: "SF Mono", "JetBrains Mono", monospace; +} +.field-row__actions { + display: flex; + gap: 6px; + font-size: 11px; + color: #8b949e; +} +.field-row__actions button { + background: transparent; + border: 0; + color: inherit; + cursor: pointer; + padding: 0; + font: inherit; +} +.field-row__actions button:hover { color: #c9d1d9; } + +.sig-block { + background: #161b22; + border: 1px solid #30363d; + border-left: 3px solid #7c5719; + border-radius: 5px; + padding: 14px; + margin-bottom: 10px; +} +.sig-block--gold { border-left-color: #7c5719; } +.sig-block--green { border-left-color: #3fb950; } +.sig-block--amber { border-left-color: #d29922; } +.sig-block--red { border-left-color: #ab2b20; } + +/* --- custom-section rendering --- */ +.section-header { + margin-top: 14px; + margin-bottom: 4px; + padding-top: 10px; + border-top: 1px solid #21262d; + color: #8b949e; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; +} +.section-separator { + margin: 10px 0 4px; + border: 0; + border-top: 1px solid #21262d; +} + +/* --- custom-section editor --- */ +.disclosure { + border-top: 1px solid #21262d; + margin-top: 14px; + padding-top: 10px; +} +.disclosure__toggle { + background: transparent; border: 0; color: #d2ab43; + cursor: pointer; font-size: 12px; padding: 0; + font-family: inherit; +} +.disclosure[data-expanded="false"] .disclosure__body { display: none; } + +.section-editor__head { + display: flex; align-items: baseline; gap: 8px; + margin-top: 10px; margin-bottom: 4px; + font-size: 11px; +} +.section-editor__head .name { color: #c9d1d9; font-weight: 600; } +.section-editor__head .name.anon { color: #8b949e; font-style: italic; font-weight: normal; } +.section-editor__head .actions { color: #8b949e; font-size: 10px; margin-left: auto; } +.section-editor__head .actions button { + background: transparent; border: 0; color: inherit; + cursor: pointer; padding: 0; margin-left: 8px; + font: inherit; +} +.section-editor__head .actions button:hover { color: #c9d1d9; } + +.section-editor__field { + display: grid; grid-template-columns: 120px 1fr auto; + gap: 4px; margin-bottom: 4px; font-size: 11px; +} +.section-editor__field input { + background: #0d1117; border: 1px solid #30363d; color: #c9d1d9; + padding: 3px 6px; border-radius: 3px; font: inherit; font-size: 11px; +} +.section-editor__field .delete-field { + background: transparent; border: 0; color: #ab2b20; + cursor: pointer; font-size: 14px; padding: 0 4px; +} +.section-editor__preserved { + font-size: 10px; color: #6e7681; font-style: italic; + padding: 4px 0 4px 6px; +} + +.section-editor__add { + display: flex; gap: 6px; margin-top: 6px; +} +.section-editor__add button { + background: transparent; border: 1px solid #30363d; color: #8b949e; + padding: 2px 10px; border-radius: 3px; cursor: pointer; + font-size: 10px; font-family: inherit; +} +.section-editor__add button:hover { color: #c9d1d9; border-color: #484f58; } + +.disclosure__body .add-section { + margin-top: 12px; background: transparent; + border: 1px dashed #30363d; color: #8b949e; + padding: 6px 10px; border-radius: 4px; cursor: pointer; + width: 100%; font-size: 11px; font-family: inherit; +} +.disclosure__body .add-section:hover { border-color: #484f58; color: #c9d1d9; } + +/* --- generator panel --- */ + +.gen-trigger { + background: #7c5719; + color: #fff3cf; + border: none; + border-radius: 4px; + padding: 0 12px; + font-size: 16px; + cursor: pointer; + line-height: 1; + min-width: 38px; + display: inline-flex; + align-items: center; + justify-content: center; +} +.gen-trigger:hover { background: #aa812a; } +.gen-trigger[aria-expanded="true"] { background: #aa812a; } + +.gen-panel { + background: #161b22; + border: 1px solid #aa812a; + border-radius: 6px; + padding: 11px; + margin: 6px 0; + font-size: 11px; + color: #c9d1d9; +} +.gen-panel .panel-toggle { + display: flex; + gap: 4px; + background: #21262d; + border-radius: 4px; + padding: 2px; + margin-bottom: 8px; +} +.gen-panel .panel-toggle button { + flex: 1; + background: transparent; + border: 0; + color: #8b949e; + padding: 5px; + font-size: 11px; + cursor: pointer; + border-radius: 3px; + font-weight: 600; +} +.gen-panel .panel-toggle button.active { + background: #aa812a; + color: #fff3cf; +} +.gen-panel .knob { + display: flex; + align-items: center; + gap: 8px; + margin: 6px 0; +} +.gen-panel .knob__label { + color: #8b949e; + width: 56px; + flex-shrink: 0; + font-size: 10px; +} +.gen-panel .knob__slider { flex: 1; } +.gen-panel .knob__value { + font-family: ui-monospace, monospace; + min-width: 24px; + text-align: right; + color: #c9d1d9; +} +.gen-panel .classes { + display: flex; + gap: 8px; + font-size: 10px; + margin: 6px 0; + flex-wrap: wrap; + color: #8b949e; +} +.gen-panel .classes label { + display: flex; + align-items: center; + gap: 3px; + user-select: none; + cursor: pointer; +} +.gen-panel .preview { + background: #0d1117; + border: 1px solid #30363d; + border-radius: 4px; + padding: 8px 10px; + margin-top: 8px; + display: flex; + align-items: center; + gap: 8px; +} +.gen-panel .preview__value { + flex: 1; + color: #f1cf6e; + font-family: ui-monospace, monospace; + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.gen-panel .preview__regen { + background: transparent; + border: 0; + color: #8b949e; + cursor: pointer; + padding: 0 4px; + font-size: 14px; +} +.gen-panel .more { + color: #8b949e; + font-size: 10px; + margin-top: 6px; + cursor: pointer; + user-select: none; + padding: 2px 0; +} +.gen-panel .more summary { + list-style: none; + outline: none; +} +.gen-panel .more summary::-webkit-details-marker { display: none; } +.gen-panel .more:hover { color: #d2ab43; } +.gen-panel .more__advanced { margin-top: 6px; } +.gen-panel .actions { + display: flex; + gap: 6px; + margin-top: 10px; + align-items: center; +} +.gen-panel .actions .save-link { + flex: 1; + background: transparent; + border: 0; + color: #8b949e; + cursor: pointer; + font-size: 10px; + text-align: left; + padding: 4px 0; + text-decoration: underline; + text-decoration-color: #30363d; + text-underline-offset: 2px; +} +.gen-panel .actions .save-link:hover { + color: #d2ab43; + text-decoration-color: #d2ab43; +} +.gen-panel .actions .save-link__toast { + color: #3fb950; + margin-left: 6px; + font-size: 10px; +} + +.gen-preview-line { + margin: 0 0 6px; font-size: 11px; color: #c9d1d9; + font-family: "SF Mono", "JetBrains Mono", monospace; +} + +/* --- settings-vault screen --- */ +.settings-header { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; } +.settings-section { + margin-top: 14px; padding-top: 10px; + border-top: 1px solid #21262d; +} +.settings-section__title { + color: #8b949e; font-size: 10px; + text-transform: uppercase; letter-spacing: 0.08em; + margin-bottom: 6px; +} +.settings-row { + display: grid; grid-template-columns: 110px 1fr; + gap: 6px 10px; align-items: center; + margin: 4px 0; font-size: 12px; +} +.settings-row__label { color: #8b949e; } +.settings-row select { + background: #0d1117; border: 1px solid #30363d; color: #c9d1d9; + padding: 3px 8px; border-radius: 3px; font: inherit; font-size: 11px; +} +.ack-row { + display: grid; grid-template-columns: 1fr auto auto; + gap: 8px; align-items: center; + padding: 4px 0; font-size: 11px; + border-bottom: 1px solid #161b22; +} +.ack-row__host { color: #c9d1d9; font-family: monospace; } +.ack-row__meta { color: #6e7681; font-size: 10px; } +.ack-row__revoke { + background: transparent; border: 0; color: #ab2b20; + cursor: pointer; font-size: 10px; +} +.settings-footer { + display: flex; justify-content: flex-end; gap: 6px; + margin-top: 20px; padding-top: 12px; border-top: 1px solid #21262d; +} + +/* --- attachments disclosure --- */ + +.attachments-disclosure { + margin: 8px 0; + border: 1px solid #30363d; + border-radius: 4px; + padding: 6px 8px; + font-size: 11px; + color: #8b949e; +} +.attachments-disclosure[open] { + border-color: #aa812a; +} +.attachments-disclosure summary { + cursor: pointer; + list-style: none; + outline: none; + user-select: none; + padding: 2px 0; +} +.attachments-disclosure summary::-webkit-details-marker { display: none; } +.attachments-disclosure summary:hover { color: #c9d1d9; } +.attachments-disclosure__body { + margin-top: 6px; +} +.attachment-row { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 0; + font-size: 10px; + border-bottom: 1px solid #21262d; +} +.attachment-row:last-of-type { + border-bottom: 0; +} +.attachment-row__icon, +.attachment-row__thumb { + width: 16px; + height: 16px; + background: #21262d; + border-radius: 2px; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + flex-shrink: 0; + overflow: hidden; +} +.attachment-row__thumb img { + width: 100%; + height: 100%; + object-fit: cover; +} +.attachment-row__name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #c9d1d9; +} +.attachment-row__meta { + color: #6e7681; + font-size: 9px; + font-family: ui-monospace, monospace; + flex-shrink: 0; +} +.attachment-row__remove, +.attachment-row__download { + color: #d2ab43; + cursor: pointer; + padding: 0 6px; + flex-shrink: 0; +} +.attachment-row__remove { color: #ab2b20; } +.attachment-add-btn { + background: transparent; + border: 1px dashed #30363d; + color: #8b949e; + padding: 5px 8px; + font-size: 10px; + cursor: pointer; + border-radius: 3px; + width: 100%; + margin-top: 6px; + text-align: center; +} +.attachment-add-btn:hover { + border-color: #aa812a; + color: #c9d1d9; +} + +/* --- Document type signature block + primary picker --- */ + +.document-signature-block { + border-left: 3px solid #aa812a; + background: #161b22; + padding: 10px; + margin: 8px 0; + border-radius: 0 4px 4px 0; + display: flex; + align-items: center; + gap: 10px; +} +.document-signature-block__thumb { + width: 48px; + height: 60px; + border-radius: 2px; + background: linear-gradient(135deg, #b88a30, #7c5719); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + flex-shrink: 0; + overflow: hidden; + color: #fff3cf; +} +.document-signature-block__thumb img { + width: 100%; height: 100%; object-fit: contain; +} +.document-signature-block__info { flex: 1; min-width: 0; } +.document-signature-block__name { + font-size: 11px; + color: #f1cf6e; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.document-signature-block__meta { + font-size: 9px; + color: #8b949e; + font-family: ui-monospace, monospace; + margin-top: 2px; +} +.document-signature-block__actions { + font-size: 9px; + margin-top: 4px; +} +.document-signature-block__preview { + margin-top: 8px; + background: #0d1117; + border: 1px solid #30363d; + border-radius: 3px; + padding: 6px; + text-align: center; +} +.document-signature-block__preview img { + max-width: 100%; + max-height: 200px; + border-radius: 2px; +} + +/* Document primary picker (form mode) */ +.document-primary-row { + background: #161b22; + border: 1px solid #30363d; + border-radius: 4px; + padding: 6px 8px; + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + cursor: pointer; +} +.document-primary-row--empty { + border-style: dashed; + border-color: #aa812a; + color: #8b949e; + justify-content: center; + padding: 10px 8px; +} +.document-primary-row__thumb { + width: 18px; height: 18px; + border-radius: 2px; + background: linear-gradient(135deg, #b88a30, #7c5719); + display: flex; align-items: center; justify-content: center; + font-size: 10px; flex-shrink: 0; +} +.document-primary-row__name { + flex: 1; + color: #c9d1d9; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.document-primary-row__meta { + color: #6e7681; + font-size: 9px; + font-family: ui-monospace, monospace; +} +.document-primary-row__action { + color: #d2ab43; + font-size: 10px; + padding: 0 6px; + cursor: pointer; +} + +/* --- Trash view --- */ + +.trash-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + +.trash-row { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + border-radius: 4px; + background: #161b22; + margin-bottom: 6px; +} + +.trash-row__icon { + font-size: 16px; + flex-shrink: 0; +} + +.trash-row__info { + flex: 1; + min-width: 0; +} + +.trash-row__title { + display: block; + font-size: 13px; + color: #c9d1d9; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.trash-row__meta { + font-size: 11px; + color: #8b949e; +} + +.trash-row__restore { + font-size: 11px; + padding: 4px 8px; + background: #238636; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.trash-row__restore:hover { + background: #2ea043; +} + +.trash-row__restore:disabled { + opacity: 0.5; + cursor: default; +} + +/* --- Devices view --- */ + +.devices-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.device-banner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 10px; + background: #3d1f00; + border: 1px solid #9e6a03; + border-radius: 4px; + margin-bottom: 12px; + font-size: 12px; + color: #f0c674; +} + +.device-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px; + border-radius: 4px; + background: #161b22; + margin-bottom: 6px; +} + +.device-row__info { + flex: 1; + min-width: 0; +} + +.device-row__name { + display: block; + font-size: 13px; + color: #c9d1d9; +} + +.device-row__you { + font-size: 11px; + color: #58a6ff; +} + +.device-row__meta { + font-size: 11px; + color: #8b949e; +} + +.device-row__revoke { + font-size: 11px; + padding: 4px 8px; + background: #da3633; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.device-row__revoke:hover { + background: #f85149; +} + +.device-row__revoke:disabled { + opacity: 0.5; + cursor: default; +} + +/* --- Field history view --- */ + +.history-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + +.history-item-title { + font-size: 14px; + font-weight: 600; + color: #c9d1d9; + margin-bottom: 12px; +} + +.history-field-label { + font-size: 11px; + color: #8b949e; + text-transform: uppercase; + margin: 12px 0 6px; +} + +.history-entry { + display: flex; + align-items: center; + gap: 8px; + padding: 10px; + border-radius: 4px; + background: #161b22; + margin-bottom: 6px; + cursor: pointer; +} + +.history-entry:hover { + background: #1c2128; +} + +.history-entry__value { + flex: 1; + font-family: monospace; + font-size: 13px; +} + +.history-entry__value.masked { + color: #8b949e; +} + +.history-entry__value.revealed { + color: #c9d1d9; +} + +.history-entry__meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + font-size: 11px; + color: #8b949e; +} + +.history-entry__current { + color: #58a6ff; + font-weight: 500; +} + +.history-entry__copy { + background: none; + border: none; + cursor: pointer; + font-size: 14px; + padding: 4px; +} + +.history-entry__copy:hover { + opacity: 0.8; +} + +/* --- Type selection --- */ + +.type-select-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.type-select-row { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + background: #161b22; + border: 1px solid transparent; + border-radius: 6px; + color: #c9d1d9; + font-size: 13px; + cursor: pointer; + text-align: left; +} + +.type-select-row:hover { + background: #21262d; + border-color: #30363d; +} + +.type-select-icon { + font-size: 16px; + width: 20px; + text-align: center; +} + +/* ===================================================== + Vault-specific layout styles (tab UI) + ===================================================== */ + +#vault-app { + display: flex; + height: 100vh; +} + +.vault-sidebar { + width: 260px; + min-width: 260px; + border-right: 1px solid #21262d; + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; +} + +.vault-sidebar__header { + padding: 12px 16px; + border-bottom: 1px solid #21262d; + display: flex; + align-items: center; + gap: 8px; +} + +.vault-sidebar__search { + padding: 8px 12px; + border-bottom: 1px solid #21262d; +} + +.vault-sidebar__search input { + width: 100%; + background: #161b22; + border: 1px solid #30363d; + border-radius: 4px; + color: #c9d1d9; + padding: 6px 10px; + font-size: 12px; + font-family: inherit; +} + +.vault-sidebar__list { + flex: 1; + overflow-y: auto; +} + +.vault-sidebar__nav { + border-top: 1px solid #21262d; + padding: 8px 0; +} + +.vault-sidebar__nav-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 16px; + color: #8b949e; + font-size: 12px; + cursor: pointer; + border: none; + background: none; + width: 100%; + text-align: left; + font-family: inherit; +} + +.vault-sidebar__nav-item:hover { + color: #c9d1d9; + background: #161b22; +} + +.vault-pane { + flex: 1; + overflow-y: auto; + padding: 24px 32px; +} + +.vault-pane--empty { + display: flex; + align-items: center; + justify-content: center; + color: #484f58; + font-size: 14px; +} + +.vault-entry { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + cursor: pointer; + border-left: 2px solid transparent; + font-size: 12px; +} + +.vault-entry:hover { background: #161b22; } + +.vault-entry.selected { + background: #161b22; + border-left-color: #d2ab43; +} + +.vault-entry__title { + color: #c9d1d9; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.vault-entry__meta { + color: #484f58; + font-size: 11px; +} + +.vault-group-header { + padding: 12px 16px 4px; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #484f58; +} + +/* --- Lock screen (vault tab) --- */ + +.vault-lock-screen { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + gap: 16px; +} + +.vault-lock-screen__form { + display: flex; + flex-direction: column; + gap: 12px; + width: 320px; +} + +.vault-lock-screen__form input { + text-align: center; +} diff --git a/extension/src/vault/vault.html b/extension/src/vault/vault.html new file mode 100644 index 0000000..5459fac --- /dev/null +++ b/extension/src/vault/vault.html @@ -0,0 +1,12 @@ + + + + + relicario — vault + + + +
+ + + diff --git a/extension/src/vault/vault.ts b/extension/src/vault/vault.ts new file mode 100644 index 0000000..4d30f8b --- /dev/null +++ b/extension/src/vault/vault.ts @@ -0,0 +1,848 @@ +/// 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. + +import type { Request, Response } from '../shared/messages'; +import type { + ItemId, ItemType, ManifestEntry, Item, VaultSettings, +} from '../shared/types'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function sendMessage(request: Request): Promise { + return new Promise((resolve) => { + chrome.runtime.sendMessage(request, (response: Response) => { + resolve(response); + }); + }); +} + +function escapeHtml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +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 + } +} + +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'; + } +} + +// --------------------------------------------------------------------------- +// Hash routing +// --------------------------------------------------------------------------- + +type VaultView = 'list' | 'detail' | 'add' | 'edit' | 'trash' | 'devices' | 'settings'; + +interface HashRoute { + view: VaultView; + id?: string; + type?: string; +} + +function parseHash(): HashRoute { + const raw = window.location.hash.replace(/^#\/?/, ''); + if (!raw) return { view: 'list' }; + + const parts = raw.split('/'); + const view = parts[0] as VaultView; + + switch (view) { + case 'detail': + case 'edit': + return { view, id: parts[1] }; + case 'add': + return { view, type: parts[1] }; + case 'trash': + case 'devices': + case 'settings': + return { view }; + default: + return { view: 'list' }; + } +} + +function setHash(view: VaultView, param?: string): void { + const fragment = param ? `${view}/${param}` : view; + window.location.hash = fragment === 'list' ? '' : fragment; +} + +// --------------------------------------------------------------------------- +// State +// --------------------------------------------------------------------------- + +interface VaultState { + unlocked: boolean; + entries: Array<[ItemId, ManifestEntry]>; + selectedId: ItemId | null; + selectedItem: Item | null; + searchQuery: string; + vaultSettings: VaultSettings | null; + error: string | null; + loading: boolean; +} + +const state: VaultState = { + unlocked: false, + entries: [], + selectedId: null, + selectedItem: null, + searchQuery: '', + vaultSettings: null, + error: null, + loading: false, +}; + +// --------------------------------------------------------------------------- +// 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 = ` +
+ relicario +
+ + + ${state.error ? `
${escapeHtml(state.error)}
` : ''} +
+
+ `; + + 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 (sidebar + pane) +// --------------------------------------------------------------------------- + +function renderShell(app: HTMLElement): void { + // Only create the shell structure if it's not present yet + if (!app.querySelector('.vault-sidebar')) { + app.innerHTML = ` +
+
+ relicario +
+ +
+
+ + + + + +
+
+
+ select an item +
+ `; + wireSidebar(); + } + + renderSidebarList(); + renderPane(); +} + +// --------------------------------------------------------------------------- +// Sidebar wiring +// --------------------------------------------------------------------------- + +function wireSidebar(): void { + // Search + const searchInput = document.getElementById('vault-search') as HTMLInputElement | null; + searchInput?.addEventListener('input', () => { + state.searchQuery = searchInput.value; + renderSidebarList(); + }); + + // Nav buttons + document.querySelectorAll('.vault-sidebar__nav-item').forEach((btn) => { + btn.addEventListener('click', async () => { + const nav = (btn as HTMLElement).dataset.nav; + if (nav === 'lock') { + await sendMessage({ type: 'lock' }); + state.unlocked = false; + state.selectedId = null; + state.selectedItem = null; + state.entries = []; + render(); + return; + } + if (nav === 'add') { + state.selectedId = null; + state.selectedItem = null; + setHash('add'); + renderPane(); + return; + } + if (nav === 'trash' || nav === 'devices' || nav === 'settings') { + state.selectedId = null; + state.selectedItem = null; + setHash(nav); + renderPane(); + return; + } + }); + }); + + // Global "/" shortcut to focus search + document.addEventListener('keydown', (e) => { + if (e.key === '/' && !isEditableTarget(e.target)) { + e.preventDefault(); + searchInput?.focus(); + } + }); +} + +function isEditableTarget(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) return false; + const tag = target.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true; + if (target.isContentEditable) return true; + return false; +} + +// --------------------------------------------------------------------------- +// Sidebar list +// --------------------------------------------------------------------------- + +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 renderSidebarList(): void { + const container = document.getElementById('vault-sidebar-list'); + if (!container) return; + + const filtered = getFilteredEntries(); + + // Group by type + const groups = new Map>(); + for (const entry of filtered) { + const t = entry[1].type; + if (!groups.has(t)) groups.set(t, []); + groups.get(t)!.push(entry); + } + + if (filtered.length === 0) { + container.innerHTML = '
no items
'; + return; + } + + let html = ''; + // Stable type ordering + const typeOrder: ItemType[] = ['login', 'secure_note', 'identity', 'card', 'key', 'document', 'totp']; + for (const t of typeOrder) { + const items = groups.get(t); + if (!items || items.length === 0) continue; + html += `
${typeIcon(t)} ${escapeHtml(typeLabel(t))}
`; + for (const [id, e] of items) { + const sel = id === state.selectedId ? ' selected' : ''; + const meta = e.icon_hint ? escapeHtml(e.icon_hint) : ''; + html += ` +
+ ${escapeHtml(e.title)} + ${meta ? `` : ''} +
+ `; + } + } + + container.innerHTML = html; + + // Wire clicks + container.querySelectorAll('.vault-entry').forEach((el) => { + el.addEventListener('click', async () => { + const id = (el as HTMLElement).dataset.id!; + await selectItem(id); + }); + }); +} + +async function selectItem(id: ItemId): Promise { + state.loading = true; + const resp = await sendMessage({ type: 'get_item', id }); + if (resp.ok) { + const data = resp.data as { item: Item }; + state.selectedId = id; + state.selectedItem = data.item; + state.loading = false; + setHash('detail', id); + renderSidebarList(); + renderPane(); + } else { + state.loading = false; + state.error = (resp as { error: string }).error; + } +} + +// --------------------------------------------------------------------------- +// Pane rendering +// --------------------------------------------------------------------------- + +function renderPane(): void { + const pane = document.getElementById('vault-pane'); + if (!pane) return; + + const route = parseHash(); + + switch (route.view) { + case 'detail': + renderDetailPane(pane); + break; + case 'add': + renderAddPane(pane, route.type); + break; + case 'edit': + renderEditPane(pane); + break; + case 'trash': + renderTrashPane(pane); + break; + case 'devices': + renderDevicesPane(pane); + break; + case 'settings': + renderSettingsPane(pane); + break; + default: + renderEmptyPane(pane); + 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 += `
${escapeHtml(sectionName)}
`; + 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 += `
notes
`; + fieldsHtml += `
${escapeHtml(item.notes)}
`; + } + + const modified = new Date(item.modified * 1000).toLocaleDateString(); + + pane.innerHTML = ` +
+
+ ${typeIcon(item.type)} + ${escapeHtml(item.title)} +
+
+ + +
+
+
+ ${fieldsHtml} +
+
modified ${escapeHtml(modified)}
+ ${item.tags.length > 0 ? `
tags: ${item.tags.map(t => escapeHtml(t)).join(', ')}
` : ''} + `; + + 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 ` +
+
${escapeHtml(label)}
+
${escapeHtml(value)}
+
+ +
+
+ `; +} + +// --------------------------------------------------------------------------- +// 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 = ` +

new item

+
+ ${types.map(t => ` + + `).join('')} +
+ `; + 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 = ` +

+ ${typeIcon(itemType as ItemType)} new ${escapeHtml(itemType)} +

+

+ Full form will be wired in Task 4 (shared state host). +

+
+ +
+ `; + + 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 = ` +

+ ${typeIcon(item.type)} edit: ${escapeHtml(item.title)} +

+

+ Full edit form will be wired in Task 4 (shared state host). +

+
+ +
+ `; + + 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 = ` +
+ +

\u{1F5D1} trash

+
+ ${trashedEntries.length === 0 + ? '
trash is empty
' + : trashedEntries.map(([id, e]) => ` +
+ ${typeIcon(e.type)} +
+ ${escapeHtml(e.title)} + ${e.type} +
+ +
+ `).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 = ` +
+ +

\u{1F4F1} devices

+
+

loading devices...

+ `; + + 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 = '
no devices registered
'; + return; + } + + devicesContainer.outerHTML = data.devices.map((d) => ` +
+
+ ${escapeHtml(d.name)} + added ${new Date(d.added_at * 1000).toLocaleDateString()} +
+
+ `).join(''); + }); +} + +// --------------------------------------------------------------------------- +// Settings pane (placeholder) +// --------------------------------------------------------------------------- + +function renderSettingsPane(pane: HTMLElement): void { + pane.className = 'vault-pane'; + + pane.innerHTML = ` +
+ +

⚙ vault settings

+
+

+ Full settings view will be wired in Task 4 (shared state host). +

+ `; + + 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 += ` +
+
retention
+
+ trash + ${escapeHtml(trashRetention)} +
+
+ history + ${escapeHtml(historyRetention)} +
+
+ `; + } + + document.getElementById('pane-settings-back')?.addEventListener('click', () => { + setHash('list'); + renderPane(); + }); +} + +// --------------------------------------------------------------------------- +// Data loading +// --------------------------------------------------------------------------- + +async function loadManifest(): Promise { + const listResp = await sendMessage({ type: 'list_items' }); + if (listResp.ok) { + const data = listResp.data as { items: Array<[ItemId, ManifestEntry]> }; + state.entries = data.items; + } + + const vsResp = await sendMessage({ type: 'get_vault_settings' }); + if (vsResp.ok) { + const data = vsResp.data as { settings: VaultSettings }; + state.vaultSettings = data.settings; + } + + // Handle deep link from hash + const route = parseHash(); + if (route.view === 'detail' && route.id) { + const itemResp = await sendMessage({ type: 'get_item', id: route.id }); + if (itemResp.ok) { + const data = itemResp.data as { item: Item }; + state.selectedId = route.id; + state.selectedItem = data.item; + } + } +} + +// --------------------------------------------------------------------------- +// Init +// --------------------------------------------------------------------------- + +document.addEventListener('DOMContentLoaded', async () => { + // Check if already unlocked + const resp = await sendMessage({ type: 'is_unlocked' }); + if (resp.ok) { + const data = resp.data as { unlocked: boolean }; + if (data.unlocked) { + state.unlocked = true; + await loadManifest(); + } + } + + render(); + + // Session expired listener + 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 + window.addEventListener('hashchange', () => { + if (!state.unlocked) return; + + const route = parseHash(); + + // 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(); + renderSidebarList(); + return; + } + // Need to fetch the item + selectItem(route.id); + return; + } + + // For non-item views, just re-render the pane + state.selectedId = null; + state.selectedItem = null; + renderSidebarList(); + renderPane(); + }); +}); diff --git a/extension/webpack.config.js b/extension/webpack.config.js index 4eee474..e1a2e6d 100644 --- a/extension/webpack.config.js +++ b/extension/webpack.config.js @@ -7,6 +7,7 @@ module.exports = { popup: './src/popup/popup.ts', content: './src/content/detector.ts', setup: './src/setup/setup.ts', + vault: './src/vault/vault.ts', }, output: { path: path.resolve(__dirname, 'dist'), @@ -27,6 +28,8 @@ module.exports = { { from: 'src/popup/styles.css', to: 'styles.css' }, { from: 'setup.html', to: '.' }, { from: 'icons', to: 'icons' }, + { from: 'src/vault/vault.html', to: 'vault.html' }, + { from: 'src/vault/vault.css', to: 'vault.css' }, { from: 'wasm/relicario_wasm_bg.wasm', to: '.' }, { from: 'wasm/relicario_wasm.js', to: '.' }, ],