Import source: popup/popup

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:07:29 +03:30
parent 5daceff61c
commit 1d2fbd36c6
3 changed files with 424 additions and 0 deletions
+214
View File
@@ -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; }
}
+92
View File
@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Persian RTL Fixer</title>
<link rel="stylesheet" href="popup.css" />
</head>
<body>
<header class="masthead">
<div class="brand-mark" aria-hidden="true">ف</div>
<div class="brand-copy">
<p class="eyebrow">Persian reading tools</p>
<h1>RTL Fixer</h1>
</div>
<span class="edition">01</span>
</header>
<main>
<section class="site-panel" aria-labelledby="site-heading">
<div class="site-copy">
<span id="site-heading" class="section-label">Current site</span>
<strong id="host" class="site-host">Loading…</strong>
</div>
<p id="siteStatus" class="site-status" role="status" aria-live="polite">Checking status…</p>
</section>
<section class="settings-card" aria-label="Site and correction settings">
<label class="setting-row" for="thisSite">
<span class="setting-copy">
<strong>Enable on this site</strong>
<small>Pause corrections for this domain</small>
</span>
<span class="switch">
<input type="checkbox" id="thisSite" />
<span class="switch-track" aria-hidden="true"></span>
</span>
</label>
<label class="setting-row" for="fixDirection">
<span class="setting-copy">
<strong>Conservative RTL correction</strong>
<small>Fix clearly Persian text without flipping layouts</small>
</span>
<span class="switch">
<input type="checkbox" id="fixDirection" />
<span class="switch-track" aria-hidden="true"></span>
</span>
</label>
<label class="setting-row" for="useFont">
<span class="setting-copy">
<strong>Vazirmatn for unstyled text</strong>
<small>Preserve every font chosen by the website</small>
</span>
<span class="switch">
<input type="checkbox" id="useFont" />
<span class="switch-track" aria-hidden="true"></span>
</span>
</label>
<label class="setting-row" for="fixInputs">
<span class="setting-copy">
<strong>Smart input direction</strong>
<small>Adapt text fields as you type Persian</small>
</span>
<span class="switch">
<input type="checkbox" id="fixInputs" />
<span class="switch-track" aria-hidden="true"></span>
</span>
</label>
</section>
<section class="master-card" aria-label="Global extension setting">
<label class="setting-row master-row" for="enabled">
<span class="setting-copy">
<span class="section-label">Everywhere</span>
<strong>Master switch</strong>
</span>
<span class="switch">
<input type="checkbox" id="enabled" />
<span class="switch-track" aria-hidden="true"></span>
</span>
</label>
</section>
<p class="privacy-note">Runs locally · Never rewrites your text</p>
</main>
<script src="popup.js"></script>
</body>
</html>
+118
View File
@@ -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();
})();