diff --git a/docs-site/dist/app.js b/docs-site/dist/app.js
index ed45ec3..67b4713 100644
--- a/docs-site/dist/app.js
+++ b/docs-site/dist/app.js
@@ -44,8 +44,16 @@ function statusBadge(statusValue) {
[STATUS_MODEL.FAILED]: 'border-rose-500/40 bg-rose-500/10 text-rose-300',
[STATUS_MODEL.UNTESTED]: 'border-slate-500/40 bg-slate-500/10 text-slate-300'
};
- const label = String(statusValue || STATUS_MODEL.UNTESTED).replace(/-/g, ' ');
- return `${label} `;
+ const labelsFa = {
+ [STATUS_MODEL.OK]: 'سالم',
+ [STATUS_MODEL.SLOW]: 'کند',
+ [STATUS_MODEL.BLOCKED_BY_BROWSER]: 'محدودیت مرورگر',
+ [STATUS_MODEL.FAILED]: 'ناموفق',
+ [STATUS_MODEL.UNTESTED]: 'بررسینشده'
+ };
+ const rawLabel = String(statusValue || STATUS_MODEL.UNTESTED);
+ const label = labelsFa[rawLabel] || labelsFa[STATUS_MODEL.UNTESTED];
+ return `${label} `;
}
function getRegionBadge(raw = {}) {
@@ -54,9 +62,9 @@ function getRegionBadge(raw = {}) {
const alpha2 = region.length === 2 ? region.toLowerCase() : '';
const allowedFlags = new Set(['ir', 'jp', 'fr', 'cn', 'nl', 'us', 'ru', 'de', 'gb']);
if (alpha2 && allowedFlags.has(alpha2)) {
- return ` ${alpha2.toUpperCase()} `;
+ return ` ${alpha2.toUpperCase()} `;
}
- return `${region} `;
+ return `${region} `;
}
function normalizeMirrorData(rawData) { /* unchanged logic */
@@ -110,7 +118,7 @@ function renderSummary() { const rows = state.results; const okOrSlow = rows.fil
function renderFilters() { byId('filters').innerHTML = ['all', ...state.ecosystems].map((category) => { const icon = getEcosystemIcon(category); return `${icon ? ` ` : ''}${category} `; }).join(''); byId('filters').querySelectorAll('button').forEach((btn) => { btn.onclick = () => { state.activeCategory = btn.dataset.cat; renderRows(); }; }); }
-function renderRows() { const filtered = state.results.filter((r) => state.activeCategory === 'all' || r.ecosystem === state.activeCategory); byId('latencyRows').innerHTML = filtered.map((r) => { const icon = getEcosystemIcon(r.ecosystem); const region = getRegionBadge(r.raw); return `${icon ? ` ` : ''}${r.name}
${r.ecosystem}
${r.url}
${statusBadge(r.status)}${Number.isFinite(r.latency) ? `
${r.latency}ms
` : ''}${region ? `
${region}
` : ''}
`; }).join(''); }
+function renderRows() { const filtered = state.results.filter((r) => state.activeCategory === 'all' || r.ecosystem === state.activeCategory); byId('latencyRows').innerHTML = filtered.map((r) => { const icon = getEcosystemIcon(r.ecosystem); const region = getRegionBadge(r.raw); return `${icon ? ` ` : ''}${r.name}
${r.ecosystem}
${r.url}
${statusBadge(r.status)}${Number.isFinite(r.latency) ? `
${r.latency}ms
` : ''}${region ? `
${region}
` : ''}
`; }).join(''); }
function optimizeDockerfile(input) { const lines = input.split(/\r?\n/); const out = []; const inserted = []; let baseImage = null; for (const line of lines) { if (!baseImage && /^\s*FROM\s+(.+)/i.test(line)) baseImage = line.match(/^\s*FROM\s+(.+)/i)[1].trim(); out.push(line);} if (baseImage) { const argLines = ['ARG NPM_CONFIG_REGISTRY', 'ARG PIP_INDEX_URL', 'ARG APT_UBUNTU_MIRROR', 'ARG APT_DEBIAN_MIRROR']; const firstFrom = out.findIndex((l) => /^\s*FROM\s+/i.test(l)); argLines.forEach((arg) => { if (!out.some((l) => l.trim() === arg)) { out.splice(firstFrom + 1, 0, arg); inserted.push(arg); } }); } return { text: out.join('\n'), summary: { baseImage: baseImage || 'not detected', insertedArgs: inserted, warnings: baseImage ? [] : ['No FROM line detected; no ARG lines inserted.'] } }; }
diff --git a/docs-site/dist/assets/fonts/handjet/Handjet-VariableFont_ELGR,ELSH,wght.ttf b/docs-site/dist/assets/fonts/handjet/Handjet-VariableFont.ttf
similarity index 100%
rename from docs-site/dist/assets/fonts/handjet/Handjet-VariableFont_ELGR,ELSH,wght.ttf
rename to docs-site/dist/assets/fonts/handjet/Handjet-VariableFont.ttf
diff --git a/docs-site/dist/assets/icons/maven.svg b/docs-site/dist/assets/icons/maven.svg
index 9dbf073..a04b2aa 100644
--- a/docs-site/dist/assets/icons/maven.svg
+++ b/docs-site/dist/assets/icons/maven.svg
@@ -4,179 +4,179 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
\ No newline at end of file
diff --git a/docs-site/dist/assets/tailwind.css b/docs-site/dist/assets/tailwind.css
index e1c9d5e..c8800af 100644
--- a/docs-site/dist/assets/tailwind.css
+++ b/docs-site/dist/assets/tailwind.css
@@ -1 +1 @@
-*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Vazirmatn,Inter,system-ui,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{color-scheme:dark}body{--tw-bg-opacity:1;background-color:rgb(2 6 23/var(--tw-bg-opacity,1));font-family:Vazirmatn,Inter,system-ui,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity,1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.font-brand{font-family:Handjet,ui-sans-serif,system-ui,sans-serif}.sticky{position:sticky}.top-2{top:.5rem}.z-20{z-index:20}.mx-auto{margin-left:auto;margin-right:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-4{height:1rem}.h-44{height:11rem}.h-48{height:12rem}.max-h-80{max-height:20rem}.w-4{width:1rem}.w-full{width:100%}.min-w-0{min-width:0}.max-w-7xl{max-width:80rem}.shrink-0{flex-shrink:0}.cursor-pointer{cursor:pointer}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-dashed{border-style:dashed}.border-amber-500\/40{border-color:rgba(245,158,11,.4)}.border-cyan-500\/40{border-color:rgba(6,182,212,.4)}.border-emerald-500\/40{border-color:rgba(16,185,129,.4)}.border-indigo-500\/40{border-color:rgba(99,102,241,.4)}.border-primary{--tw-border-opacity:1;border-color:rgb(34 211 238/var(--tw-border-opacity,1))}.border-primary\/70{border-color:rgba(34,211,238,.7)}.border-rose-500\/40{border-color:rgba(244,63,94,.4)}.border-rose-500\/50{border-color:rgba(244,63,94,.5)}.border-slate-500\/40{border-color:rgba(100,116,139,.4)}.border-slate-700{--tw-border-opacity:1;border-color:rgb(51 65 85/var(--tw-border-opacity,1))}.border-slate-800{--tw-border-opacity:1;border-color:rgb(30 41 59/var(--tw-border-opacity,1))}.border-slate-800\/90{border-color:rgba(30,41,59,.9)}.border-warning\/60{border-color:rgba(245,158,11,.6)}.bg-amber-500\/10{background-color:rgba(245,158,11,.1)}.bg-cyan-500\/10{background-color:rgba(6,182,212,.1)}.bg-emerald-500\/10{background-color:rgba(16,185,129,.1)}.bg-indigo-500\/10{background-color:rgba(99,102,241,.1)}.bg-primary{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity,1))}.bg-primary\/15{background-color:rgba(34,211,238,.15)}.bg-primary\/20{background-color:rgba(34,211,238,.2)}.bg-rose-500\/10{background-color:rgba(244,63,94,.1)}.bg-slate-500\/10{background-color:rgba(100,116,139,.1)}.bg-slate-700{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity,1))}.bg-slate-900\/90{background-color:rgba(15,23,42,.9)}.bg-slate-950{--tw-bg-opacity:1;background-color:rgb(2 6 23/var(--tw-bg-opacity,1))}.bg-success{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-surface\/70{background-color:rgba(15,23,42,.7)}.bg-warning\/10{background-color:rgba(245,158,11,.1)}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[11px\]{font-size:11px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-none{line-height:1}.tracking-wide{letter-spacing:.025em}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.text-indigo-300{--tw-text-opacity:1;color:rgb(165 180 252/var(--tw-text-opacity,1))}.text-rose-300{--tw-text-opacity:1;color:rgb(253 164 175/var(--tw-text-opacity,1))}.text-slate-100{--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity,1))}.text-slate-200{--tw-text-opacity:1;color:rgb(226 232 240/var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.text-slate-400{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-slate-900{--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity,1))}.text-warning{--tw-text-opacity:1;color:rgb(245 158 11/var(--tw-text-opacity,1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur:blur(8px);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}@font-face{font-family:Handjet;src:url(assets/fonts/handjet/Handjet-VariableFont.ttf) format("truetype");font-weight:100 900;font-style:normal;font-display:swap}.focus-visible\:outline:focus-visible{outline-style:solid}.focus-visible\:outline-2:focus-visible{outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-primary:focus-visible{outline-color:#22d3ee}@media (min-width:768px){.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:p-4{padding:1rem}.md\:p-5{padding:1.25rem}.md\:text-5xl{font-size:3rem;line-height:1}}
\ No newline at end of file
+*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Vazirmatn,Inter,system-ui,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{color-scheme:dark}body{--tw-bg-opacity:1;background-color:rgb(2 6 23/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity,1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:Vazirmatn,IRANSansX,Noto Sans Arabic,Tahoma,Segoe UI,system-ui,sans-serif}.font-brand{font-family:Handjet,ui-sans-serif,system-ui,sans-serif}.ltr{direction:ltr;text-align:left}.rtl{direction:rtl;text-align:right}.technical{direction:ltr;text-align:left;unicode-bidi:plaintext}.sticky{position:sticky}.top-2{top:.5rem}.z-20{z-index:20}.mx-auto{margin-left:auto;margin-right:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-4{height:1rem}.h-44{height:11rem}.h-48{height:12rem}.max-h-80{max-height:20rem}.w-4{width:1rem}.w-full{width:100%}.min-w-0{min-width:0}.max-w-7xl{max-width:80rem}.shrink-0{flex-shrink:0}.cursor-pointer{cursor:pointer}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-dashed{border-style:dashed}.border-amber-500\/40{border-color:rgba(245,158,11,.4)}.border-cyan-500\/40{border-color:rgba(6,182,212,.4)}.border-emerald-500\/40{border-color:rgba(16,185,129,.4)}.border-indigo-500\/40{border-color:rgba(99,102,241,.4)}.border-primary{--tw-border-opacity:1;border-color:rgb(34 211 238/var(--tw-border-opacity,1))}.border-primary\/70{border-color:rgba(34,211,238,.7)}.border-rose-500\/40{border-color:rgba(244,63,94,.4)}.border-rose-500\/50{border-color:rgba(244,63,94,.5)}.border-slate-500\/40{border-color:rgba(100,116,139,.4)}.border-slate-700{--tw-border-opacity:1;border-color:rgb(51 65 85/var(--tw-border-opacity,1))}.border-slate-800{--tw-border-opacity:1;border-color:rgb(30 41 59/var(--tw-border-opacity,1))}.border-slate-800\/90{border-color:rgba(30,41,59,.9)}.border-warning\/60{border-color:rgba(245,158,11,.6)}.bg-amber-500\/10{background-color:rgba(245,158,11,.1)}.bg-cyan-500\/10{background-color:rgba(6,182,212,.1)}.bg-emerald-500\/10{background-color:rgba(16,185,129,.1)}.bg-indigo-500\/10{background-color:rgba(99,102,241,.1)}.bg-primary{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity,1))}.bg-primary\/15{background-color:rgba(34,211,238,.15)}.bg-primary\/20{background-color:rgba(34,211,238,.2)}.bg-rose-500\/10{background-color:rgba(244,63,94,.1)}.bg-slate-500\/10{background-color:rgba(100,116,139,.1)}.bg-slate-700{--tw-bg-opacity:1;background-color:rgb(51 65 85/var(--tw-bg-opacity,1))}.bg-slate-900\/90{background-color:rgba(15,23,42,.9)}.bg-slate-950{--tw-bg-opacity:1;background-color:rgb(2 6 23/var(--tw-bg-opacity,1))}.bg-success{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-surface\/70{background-color:rgba(15,23,42,.7)}.bg-warning\/10{background-color:rgba(245,158,11,.1)}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[11px\]{font-size:11px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-none{line-height:1}.tracking-wide{letter-spacing:.025em}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.text-indigo-300{--tw-text-opacity:1;color:rgb(165 180 252/var(--tw-text-opacity,1))}.text-rose-300{--tw-text-opacity:1;color:rgb(253 164 175/var(--tw-text-opacity,1))}.text-slate-100{--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity,1))}.text-slate-200{--tw-text-opacity:1;color:rgb(226 232 240/var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.text-slate-400{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-slate-900{--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity,1))}.text-warning{--tw-text-opacity:1;color:rgb(245 158 11/var(--tw-text-opacity,1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur:blur(8px);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}@font-face{font-family:Handjet;src:url(assets/fonts/handjet/Handjet[wght].woff2) format("woff2"),url(assets/fonts/handjet/Handjet-VariableFont.ttf) format("truetype");font-weight:100 900;font-style:normal;font-display:swap}.focus-visible\:outline:focus-visible{outline-style:solid}.focus-visible\:outline-2:focus-visible{outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-primary:focus-visible{outline-color:#22d3ee}@media (min-width:768px){.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:p-4{padding:1rem}.md\:p-5{padding:1.25rem}.md\:text-5xl{font-size:3rem;line-height:1}}
\ No newline at end of file
diff --git a/docs-site/dist/index.html b/docs-site/dist/index.html
index 97e1bc1..1724846 100644
--- a/docs-site/dist/index.html
+++ b/docs-site/dist/index.html
@@ -1,5 +1,5 @@
-
+
@@ -11,7 +11,8 @@
MirrorToolkit
- Check mirrors, copy env snippets, and generate a mirror-aware Dockerfile.
+ بررسی سریع میرورها، تولید تنظیمات محیطی، و ساخت Dockerfile سازگار با میرورهای سریعتر.
+ Check mirrors → review results → paste Dockerfile → copy optimized output
CLI-first Static GitHub Pages Dockerfile Optimizer Offline-friendly No backend
@@ -19,11 +20,11 @@ MirrorToo
Browser benchmark is approximate because registry CORS policies vary. CLI benchmark remains authoritative.
-
+
- Dockerfile optimizer
-
- Optimize Copy optimized Dockerfile Download optimized Dockerfile
+ بهینهساز Dockerfile
+
+ بهینهسازی کپی Dockerfile خروجی دانلود Dockerfile خروجی
-
+
diff --git a/docs-site/package.json b/docs-site/package.json
index ae9906d..f668d2e 100644
--- a/docs-site/package.json
+++ b/docs-site/package.json
@@ -6,7 +6,9 @@
"clean": "node build.mjs --clean",
"build:css": "tailwindcss -i ./src/input.css -o ./dist/assets/tailwind.css --minify",
"build": "node build.mjs",
- "watch": "tailwindcss -i ./src/input.css -o ./dist/assets/tailwind.css --watch"
+ "watch": "tailwindcss -i ./src/input.css -o ./dist/assets/tailwind.css --watch",
+ "check:ui-contract": "node scripts/check-ui-contract.mjs",
+ "test": "npm run build && npm run check:ui-contract"
},
"devDependencies": {
"autoprefixer": "^10.4.20",
diff --git a/docs-site/scripts/check-ui-contract.mjs b/docs-site/scripts/check-ui-contract.mjs
new file mode 100644
index 0000000..7c8d143
--- /dev/null
+++ b/docs-site/scripts/check-ui-contract.mjs
@@ -0,0 +1,29 @@
+import { readFile } from 'node:fs/promises';
+import path from 'node:path';
+
+const root = new URL('..', import.meta.url).pathname;
+const read = (p) => readFile(path.join(root, p), 'utf8');
+
+const failures = [];
+const indexHtml = await read('src/index.html');
+const inputCss = await read('src/input.css');
+const appJs = await read('src/app.js');
+
+if (!/html[^>]*\blang=["']fa["']/i.test(indexHtml)) failures.push('index.html must include lang="fa" on .');
+if (!/html[^>]*\bdir=["']rtl["']/i.test(indexHtml)) failures.push('index.html must include dir="rtl" on .');
+if (!/href=["'][^"']*assets\/vendor\/flag-icons\/flag-icons\.min\.css["']/.test(indexHtml)) failures.push('index.html must include local flag-icons stylesheet.');
+if (!/\.font-brand\b/.test(inputCss)) failures.push('input.css must define .font-brand.');
+if (!/\.technical\b/.test(inputCss)) failures.push('input.css must define .technical.');
+if (!/function\s+getRegionBadge\s*\(/.test(appJs)) failures.push('app.js must contain getRegionBadge().');
+for (const label of ['سالم', 'کند', 'محدودیت مرورگر', 'ناموفق', 'بررسی\u200cنشده']) {
+ if (!appJs.includes(label)) failures.push(`app.js must contain Persian status label: ${label}`);
+}
+if (/https:\/\/(?:cdn|unpkg|jsdelivr|fonts\.googleapis)/i.test(indexHtml)) failures.push('index.html contains forbidden CDN URL.');
+
+if (failures.length) {
+ console.error('UI identity contract check failed:');
+ failures.forEach((msg, i) => console.error(`${i + 1}. ${msg}`));
+ process.exit(1);
+}
+
+console.log('UI identity contract check passed.');
diff --git a/docs-site/src/app.js b/docs-site/src/app.js
index ed45ec3..67b4713 100644
--- a/docs-site/src/app.js
+++ b/docs-site/src/app.js
@@ -44,8 +44,16 @@ function statusBadge(statusValue) {
[STATUS_MODEL.FAILED]: 'border-rose-500/40 bg-rose-500/10 text-rose-300',
[STATUS_MODEL.UNTESTED]: 'border-slate-500/40 bg-slate-500/10 text-slate-300'
};
- const label = String(statusValue || STATUS_MODEL.UNTESTED).replace(/-/g, ' ');
- return `${label} `;
+ const labelsFa = {
+ [STATUS_MODEL.OK]: 'سالم',
+ [STATUS_MODEL.SLOW]: 'کند',
+ [STATUS_MODEL.BLOCKED_BY_BROWSER]: 'محدودیت مرورگر',
+ [STATUS_MODEL.FAILED]: 'ناموفق',
+ [STATUS_MODEL.UNTESTED]: 'بررسینشده'
+ };
+ const rawLabel = String(statusValue || STATUS_MODEL.UNTESTED);
+ const label = labelsFa[rawLabel] || labelsFa[STATUS_MODEL.UNTESTED];
+ return `${label} `;
}
function getRegionBadge(raw = {}) {
@@ -54,9 +62,9 @@ function getRegionBadge(raw = {}) {
const alpha2 = region.length === 2 ? region.toLowerCase() : '';
const allowedFlags = new Set(['ir', 'jp', 'fr', 'cn', 'nl', 'us', 'ru', 'de', 'gb']);
if (alpha2 && allowedFlags.has(alpha2)) {
- return ` ${alpha2.toUpperCase()} `;
+ return ` ${alpha2.toUpperCase()} `;
}
- return `${region} `;
+ return `${region} `;
}
function normalizeMirrorData(rawData) { /* unchanged logic */
@@ -110,7 +118,7 @@ function renderSummary() { const rows = state.results; const okOrSlow = rows.fil
function renderFilters() { byId('filters').innerHTML = ['all', ...state.ecosystems].map((category) => { const icon = getEcosystemIcon(category); return `${icon ? ` ` : ''}${category} `; }).join(''); byId('filters').querySelectorAll('button').forEach((btn) => { btn.onclick = () => { state.activeCategory = btn.dataset.cat; renderRows(); }; }); }
-function renderRows() { const filtered = state.results.filter((r) => state.activeCategory === 'all' || r.ecosystem === state.activeCategory); byId('latencyRows').innerHTML = filtered.map((r) => { const icon = getEcosystemIcon(r.ecosystem); const region = getRegionBadge(r.raw); return `${icon ? ` ` : ''}${r.name}
${r.ecosystem}
${r.url}
${statusBadge(r.status)}${Number.isFinite(r.latency) ? `
${r.latency}ms
` : ''}${region ? `
${region}
` : ''}
`; }).join(''); }
+function renderRows() { const filtered = state.results.filter((r) => state.activeCategory === 'all' || r.ecosystem === state.activeCategory); byId('latencyRows').innerHTML = filtered.map((r) => { const icon = getEcosystemIcon(r.ecosystem); const region = getRegionBadge(r.raw); return `${icon ? ` ` : ''}${r.name}
${r.ecosystem}
${r.url}
${statusBadge(r.status)}${Number.isFinite(r.latency) ? `
${r.latency}ms
` : ''}${region ? `
${region}
` : ''}
`; }).join(''); }
function optimizeDockerfile(input) { const lines = input.split(/\r?\n/); const out = []; const inserted = []; let baseImage = null; for (const line of lines) { if (!baseImage && /^\s*FROM\s+(.+)/i.test(line)) baseImage = line.match(/^\s*FROM\s+(.+)/i)[1].trim(); out.push(line);} if (baseImage) { const argLines = ['ARG NPM_CONFIG_REGISTRY', 'ARG PIP_INDEX_URL', 'ARG APT_UBUNTU_MIRROR', 'ARG APT_DEBIAN_MIRROR']; const firstFrom = out.findIndex((l) => /^\s*FROM\s+/i.test(l)); argLines.forEach((arg) => { if (!out.some((l) => l.trim() === arg)) { out.splice(firstFrom + 1, 0, arg); inserted.push(arg); } }); } return { text: out.join('\n'), summary: { baseImage: baseImage || 'not detected', insertedArgs: inserted, warnings: baseImage ? [] : ['No FROM line detected; no ARG lines inserted.'] } }; }
diff --git a/docs-site/src/index.html b/docs-site/src/index.html
index 97e1bc1..1724846 100644
--- a/docs-site/src/index.html
+++ b/docs-site/src/index.html
@@ -1,5 +1,5 @@
-
+
@@ -11,7 +11,8 @@
MirrorToolkit
- Check mirrors, copy env snippets, and generate a mirror-aware Dockerfile.
+ بررسی سریع میرورها، تولید تنظیمات محیطی، و ساخت Dockerfile سازگار با میرورهای سریعتر.
+ Check mirrors → review results → paste Dockerfile → copy optimized output
CLI-first Static GitHub Pages Dockerfile Optimizer Offline-friendly No backend
@@ -19,11 +20,11 @@ MirrorToo
Browser benchmark is approximate because registry CORS policies vary. CLI benchmark remains authoritative.
-
+
- Dockerfile optimizer
-
- Optimize Copy optimized Dockerfile Download optimized Dockerfile
+ بهینهساز Dockerfile
+
+ بهینهسازی کپی Dockerfile خروجی دانلود Dockerfile خروجی
-
+
diff --git a/docs-site/src/input.css b/docs-site/src/input.css
index 42374f2..f5b7725 100644
--- a/docs-site/src/input.css
+++ b/docs-site/src/input.css
@@ -4,7 +4,9 @@
@font-face {
font-family: "Handjet";
- src: url("./assets/fonts/handjet/Handjet-VariableFont.ttf") format("truetype");
+ src:
+ url("./assets/fonts/handjet/Handjet[wght].woff2") format("woff2"),
+ url("./assets/fonts/handjet/Handjet-VariableFont.ttf") format("truetype");
font-weight: 100 900;
font-style: normal;
font-display: swap;
@@ -12,9 +14,19 @@
@layer base {
:root { color-scheme: dark; }
- body { @apply bg-slate-950 text-slate-100 antialiased font-sans; }
+ body {
+ @apply bg-slate-950 text-slate-100 antialiased;
+ font-family: Vazirmatn, IRANSansX, "Noto Sans Arabic", Tahoma, "Segoe UI", system-ui, sans-serif;
+ }
}
@layer components {
.font-brand { font-family: "Handjet", ui-sans-serif, system-ui, sans-serif; }
+ .ltr { direction: ltr; text-align: left; }
+ .rtl { direction: rtl; text-align: right; }
+ .technical {
+ direction: ltr;
+ text-align: left;
+ unicode-bidi: plaintext;
+ }
}
diff --git a/docs/github-pages.md b/docs/github-pages.md
index 89d8cbf..1f3b1dc 100644
--- a/docs/github-pages.md
+++ b/docs/github-pages.md
@@ -3,3 +3,16 @@
Enable Pages from GitHub Actions in repository settings.
The browser optimizer in `docs-site/app.js` applies the same simple ARG/ENV/marker rules client-side and downloads the optimized Dockerfile. No backend is used.
+
+## UI identity contract (required)
+- The docs-site UI identity is locked by `docs/web-ui-identity-contract.md`.
+- Do not casually redesign typography, language hierarchy, or direction behavior.
+- Persian RTL is the default page direction, while technical values (URLs/code/env/Dockerfile/ecosystem labels) stay LTR.
+- Font, flag, and image assets must remain local-only; CDN usage is not allowed.
+
+## CI and local checks
+Before publishing Pages, run:
+- `npm run build`
+- `npm run check:ui-contract`
+
+These checks guard against accidental removal of RTL, Persian labels, brand font hooks, local flag rendering, or introduction of CDN dependencies.
diff --git a/docs/web-ui-identity-contract.md b/docs/web-ui-identity-contract.md
new file mode 100644
index 0000000..38c02f0
--- /dev/null
+++ b/docs/web-ui-identity-contract.md
@@ -0,0 +1,34 @@
+# MirrorToolkit Web UI Identity Contract
+
+## Purpose
+This contract locks the visual and language identity of the MirrorToolkit docs-site and prevents accidental redesign drift.
+
+## Language and bilingual strategy
+- The UI is bilingual with **Persian as the primary explanatory language**.
+- English remains in place for technical labels and ecosystem terms where it improves clarity.
+- Short bilingual/English helper text is allowed for workflow guidance.
+
+## Direction (RTL/LTR) rules
+- Root document must stay:
+ - ``
+- Persian narrative content is RTL.
+- Technical values must remain LTR, including:
+ - URLs
+ - code snippets
+ - Dockerfile input/output
+ - env output
+ - ecosystem names and low-level technical identifiers
+
+## Typography
+- Brand title uses **Handjet** from local assets.
+- Body text uses local/system Persian-friendly sans-serif stack.
+- Handjet is limited to brand/title styling and should not replace body text.
+
+## Flags and assets
+- Country flags must be rendered using local `flag-icons` assets bundled in the repository.
+- CDN-hosted flags are not allowed.
+- No external font/CDN/image dependency is allowed.
+
+## Stability requirement
+- Do not replace or substantially alter this visual identity unless this contract is intentionally updated first.
+- Any change that removes RTL defaults, local flags, local font strategy, or Persian-first UI violates this contract.
diff --git a/docs/web-v1-validation.md b/docs/web-v1-validation.md
index 96d516b..36bdf40 100644
--- a/docs/web-v1-validation.md
+++ b/docs/web-v1-validation.md
@@ -6,27 +6,39 @@ This checklist validates only:
- schema normalization
- loading/error/empty states
- browser-safe mirror status model
+- UI identity contract consistency
## Manual steps
1. Build site assets:
- `cd docs-site && npm install && npm run build`
-2. Serve docs-site `dist/` and open in browser.
-3. Verify default load with repository `mirrors.json`:
+2. Enforce UI identity contract:
+ - `cd docs-site && npm run check:ui-contract`
+3. Serve docs-site `dist/` and open in browser.
+4. Verify default load with repository `mirrors.json`:
- status message reports loaded mirror and ecosystem counts
- summary cards show total mirrors and total ecosystems
-4. Verify error state:
+5. Verify error state:
- rename/remove `dist/mirrors.json` and refresh
- page shows error panel with failed path, likely cause, suggested action
- page does not stay on `Loading mirrors.json...`
-5. Verify empty state:
+6. Verify empty state:
- upload a JSON file with schema wrapper but no valid `{ ecosystem, url }` entries
- page shows empty-state message that schema loaded but no valid mirrors were found
-6. Verify legacy schema compatibility:
+7. Verify legacy schema compatibility:
- upload JSON using `{ "ecosystems": { "docker": [ ... ] } }`
- mirrors render and ecosystem filters populate
-7. Verify status model labels in benchmark output:
- - before run: mirrors appear as `Untested`
- - successful faster checks: `OK`
- - slower successful checks: `Slow`
- - browser policy/CORS blocks: `Blocked by browser` (not `Failed`)
- - explicit HTTP error responses: `Failed`
+8. Verify status model labels in benchmark output:
+ - before run: mirrors appear as `بررسینشده`
+ - successful faster checks: `سالم`
+ - slower successful checks: `کند`
+ - browser policy/CORS blocks: `محدودیت مرورگر` (not `ناموفق`)
+ - explicit HTTP error responses: `ناموفق`
+9. Verify identity rules visually:
+ - page root is Persian RTL (`lang="fa"`, `dir="rtl"`)
+ - brand title uses Handjet style
+ - technical blocks are LTR (env output, Dockerfile input/output, URLs)
+ - region flags are loaded from local `assets/vendor/flag-icons`
+ - no CDN dependencies exist in HTML
+
+## Policy note
+Visual identity must not be changed casually. Any intentional identity update must first revise `docs/web-ui-identity-contract.md`, then update tests/checks accordingly.