feat(tasarim): görsel dilin tek kaynağı DESIGN.md, proje skill'i ve tasarım denetim betiği
All checks were successful
Deploy / deploy (push) Successful in 5m33s
All checks were successful
Deploy / deploy (push) Successful in 5m33s
- docs/tasarim/DESIGN.md: 9 bölümlük görsel dil belgesi (renk rolleri, tipografi, tek kaynak bileşenlerin sınıf dizileri, yerleşim, yap/yapma, brief şablonu) - kolaytercih-tasarim skill'i: UI işinde tetiklenir, belgeye götürür, örnek verir; tasarimci ve yazilimci ajanlarına skills: ile önyüklenir - scripts/tasarim-denetim.ts: 13 kural (Badge, CTA'da Sparkles, palet dışı ton, gradyan metin, parlama, hareket spam'i, çarpı, font); pnpm lint'e bağlandı - ui-ux-pro-max genel kataloğu skills'ten .agents/skills-devre-disi/ altına taşındı - AGENTS.md: DESIGN.md kuralı; rozet sınıfı koda göre düzeltildi (font-bricolage yoktu) - arama-funnel-karti: palet dışı text-cyan-300 → text-orange-500 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
215
scripts/tasarim-denetim.ts
Normal file
215
scripts/tasarim-denetim.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Tasarım dili denetimi — docs/tasarim/DESIGN.md kurallarının makineyle
|
||||
* yakalanabilen kısmı. Ajan/insan fark etmez: `pnpm tasarim:denetim`.
|
||||
*
|
||||
* Çıkış kodu: hata varsa 1, yoksa 0. Uyarılar çıkış kodunu etkilemez.
|
||||
* Kapsam: src/**\/*.tsx. Hariç: src/components/ui (shadcn tabanı) ve
|
||||
* src/components/dev (yalnızca geliştirici paneli) — bazı kurallarda.
|
||||
*/
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
|
||||
type Seviye = "hata" | "uyari";
|
||||
|
||||
type Bulgu = {
|
||||
dosya: string;
|
||||
satir: number;
|
||||
kural: string;
|
||||
seviye: Seviye;
|
||||
mesaj: string;
|
||||
parca: string;
|
||||
};
|
||||
|
||||
type Kural = {
|
||||
ad: string;
|
||||
seviye: Seviye;
|
||||
mesaj: string;
|
||||
/** ui/dev klasörleri de taransın mı */
|
||||
uiDahil?: boolean;
|
||||
/** Tam yolu eşleşen dosyalar kuraldan muaf */
|
||||
muaf?: RegExp;
|
||||
bul: (icerik: string) => Array<{ index: number; parca: string }>;
|
||||
};
|
||||
|
||||
const KOK = process.cwd();
|
||||
const KAYNAK = join(KOK, "src");
|
||||
const UI_VEYA_DEV = /src\/components\/(ui|dev)\//;
|
||||
|
||||
function tsxDosyalari(dizin: string): string[] {
|
||||
const sonuc: string[] = [];
|
||||
for (const ad of readdirSync(dizin)) {
|
||||
const yol = join(dizin, ad);
|
||||
if (statSync(yol).isDirectory()) sonuc.push(...tsxDosyalari(yol));
|
||||
else if (yol.endsWith(".tsx")) sonuc.push(yol);
|
||||
}
|
||||
return sonuc;
|
||||
}
|
||||
|
||||
function satirNo(icerik: string, index: number): number {
|
||||
let n = 1;
|
||||
for (let i = 0; i < index && i < icerik.length; i++) if (icerik[i] === "\n") n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
function satirParcasi(icerik: string, index: number): string {
|
||||
const bas = icerik.lastIndexOf("\n", index) + 1;
|
||||
let son = icerik.indexOf("\n", index);
|
||||
if (son === -1) son = icerik.length;
|
||||
return icerik.slice(bas, son).trim().slice(0, 110);
|
||||
}
|
||||
|
||||
/** Basit regex kuralı: her eşleşme bir bulgu. */
|
||||
function regexKurali(re: RegExp) {
|
||||
return (icerik: string) => {
|
||||
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g");
|
||||
const sonuc: Array<{ index: number; parca: string }> = [];
|
||||
for (const m of icerik.matchAll(g)) sonuc.push({ index: m.index ?? 0, parca: m[0] });
|
||||
return sonuc;
|
||||
};
|
||||
}
|
||||
|
||||
/** <Etiket …>…</Etiket> bloklarının içinde `ic` deseni varsa bulgu. */
|
||||
function blokIcinde(etiketler: string[], ic: RegExp) {
|
||||
return (icerik: string) => {
|
||||
const sonuc: Array<{ index: number; parca: string }> = [];
|
||||
for (const etiket of etiketler) {
|
||||
const blok = new RegExp(`<${etiket}\\b[\\s\\S]*?<\\/${etiket}>`, "g");
|
||||
for (const m of icerik.matchAll(blok)) {
|
||||
const icM = ic.exec(m[0]);
|
||||
if (icM) sonuc.push({ index: (m.index ?? 0) + icM.index, parca: icM[0] });
|
||||
}
|
||||
}
|
||||
return sonuc;
|
||||
};
|
||||
}
|
||||
|
||||
const RENK_ONEK = "(?:bg|text|border|ring|fill|stroke|from|to|via|divide|outline|placeholder|decoration|accent|caret|shadow)";
|
||||
|
||||
const KURALLAR: Kural[] = [
|
||||
{
|
||||
ad: "badge-import",
|
||||
seviye: "hata",
|
||||
mesaj: "Rozet için @/components/ui/badge değil SectionEyebrow kullanılır (DESIGN.md §4.2)",
|
||||
bul: regexKurali(/from\s+["']@\/components\/ui\/badge["']/),
|
||||
},
|
||||
{
|
||||
ad: "elle-rozet",
|
||||
seviye: "hata",
|
||||
mesaj: "Elle yazılmış rozet; SectionEyebrow'u içe aktar (DESIGN.md §4.2)",
|
||||
muaf: /src\/components\/pixel-decor\.tsx$/,
|
||||
bul: regexKurali(/rounded-full border border-slate-200 bg-white px-3\.5 py-1\.5/),
|
||||
},
|
||||
{
|
||||
ad: "cta-yildiz",
|
||||
seviye: "hata",
|
||||
mesaj: "CTA butonunda Sparkles/yıldız ikonu olmaz; metin + sondaki ArrowRight (DESIGN.md §4.1)",
|
||||
bul: blokIcinde(["Button", "Link"], /<Sparkles\b/),
|
||||
},
|
||||
{
|
||||
ad: "yabanci-notr",
|
||||
seviye: "hata",
|
||||
mesaj: "Nötr renk yalnızca slate; gray/zinc/neutral/stone yazılmaz (DESIGN.md §2)",
|
||||
bul: regexKurali(new RegExp(`\\b${RENK_ONEK}-(?:gray|zinc|neutral|stone)-\\d+`)),
|
||||
},
|
||||
{
|
||||
ad: "yabanci-renk",
|
||||
seviye: "hata",
|
||||
mesaj: "Paletin dışında ton; primary mavi / orange-500 / slate / risk renkleri kullanılır (DESIGN.md §2)",
|
||||
bul: regexKurali(
|
||||
new RegExp(`\\b${RENK_ONEK}-(?:indigo|purple|fuchsia|pink|cyan|teal|lime|green|yellow)-\\d+`),
|
||||
),
|
||||
},
|
||||
{
|
||||
ad: "gradyan-metin",
|
||||
seviye: "hata",
|
||||
mesaj: "Gradyan metin yok (DESIGN.md §2)",
|
||||
bul: regexKurali(/\b(?:bg-clip-text|text-transparent)\b/),
|
||||
},
|
||||
{
|
||||
ad: "parlama-golgesi",
|
||||
seviye: "hata",
|
||||
mesaj: "Parlama (glow) gölgesi yok; shadow-sm / hover:shadow-lg shadow-primary/5 (DESIGN.md §4.4)",
|
||||
bul: regexKurali(/shadow-\[0_0_[1-9]\d*px/),
|
||||
},
|
||||
{
|
||||
ad: "hareket-spam",
|
||||
seviye: "hata",
|
||||
mesaj:
|
||||
"whileHover scale / aos / bounce yok; Reveal, piksel ısısı, kt-unblur (DESIGN.md §7). animate-ping yalnızca canlı göstergesi noktasında",
|
||||
bul: regexKurali(/whileHover=\{\{\s*scale|data-aos=|\banimate-bounce\b/),
|
||||
},
|
||||
{
|
||||
ad: "kapatma-carpisi",
|
||||
seviye: "hata",
|
||||
mesaj: "showCloseButton={false} verilen dosyada kanonik çarpı sınıf seti yok (DESIGN.md §4.3)",
|
||||
bul: (icerik) => {
|
||||
const m = /showCloseButton=\{false\}/.exec(icerik);
|
||||
if (!m) return [];
|
||||
const kanonik =
|
||||
/size-9 cursor-pointer items-center justify-center rounded-full border border-slate-200/.test(icerik);
|
||||
return kanonik ? [] : [{ index: m.index, parca: m[0] }];
|
||||
},
|
||||
},
|
||||
{
|
||||
ad: "yabanci-font",
|
||||
seviye: "hata",
|
||||
mesaj: "Yalnızca font-heading / font-sans / font-bricolage / font-mono (DESIGN.md §3)",
|
||||
bul: regexKurali(/\bfont-(?:serif|\[)|["'](?:Inter|Roboto|Arial|Helvetica)["']/),
|
||||
},
|
||||
{
|
||||
ad: "dark-sinif",
|
||||
seviye: "uyari",
|
||||
mesaj: "Üründe karanlık mod yok; dark: sınıfı gereksiz (DESIGN.md §1)",
|
||||
bul: regexKurali(/(?<=[\s"'`])dark:[a-z]/),
|
||||
},
|
||||
{
|
||||
ad: "hex-renk",
|
||||
seviye: "uyari",
|
||||
mesaj: "Sınıf içinde hex renk; token ya da Tailwind tonu kullan (DESIGN.md §2)",
|
||||
bul: regexKurali(/\[#[0-9a-fA-F]{3,8}\]/),
|
||||
},
|
||||
{
|
||||
ad: "emoji-ikon",
|
||||
seviye: "uyari",
|
||||
mesaj: "Emoji ikon olarak kullanılmaz; lucide ikon (DESIGN.md §4.7)",
|
||||
bul: (icerik) =>
|
||||
// Yorum satırındaki işaretler (// ✗ gibi) ikon değildir
|
||||
regexKurali(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]\uFE0F?/u)(icerik).filter(
|
||||
(e) => !/^(?:\/\/|\*|\/\*)/.test(satirParcasi(icerik, e.index)),
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const bulgular: Bulgu[] = [];
|
||||
for (const yol of tsxDosyalari(KAYNAK)) {
|
||||
const gorece = relative(KOK, yol);
|
||||
const icerik = readFileSync(yol, "utf8");
|
||||
for (const k of KURALLAR) {
|
||||
if (!k.uiDahil && UI_VEYA_DEV.test(gorece)) continue;
|
||||
if (k.muaf && k.muaf.test(gorece)) continue;
|
||||
for (const e of k.bul(icerik)) {
|
||||
bulgular.push({
|
||||
dosya: gorece,
|
||||
satir: satirNo(icerik, e.index),
|
||||
kural: k.ad,
|
||||
seviye: k.seviye,
|
||||
mesaj: k.mesaj,
|
||||
parca: satirParcasi(icerik, e.index),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bulgular.sort((a, b) =>
|
||||
a.seviye !== b.seviye ? (a.seviye === "hata" ? -1 : 1) : a.dosya.localeCompare(b.dosya) || a.satir - b.satir,
|
||||
);
|
||||
|
||||
for (const b of bulgular) {
|
||||
const etiket = b.seviye === "hata" ? "HATA " : "uyarı";
|
||||
console.log(`${b.dosya}:${b.satir} [${etiket}] ${b.kural} — ${b.mesaj}\n ${b.parca}`);
|
||||
}
|
||||
|
||||
const hata = bulgular.filter((b) => b.seviye === "hata").length;
|
||||
const uyari = bulgular.length - hata;
|
||||
console.log(`\nTasarım denetimi: ${hata} hata, ${uyari} uyarı (${KURALLAR.length} kural, docs/tasarim/DESIGN.md)`);
|
||||
process.exit(hata > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user