feat: add options panel embedded in about:addons

This commit is contained in:
golem 2026-08-01 22:35:17 -06:00
parent 6bedd700e8
commit b42b11755c
3 changed files with 270 additions and 0 deletions

127
options/options.css Normal file

@ -0,0 +1,127 @@
:root {
--fg: #15141a;
--fg-dim: #5b5b66;
--bg: transparent;
--border: #8f8f9d;
--field-bg: #ffffff;
--accent: #0060df;
}
@media (prefers-color-scheme: dark) {
:root {
--fg: #fbfbfe;
--fg-dim: #b1b1bd;
--border: #8f8f9d;
--field-bg: #2b2a33;
--accent: #0df;
}
}
body {
margin: 0;
padding: 0.5rem 0;
color: var(--fg);
background: var(--bg);
font: message-box;
font-size: 1rem;
line-height: 1.4;
max-width: 42rem;
}
.field {
margin-bottom: 1.15rem;
}
label {
display: block;
font-weight: 600;
margin-bottom: 0.3rem;
}
.checkbox label {
font-weight: 400;
display: flex;
align-items: baseline;
gap: 0.45rem;
}
input[type="text"],
select {
width: 100%;
max-width: 100%;
box-sizing: border-box;
padding: 0.4rem 0.5rem;
color: var(--fg);
background: var(--field-bg);
border: 1px solid var(--border);
border-radius: 4px;
font: inherit;
}
input[type="text"]:focus-visible,
select:focus-visible,
button:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.hint {
margin: 0.3rem 0 0;
color: var(--fg-dim);
font-size: 0.85rem;
}
.tag {
font-weight: 400;
font-size: 0.75rem;
color: var(--fg-dim);
border: 1px solid var(--border);
border-radius: 3px;
padding: 0 0.3rem;
vertical-align: 1px;
}
.actions {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
margin-top: 1.5rem;
}
button {
padding: 0.35rem 0.8rem;
color: var(--fg);
background: var(--field-bg);
border: 1px solid var(--border);
border-radius: 4px;
font: inherit;
cursor: pointer;
}
#status {
color: var(--fg-dim);
font-size: 0.85rem;
}
.note {
margin-top: 1.75rem;
padding-top: 1rem;
border-top: 1px solid var(--border);
color: var(--fg-dim);
font-size: 0.85rem;
}
code,
kbd {
font-family: monospace;
font-size: 0.9em;
}
/* The inline about:addons surface can be very narrow. */
@media (max-width: 26rem) {
.actions {
align-items: flex-start;
flex-direction: column;
}
}

60
options/options.html Normal file

@ -0,0 +1,60 @@
<!-- Rendered inline inside about:addons. Keep it narrow-friendly and do not
rely on window sizing; a pref can degrade this to a standalone tab. -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Search with ChatGPT — Options</title>
<link rel="stylesheet" href="options.css" />
<form id="settings" autocomplete="off">
<div class="field">
<label for="model">Model <span class="tag">experimental</span></label>
<input type="text" id="model" placeholder="auto" spellcheck="false" />
<p class="hint">
ChatGPT may ignore this and use your account default. Leave blank to
always use your account default.
</p>
</div>
<div class="field checkbox">
<label><input type="checkbox" id="temporaryChat" /> Request a temporary chat</label>
<p class="hint">
ChatGPT may not honour this. Check ChatGPT's own indicator before sending
anything sensitive.
</p>
</div>
<div class="field checkbox">
<label><input type="checkbox" id="webSearch" /> Ask ChatGPT to use web search (hint only)</label>
<p class="hint">A hint to ChatGPT's interface, not a guarantee.</p>
</div>
<div class="field">
<label for="openIn">Open in</label>
<select id="openIn">
<option value="new-tab">A new tab</option>
<option value="background-tab">A new background tab</option>
<option value="new-window">A new window</option>
</select>
</div>
<div class="field">
<label for="promptTemplate">Prompt template</label>
<input type="text" id="promptTemplate" spellcheck="false" />
<p class="hint"><code>{query}</code> is replaced with your selected text.</p>
</div>
<div class="actions">
<button type="button" id="restore">Restore defaults</button>
<span id="status" role="status" aria-live="polite"></span>
</div>
</form>
<p class="note">
Selecting text and choosing <strong>Ask ChatGPT</strong> — or pressing
<kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd> — opens ChatGPT with your text
filled in. ChatGPT no longer submits prompts that arrive from a link, so press
Enter there to send. Change the shortcut under
<em>Manage Extension Shortcuts</em> in Add-ons.
</p>
<script type="module" src="options.js"></script>

83
options/options.js Normal file

@ -0,0 +1,83 @@
import { DEFAULT_SETTINGS } from "../lib/defaults.js";
import { load, save } from "../lib/settings.js";
const SAVE_DEBOUNCE_MS = 400;
const STATUS_CLEAR_MS = 1500;
const field = (id) => document.getElementById(id);
let saveTimer = null;
let statusTimer = null;
// Suppresses the change/input handlers while we are programmatically writing
// values into the form, so a remote sync cannot trigger a save loop.
let applying = false;
function render(settings) {
applying = true;
field("model").value = settings.model;
field("temporaryChat").checked = settings.temporaryChat;
field("webSearch").checked = settings.webSearch;
field("openIn").value = settings.openIn;
field("promptTemplate").value = settings.promptTemplate;
applying = false;
}
function collect() {
return {
model: field("model").value,
temporaryChat: field("temporaryChat").checked,
webSearch: field("webSearch").checked,
openIn: field("openIn").value,
promptTemplate: field("promptTemplate").value,
};
}
function showStatus(message) {
field("status").textContent = message;
clearTimeout(statusTimer);
statusTimer = setTimeout(() => {
field("status").textContent = "";
}, STATUS_CLEAR_MS);
}
async function persist(settings) {
try {
await save(settings);
showStatus("Saved");
} catch {
// storage.sync sanitises its errors, so there is nothing specific to report.
showStatus("Could not save settings");
}
}
// Debounced so typing in a text field cannot approach any write-rate quota.
function scheduleSave() {
if (applying) {
return;
}
clearTimeout(saveTimer);
saveTimer = setTimeout(() => persist(collect()), SAVE_DEBOUNCE_MS);
}
// Module scripts are deferred, so the DOM is already parsed here.
render(await load());
for (const id of Object.keys(DEFAULT_SETTINGS)) {
field(id).addEventListener("input", scheduleSave);
field(id).addEventListener("change", scheduleSave);
}
field("restore").addEventListener("click", () => {
clearTimeout(saveTimer);
render(DEFAULT_SETTINGS);
persist({ ...DEFAULT_SETTINGS });
});
// A remote sync landing while the panel is open must update the controls rather
// than being clobbered by stale field values on the next edit.
browser.storage.onChanged.addListener(async (changes, area) => {
if (area !== "sync") {
return;
}
render(await load());
});