diff --git a/popup/popup.css b/popup/popup.css new file mode 100644 index 0000000..2055fa6 --- /dev/null +++ b/popup/popup.css @@ -0,0 +1,214 @@ +@font-face { + font-family: "Vazirmatn"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("../fonts/Vazirmatn-Regular.woff2") format("woff2"); +} +@font-face { + font-family: "Vazirmatn"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("../fonts/Vazirmatn-Bold.woff2") format("woff2"); +} + +:root { + color-scheme: light; + --paper: #f3efe5; + --paper-deep: #e7dfd0; + --ink: #172628; + --muted: #6f7772; + --line: #d4ccbd; + --teal: #176f68; + --teal-dark: #0e514d; + --saffron: #e5a82c; + --danger: #9d4b3f; + --white: #fffdf8; +} + +* { box-sizing: border-box; } + +body { + width: 348px; + margin: 0; + color: var(--ink); + background: + linear-gradient(rgba(23, 38, 40, .025) 1px, transparent 1px), + var(--paper); + background-size: 100% 4px; + font-family: "Vazirmatn", "Segoe UI", sans-serif; + -webkit-font-smoothing: antialiased; +} + +.masthead { + position: relative; + display: flex; + align-items: center; + min-height: 82px; + padding: 16px 18px; + overflow: hidden; + color: var(--white); + background: var(--ink); +} + +.masthead::after { + content: ""; + position: absolute; + right: -24px; + bottom: -48px; + width: 118px; + height: 118px; + border: 18px solid rgba(229, 168, 44, .16); + border-radius: 50%; +} + +.brand-mark { + position: relative; + z-index: 1; + display: grid; + place-items: center; + width: 47px; + height: 47px; + margin-right: 13px; + border: 1px solid rgba(255, 255, 255, .28); + border-radius: 4px 16px 4px 4px; + color: var(--ink); + background: var(--saffron); + font-size: 27px; + font-weight: 700; + line-height: 1; + box-shadow: 4px 4px 0 rgba(255, 255, 255, .09); +} + +.brand-copy { position: relative; z-index: 1; } +.brand-copy h1 { margin: 2px 0 0; font-size: 20px; line-height: 1; letter-spacing: -.25px; } +.eyebrow, .section-label { + margin: 0; + color: var(--muted); + font-size: 9px; + font-weight: 700; + letter-spacing: 1.25px; + text-transform: uppercase; +} +.masthead .eyebrow { color: #aab6b1; } +.edition { + position: relative; + z-index: 1; + align-self: flex-start; + margin-left: auto; + color: #8d9b96; + font: 700 10px/1 "Courier New", monospace; +} + +main { padding: 14px; } + +.site-panel { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 59px; + padding: 10px 12px; + border: 1px solid var(--line); + border-left: 3px solid var(--saffron); + background: rgba(255, 253, 248, .62); +} +.site-copy { min-width: 0; } +.site-host { + display: block; + max-width: 184px; + margin-top: 3px; + overflow: hidden; + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; + direction: ltr; +} +.site-status { + flex: 0 0 auto; + max-width: 118px; + margin: 0; + color: var(--teal-dark); + font-size: 10px; + font-weight: 700; + line-height: 1.35; + text-align: right; +} +.site-status::before { + content: ""; + display: inline-block; + width: 7px; + height: 7px; + margin-right: 5px; + border-radius: 50%; + background: currentColor; +} +.site-status[data-tone="paused"] { color: #86650e; } +.site-status[data-tone="error"] { color: var(--danger); } + +.settings-card, .master-card { + margin-top: 11px; + border: 1px solid var(--line); + background: var(--white); + box-shadow: 0 5px 18px rgba(44, 51, 45, .055); +} +.master-card { border-color: #b8c7be; } + +.setting-row { + display: flex; + align-items: center; + gap: 12px; + min-height: 61px; + padding: 10px 12px; + cursor: pointer; + transition: background-color 140ms ease, opacity 140ms ease; +} +.setting-row + .setting-row { border-top: 1px solid #e9e4da; } +.setting-row:hover { background: #f8f5ee; } +.setting-row.is-disabled { cursor: default; opacity: .48; } +.setting-copy { flex: 1; min-width: 0; } +.setting-copy strong { display: block; font-size: 12.5px; line-height: 1.3; } +.setting-copy small { display: block; margin-top: 3px; color: var(--muted); font-size: 10.5px; line-height: 1.35; } +.master-row { min-height: 54px; } +.master-row .section-label { margin-bottom: 2px; } + +.switch { position: relative; flex: 0 0 auto; width: 40px; height: 23px; } +.switch input { position: absolute; width: 1px; height: 1px; opacity: 0; } +.switch-track { + position: absolute; + inset: 0; + border: 1px solid #9ca49f; + border-radius: 3px 12px 12px 3px; + background: var(--paper-deep); + transition: background-color 160ms ease, border-color 160ms ease; +} +.switch-track::after { + content: ""; + position: absolute; + top: 3px; + left: 3px; + width: 15px; + height: 15px; + border-radius: 2px 7px 7px 2px; + background: var(--white); + box-shadow: 0 1px 3px rgba(23, 38, 40, .28); + transition: transform 160ms ease; +} +.switch input:checked + .switch-track { border-color: var(--teal-dark); background: var(--teal); } +.switch input:checked + .switch-track::after { transform: translateX(17px); } +.switch input:focus-visible + .switch-track { outline: 3px solid rgba(229, 168, 44, .55); outline-offset: 3px; } +.switch input:disabled + .switch-track { filter: grayscale(.6); } + +.privacy-note { + margin: 13px 0 1px; + color: var(--muted); + font-size: 10px; + font-weight: 700; + letter-spacing: .2px; + text-align: center; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { transition-duration: .01ms !important; } +} diff --git a/popup/popup.html b/popup/popup.html new file mode 100644 index 0000000..d2b8050 --- /dev/null +++ b/popup/popup.html @@ -0,0 +1,92 @@ + + + + + + Persian RTL Fixer + + + +
+ +
+

Persian reading tools

+

RTL Fixer

+
+ 01 +
+ +
+
+
+ + Loading… +
+

Checking status…

+
+ +
+ + + + + + + +
+ +
+ +
+ +

Runs locally · Never rewrites your text

+
+ + + + diff --git a/popup/popup.js b/popup/popup.js new file mode 100644 index 0000000..6722003 --- /dev/null +++ b/popup/popup.js @@ -0,0 +1,118 @@ +(() => { + "use strict"; + + const DEFAULTS = { + enabled: true, + fixDirection: true, + useFont: true, + fixInputs: true, + disabledSites: [] + }; + + const els = { + host: document.getElementById("host"), + status: document.getElementById("siteStatus"), + thisSite: document.getElementById("thisSite"), + fixDirection: document.getElementById("fixDirection"), + useFont: document.getElementById("useFont"), + fixInputs: document.getElementById("fixInputs"), + enabled: document.getElementById("enabled") + }; + + let hostname = ""; + let supportedPage = false; + let storageFailed = false; + let state = { ...DEFAULTS }; + + function pageInfo(url) { + try { + const parsed = new URL(url); + const supported = (parsed.protocol === "http:" || parsed.protocol === "https:") && !!parsed.hostname; + return { hostname: supported ? parsed.hostname : "", supported }; + } catch (error) { + return { hostname: "", supported: false }; + } + } + + function setDisabled(el, disabled) { + el.disabled = disabled; + const row = el.closest(".setting-row"); + if (row) row.classList.toggle("is-disabled", disabled); + } + + function setStatus(text, tone) { + els.status.textContent = text; + els.status.dataset.tone = tone; + } + + function render() { + const siteDisabled = hostname ? state.disabledSites.includes(hostname) : false; + const masterOff = !state.enabled; + + els.host.textContent = supportedPage ? hostname : "Unavailable"; + els.enabled.checked = !!state.enabled; + els.thisSite.checked = supportedPage && !siteDisabled; + els.fixDirection.checked = !!state.fixDirection; + els.useFont.checked = !!state.useFont; + els.fixInputs.checked = !!state.fixInputs; + + setDisabled(els.thisSite, storageFailed || masterOff || !supportedPage); + setDisabled(els.fixDirection, storageFailed || masterOff); + setDisabled(els.useFont, storageFailed || masterOff); + setDisabled(els.fixInputs, storageFailed || masterOff); + + if (storageFailed) setStatus("Settings unavailable", "error"); + else if (!supportedPage) setStatus("This page cannot be modified", "error"); + else if (masterOff) setStatus("Extension paused everywhere", "paused"); + else if (siteDisabled) setStatus("Paused on this site", "paused"); + else setStatus("Persian correction active", "active"); + } + + function save(patch) { + Object.assign(state, patch); + chrome.storage.local.set(patch, () => { + if (chrome.runtime.lastError) storageFailed = true; + render(); + }); + render(); + } + + function init() { + chrome.storage.local.get(DEFAULTS, (stored) => { + if (chrome.runtime.lastError) storageFailed = true; + else state = { ...DEFAULTS, ...stored }; + + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + const info = pageInfo(tabs && tabs[0] ? tabs[0].url || "" : ""); + hostname = info.hostname; + supportedPage = info.supported; + render(); + }); + }); + } + + els.enabled.addEventListener("change", (event) => save({ enabled: event.target.checked })); + els.fixDirection.addEventListener("change", (event) => save({ fixDirection: event.target.checked })); + els.useFont.addEventListener("change", (event) => save({ useFont: event.target.checked })); + els.fixInputs.addEventListener("change", (event) => save({ fixInputs: event.target.checked })); + els.thisSite.addEventListener("change", (event) => { + if (!hostname) return; + const disabledSites = state.disabledSites.filter((site) => site !== hostname); + if (!event.target.checked) disabledSites.push(hostname); + save({ disabledSites }); + }); + + chrome.storage.onChanged.addListener((changes, area) => { + if (area !== "local") return; + let changed = false; + for (const key of Object.keys(DEFAULTS)) { + if (changes[key]) { + state[key] = changes[key].newValue; + changed = true; + } + } + if (changed) render(); + }); + + init(); +})();