From d86e651104e2ff90eb25fac2e4e3399b106f80c5 Mon Sep 17 00:00:00 2001 From: Javid Date: Mon, 18 May 2026 17:13:49 +0330 Subject: [PATCH] Restore docs-site bilingual RTL identity and add UI contract checks --- docs-site/dist/app.js | 18 ++- ...ELSH,wght.ttf => Handjet-VariableFont.ttf} | Bin docs-site/dist/assets/icons/maven.svg | 142 +++++++++--------- docs-site/dist/assets/tailwind.css | 2 +- docs-site/dist/index.html | 27 ++-- docs-site/package.json | 4 +- docs-site/scripts/check-ui-contract.mjs | 29 ++++ docs-site/src/app.js | 18 ++- docs-site/src/index.html | 27 ++-- docs-site/src/input.css | 16 +- docs/github-pages.md | 13 ++ docs/web-ui-identity-contract.md | 34 +++++ docs/web-v1-validation.md | 34 +++-- 13 files changed, 242 insertions(+), 122 deletions(-) rename docs-site/dist/assets/fonts/handjet/{Handjet-VariableFont_ELGR,ELSH,wght.ttf => Handjet-VariableFont.ttf} (100%) create mode 100644 docs-site/scripts/check-ui-contract.mjs create mode 100644 docs/web-ui-identity-contract.md 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 ``; }).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-firstStatic GitHub PagesDockerfile OptimizerOffline-friendlyNo backend
@@ -19,11 +20,11 @@

MirrorToo
- - - - - + + + + +

    @@ -31,7 +32,7 @@

    MirrorToo
    @@ -39,14 +40,14 @@

    mirrors.json not found

    Browser benchmark is approximate because registry CORS policies vary. CLI benchmark remains authoritative.

    - +
    -

    Dockerfile optimizer

    - -
    +

    بهینه‌ساز 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 ``; }).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-firstStatic GitHub PagesDockerfile OptimizerOffline-friendlyNo backend
    @@ -19,11 +20,11 @@

    MirrorToo
    - - - - - + + + + +

      @@ -31,7 +32,7 @@

      MirrorToo
      @@ -39,14 +40,14 @@

      mirrors.json not found

      Browser benchmark is approximate because registry CORS policies vary. CLI benchmark remains authoritative.

      - +
      -

      Dockerfile optimizer

      - -
      +

      بهینه‌ساز 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.