diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 146e72a..6a35990 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -10,6 +10,7 @@ --bg-page: #0d1117; --bg-pane: #161b22; --bg-elevated: #21262d; + --bg-input: #161b22; --border-subtle: #30363d; /* Text */ @@ -1332,3 +1333,32 @@ textarea { padding: 1px 5px; border-radius: 3px; } + +/* Glyph button used by smart-input affordances. Sits inline with an input. */ +.glyph-btn { + min-width: 28px; + height: 28px; + padding: 0 6px; + background: var(--bg-input); + border: 1px solid var(--border-subtle); + border-radius: 3px; + color: var(--text-muted); + font-family: inherit; + font-size: 14px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} +.glyph-btn:hover:not(:disabled) { + border-color: var(--accent); + color: var(--accent); +} +.glyph-btn:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} +.glyph-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} diff --git a/extension/src/shared/form-affordances/__tests__/url-tools.test.ts b/extension/src/shared/form-affordances/__tests__/url-tools.test.ts new file mode 100644 index 0000000..7794f84 --- /dev/null +++ b/extension/src/shared/form-affordances/__tests__/url-tools.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { wireFillFromTab } from '../url-tools'; + +describe('wireFillFromTab', () => { + let form: HTMLElement; + let sendMessage: ReturnType; + + beforeEach(() => { + form = document.createElement('div'); + form.innerHTML = ` + +
+ + +
+ `; + document.body.appendChild(form); + sendMessage = vi.fn(); + }); + + it('fills url + title from active tab on click', async () => { + sendMessage.mockResolvedValue({ ok: true, data: { url: 'https://github.com/login', title: 'GitHub' } }); + wireFillFromTab(form, { sendMessage }); + (form.querySelector('#fill-from-tab-btn') as HTMLButtonElement).click(); + await Promise.resolve(); await Promise.resolve(); + expect((form.querySelector('#f-url') as HTMLInputElement).value).toBe('https://github.com/login'); + expect((form.querySelector('#f-title') as HTMLInputElement).value).toBe('GitHub'); + }); + + it('does not overwrite a non-empty title', async () => { + (form.querySelector('#f-title') as HTMLInputElement).value = 'My GitHub'; + sendMessage.mockResolvedValue({ ok: true, data: { url: 'https://github.com/login', title: 'GitHub' } }); + wireFillFromTab(form, { sendMessage }); + (form.querySelector('#fill-from-tab-btn') as HTMLButtonElement).click(); + await Promise.resolve(); await Promise.resolve(); + expect((form.querySelector('#f-title') as HTMLInputElement).value).toBe('My GitHub'); + }); + + it('disables the button if SW returns null', async () => { + sendMessage.mockResolvedValue({ ok: true, data: null }); + wireFillFromTab(form, { sendMessage }); + (form.querySelector('#fill-from-tab-btn') as HTMLButtonElement).click(); + await Promise.resolve(); await Promise.resolve(); + expect((form.querySelector('#fill-from-tab-btn') as HTMLButtonElement).disabled).toBe(true); + }); +}); diff --git a/extension/src/shared/form-affordances/url-tools.ts b/extension/src/shared/form-affordances/url-tools.ts new file mode 100644 index 0000000..93e8d13 --- /dev/null +++ b/extension/src/shared/form-affordances/url-tools.ts @@ -0,0 +1,24 @@ +import { GLYPH_FILL_FROM_TAB } from '../glyphs'; + +export interface FillFromTabOpts { + sendMessage: (msg: { type: 'get_active_tab_url' }) => Promise<{ ok: boolean; data?: { url: string; title: string } | null }>; +} + +export function wireFillFromTab(form: HTMLElement, opts: FillFromTabOpts): void { + const btn = form.querySelector('#fill-from-tab-btn'); + if (!btn) return; + btn.addEventListener('click', async () => { + const resp = await opts.sendMessage({ type: 'get_active_tab_url' }); + if (!resp.ok || !resp.data) { + btn.disabled = true; + btn.title = 'no active tab'; + return; + } + const urlEl = form.querySelector('#f-url'); + const titleEl = form.querySelector('#f-title'); + if (urlEl) urlEl.value = resp.data.url; + if (titleEl && !titleEl.value.trim()) titleEl.value = resp.data.title; + }); +} + +export const FILL_FROM_TAB_BTN_HTML = ``; diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 5340a73..02763eb 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -10,6 +10,7 @@ --bg-page: #0d1117; --bg-pane: #161b22; --bg-elevated: #21262d; + --bg-input: #161b22; --border-subtle: #30363d; /* Text */ @@ -1362,3 +1363,32 @@ textarea { .vault-lock-screen__form input { text-align: center; } + +/* Glyph button used by smart-input affordances. Sits inline with an input. */ +.glyph-btn { + min-width: 28px; + height: 28px; + padding: 0 6px; + background: var(--bg-input); + border: 1px solid var(--border-subtle); + border-radius: 3px; + color: var(--text-muted); + font-family: inherit; + font-size: 14px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} +.glyph-btn:hover:not(:disabled) { + border-color: var(--accent); + color: var(--accent); +} +.glyph-btn:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} +.glyph-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +}