@appaflytech/zeyrek-ai-mcp 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +27 -21
  2. package/dist/config.js +2 -0
  3. package/dist/knowledge/componentCatalog.js +122 -27
  4. package/dist/knowledge/componentEvents.js +35 -4
  5. package/dist/knowledge/errorPath.js +1 -1
  6. package/dist/knowledge/normalize.js +15 -0
  7. package/dist/knowledge/rules.js +3 -1
  8. package/dist/knowledge/styleResolve.js +46 -4
  9. package/dist/knowledge/themeResolve.js +936 -0
  10. package/dist/knowledge/themeRules.js +221 -0
  11. package/dist/knowledge/themeSchema.js +335 -0
  12. package/dist/platform/domSummary.js +144 -0
  13. package/dist/platform/screenshot.js +414 -23
  14. package/dist/platform/screenshotMemory.js +55 -0
  15. package/dist/resources/examples/layout-app-shell.json +1 -1
  16. package/dist/resources/examples/theme-brand.json +162 -0
  17. package/dist/resources/guides/component.md +7 -1
  18. package/dist/resources/guides/getting-started.md +4 -0
  19. package/dist/resources/guides/layout.md +30 -26
  20. package/dist/resources/guides/microservice.md +55 -1
  21. package/dist/resources/guides/page.md +87 -44
  22. package/dist/resources/guides/theme.md +210 -0
  23. package/dist/resources/guides/translation.md +6 -4
  24. package/dist/resources/guides/workflow.md +91 -6
  25. package/dist/resources/schema-fingerprint.json +2 -2
  26. package/dist/server.js +52 -43
  27. package/dist/tools/components.js +8 -0
  28. package/dist/tools/designSchemaPatch.js +152 -0
  29. package/dist/tools/flows.js +15 -18
  30. package/dist/tools/layouts.js +4 -4
  31. package/dist/tools/microservices.js +5 -5
  32. package/dist/tools/pages.js +196 -323
  33. package/dist/tools/projects.js +66 -9
  34. package/dist/tools/schemaOps.js +280 -0
  35. package/dist/tools/screenWrite.js +2 -1
  36. package/dist/tools/screens.js +0 -7
  37. package/dist/tools/themes.js +163 -0
  38. package/dist/tools/workflows.js +1 -1
  39. package/package.json +1 -1
@@ -0,0 +1,221 @@
1
+ import { GOOGLE_FONTS, MAX_PRESET_VERSIONS, SCALE_RATIO_MAX, isValidPresetName } from './themeResolve.js';
2
+ /**
3
+ * ÖZET — Tema yazmasının lint'i (THEME-001…011).
4
+ *
5
+ * İki küme, iki farklı gövdeye bakar ve bu ayrım bilinçlidir:
6
+ *
7
+ * - **block** kuralları BİRLEŞMİŞ kayda bakar (`themePolicyRules`): yazılacak kayıt geçerli
8
+ * olmalı, geçersizliği base'ten gelse bile. Bunlar runtime'ı sessizce bozan şeylerdir.
9
+ * - **warn** kuralları yalnız ÇAĞIRANIN YAZDIĞINA bakar (`themeInputWarnings`): base'te duran
10
+ * eski bir kusur yüzünden her `theme_update` çağrısında aynı uyarıyı basmak gürültüdür ve
11
+ * gürültü okunmaz.
12
+ *
13
+ * Kaynak: `docs/08-kural-envanteri.md` §Tema araçları.
14
+ */
15
+ const guide = 'ngapps://guides/theme';
16
+ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
17
+ const HEX = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
18
+ const FULL_HEX = /^#[0-9a-f]{6}$/i;
19
+ const CSS_LENGTH = /^-?\d+(\.\d+)?(px|rem|em|%|vh|vw|vmin|vmax|ch|ex|pt|pc|cm|mm|in)$/i;
20
+ function block(ruleId, message) {
21
+ return { ruleId, severity: 'block', message, guide };
22
+ }
23
+ function warn(ruleId, message) {
24
+ return { ruleId, severity: 'warn', message, guide };
25
+ }
26
+ /** Ad → gövde kayıtlarındaki adlar (preset'ler ve versiyonlar aynı kurala uyar). */
27
+ function presetNameFindings(theme) {
28
+ const findings = [];
29
+ const invalid = (label, name) => block('THEME-001', `${label} adı geçersiz: '${name}'. Ad boş olamaz, başında/sonunda boşluk taşıyamaz, '$' ile başlayamaz (çözümleyicinin sihirli anahtarlarıyla — $preset / $componentPreset — karışır) ve 40 karakteri aşamaz. Ad bir nesne ANAHTARIDIR ve düğümün stiline ham metin olarak yazılır.`);
30
+ for (const name of Object.keys(theme.presets ?? {})) {
31
+ if (!isValidPresetName(name))
32
+ findings.push(invalid('Preset', name));
33
+ }
34
+ for (const [type, preset] of Object.entries(theme.componentPresets ?? {})) {
35
+ if (!isPlainObject(preset) || !isPlainObject(preset.versions))
36
+ continue;
37
+ for (const name of Object.keys(preset.versions)) {
38
+ if (!isValidPresetName(name))
39
+ findings.push(invalid(`'${type}' versiyon`, name));
40
+ }
41
+ }
42
+ return findings;
43
+ }
44
+ function componentPresetShapeFindings(theme) {
45
+ const findings = [];
46
+ for (const [type, preset] of Object.entries(theme.componentPresets ?? {})) {
47
+ if (!isPlainObject(preset))
48
+ continue;
49
+ const versions = isPlainObject(preset.versions) ? Object.keys(preset.versions) : [];
50
+ const defaultVersion = String(preset.defaultVersion ?? '');
51
+ if (versions.length === 0) {
52
+ findings.push(block('THEME-002', `componentPresets.${type}: 'versions' boş — bir bileşen preset'i en az bir versiyon taşır. Tipin varsayılanını kaldırmak istiyorsanız kaydın tamamını silin: unset: ['componentPresets.${type}'].`));
53
+ continue;
54
+ }
55
+ if (!versions.includes(defaultVersion)) {
56
+ findings.push(block('THEME-002', `componentPresets.${type}: defaultVersion '${defaultVersion}' versiyonlar arasında yok (${versions.join(', ')}). Panel bozuk kaydı ilk versiyona onarır ama YAZARKEN doğrusunu vermek gerekir — yeni düğümler varsayılan versiyonun prop'larıyla kurulur.`));
57
+ }
58
+ if (versions.length > MAX_PRESET_VERSIONS) {
59
+ findings.push(block('THEME-002', `componentPresets.${type}: ${versions.length} versiyon — sınır ${MAX_PRESET_VERSIONS}. Panelin versiyon şeridi tek satırda okunur kalsın diye sınırlıdır ve dolduğunda 'Yeni versiyon' düğmesini hiç çizmez.`));
60
+ }
61
+ }
62
+ return findings;
63
+ }
64
+ function shadowFindings(theme) {
65
+ const color = theme.shadow?.color;
66
+ if (color === undefined || FULL_HEX.test(color))
67
+ return [];
68
+ return [block('THEME-004', `shadow.color '${color}' altı haneli hex DEĞİL. Runtime rengi karakter karakter ayrıştırıyor (shadowTokens); tanımadığı değerde --ng-shadow-sm/md/lg ÜÇÜ BİRDEN '0px 0px 0px 0px rgba(0, 0, 0, 0)' olur, yani gölge sessizce kapanır. '#000000' gibi yazın.`)];
69
+ }
70
+ function lengthFindings(theme) {
71
+ const findings = [];
72
+ const check = (path, value) => {
73
+ if (value === undefined)
74
+ return;
75
+ if (typeof value === 'string' && CSS_LENGTH.test(value.trim()))
76
+ return;
77
+ findings.push(block('THEME-006', `${path}: ${JSON.stringify(value)} bir CSS uzunluğu değil. Değer değişkene HAM basılır (--ng-font-size: ${String(value)}), tarayıcı bildirimi atar ve tek işaret konsoldur. '16px' gibi birimli yazın — çıplak sayıyı gateway kendisi px'e çevirir.`));
78
+ };
79
+ check('fontSize', theme.fontSize);
80
+ check('spacing', theme.spacing);
81
+ check('borderRadius', theme.borderRadius);
82
+ if (theme.radiusScale?.pill !== undefined)
83
+ check('radiusScale.pill', theme.radiusScale.pill);
84
+ return findings;
85
+ }
86
+ function ratioFindings(theme) {
87
+ const findings = [];
88
+ const scales = [
89
+ ['typeScale', theme.typeScale],
90
+ ['spacingScale', theme.spacingScale],
91
+ ['radiusScale', theme.radiusScale ? { sm: theme.radiusScale.sm, lg: theme.radiusScale.lg } : undefined]
92
+ ];
93
+ for (const [name, scale] of scales) {
94
+ for (const [level, value] of Object.entries(scale ?? {})) {
95
+ if (value === undefined)
96
+ continue;
97
+ if (typeof value === 'number' && value > 0 && value <= SCALE_RATIO_MAX)
98
+ continue;
99
+ findings.push(block('THEME-007', `${name}.${level}: ${JSON.stringify(value)} geçersiz oran — 0 < oran ≤ ${SCALE_RATIO_MAX} olmalı. Runtime aralık dışını SESSİZCE yok sayar (mergeRatioScale), yani kademe varsayılanda kalır ve "yazdım ama değişmedi" görürsünüz. Sıfır/negatif oran kademeyi görünmez yapacağı için yasak.`));
100
+ }
101
+ }
102
+ const opacity = theme.shadow?.opacity;
103
+ if (opacity !== undefined && (typeof opacity !== 'number' || opacity < 0 || opacity > 100)) {
104
+ findings.push(block('THEME-007', `shadow.opacity: ${JSON.stringify(opacity)} — 0-100 arası YÜZDE bekleniyor (runtime bunu 100'e bölüp alfa yapar).`));
105
+ }
106
+ return findings;
107
+ }
108
+ /** Kayıt geçerli olmalı: bu kurallar BİRLEŞMİŞ gövdeye bakar ve isteği durdurur. */
109
+ export const themePolicyRules = [
110
+ {
111
+ id: 'THEME-001',
112
+ module: 'theme',
113
+ appliesTo: ['update'],
114
+ sourceRef: "preset-name.functions.ts isValidPresetName; style-presets.ts STYLE_PRESET_KEY ('$preset' ham anahtar olarak düğüm stiline yazılır)",
115
+ check: definition => presetNameFindings(definition)
116
+ },
117
+ {
118
+ id: 'THEME-002',
119
+ module: 'theme',
120
+ appliesTo: ['update'],
121
+ sourceRef: 'component-presets.functions.ts MAX_PRESET_VERSIONS / normalizeComponentPreset (bozuk defaultVersion ilk versiyona onarılır)',
122
+ check: definition => componentPresetShapeFindings(definition)
123
+ },
124
+ {
125
+ id: 'THEME-004',
126
+ module: 'theme',
127
+ appliesTo: ['update'],
128
+ sourceRef: 'theme/utils.ts shadowTokens — color.length < 7 ⇒ üç gölge de görünmez',
129
+ check: definition => shadowFindings(definition)
130
+ },
131
+ {
132
+ id: 'THEME-006',
133
+ module: 'theme',
134
+ appliesTo: ['update'],
135
+ sourceRef: 'theme/utils.ts mergeTokens + buildThemeStyle — değer CSS değişkenine ham basılır',
136
+ check: definition => lengthFindings(definition)
137
+ },
138
+ {
139
+ id: 'THEME-007',
140
+ module: 'theme',
141
+ appliesTo: ['update'],
142
+ sourceRef: 'theme/utils.ts mergeRatioScale (>0 değilse sessizce yok sayılır); theme-logic.ts readScaleRatio (0 < r ≤ 20)',
143
+ check: definition => ratioFindings(definition)
144
+ }
145
+ ];
146
+ /** Hex olmayan renk değerlerini toplar (`palette` · `paletteDark` · `actions` · `colors`). */
147
+ function colorWarnings(theme) {
148
+ const offenders = [];
149
+ const visit = (path, value) => {
150
+ if (typeof value === 'string' && !HEX.test(value.trim()))
151
+ offenders.push(`${path}: '${value}'`);
152
+ };
153
+ for (const group of ['palette', 'paletteDark']) {
154
+ for (const [name, values] of Object.entries(theme[group] ?? {})) {
155
+ if (!isPlainObject(values))
156
+ continue;
157
+ for (const [key, value] of Object.entries(values))
158
+ visit(`${group}.${name}.${key}`, value);
159
+ }
160
+ }
161
+ for (const [name, value] of Object.entries(theme.actions ?? {}))
162
+ visit(`actions.${name}`, value);
163
+ for (const [family, tones] of Object.entries(theme.colors ?? {})) {
164
+ if (!isPlainObject(tones))
165
+ continue;
166
+ for (const [tone, value] of Object.entries(tones))
167
+ visit(`colors.${family}.${tone}`, value);
168
+ }
169
+ if (offenders.length === 0)
170
+ return [];
171
+ return [warn('THEME-005', `Hex olmayan renk değeri (${offenders.slice(0, 5).join(' · ')}${offenders.length > 5 ? ` · +${offenders.length - 5}` : ''}). CSS bunları çizer ama iki yer bozulur: studio'nun renk chip'leri hex bekler ve --ng-text-on-accent accent'i karakter karakter okuyup kontrast hesapladığı için (getContrastColor) tanımadığı değerde '#ffffff'e düşer — koyu bir accent üstüne beyaz metin yazılır sanılır. '#RRGGBB' yazın.`)];
172
+ }
173
+ /**
174
+ * Yalnız ÇAĞIRANIN yazdığına bakan uyarılar. `merged` bağlam içindir (THEME-009 karşı modun
175
+ * kayıtta olup olmadığını bilmek zorunda).
176
+ */
177
+ export function themeInputWarnings(input, merged) {
178
+ const findings = [...colorWarnings(input)];
179
+ if (input.componentStyles !== undefined) {
180
+ findings.push(warn('THEME-003', `componentStyles EMEKLİ (${Object.keys(input.componentStyles).join(', ')}). Runtime onu hâlâ uyguluyor ama Studio tema paneli açıldığı ilk kayıtta içeriği componentPresets'in varsayılan versiyonuna TAŞIYIP anahtarı siliyor — yani "tipin her örneğine canlı uygulanan stil" sessizce "yalnız YENİ düğümlere varsayılan"a döner ve sayfadaki mevcut düğümler o an görünüşlerini kaybeder. Kalıcı ortak görünüm için adlı preset + style.$preset; yeni düğümlerin başlangıç değeri için componentPresets.`));
181
+ }
182
+ for (const font of [input.fontFamily, input.headingFontFamily]) {
183
+ if (font === undefined || font === '' || GOOGLE_FONTS.includes(font))
184
+ continue;
185
+ findings.push(warn('THEME-008', `'${font}' panelin Google Fonts listesinde yok. Runtime yazı tipini ADIYLA Google'a bağlıyor (fonts.googleapis.com/css2?family=${font.replace(/ /g, '+')}); ad yanlışsa istek boş döner ve metin sessizce sans-serif çizilir. Sistem fontu kullanacaksanız bu uyarı beklenendir.`));
186
+ }
187
+ if (input.palette !== undefined && merged.paletteDark === undefined) {
188
+ const dark = merged.mode === 'dark';
189
+ findings.push(warn('THEME-009', dark
190
+ ? "Tema mode:'dark' ama yalnız 'palette' yazıldı: palet override'ları MOD BAŞINA tutulur (palette aydınlıkta, paletteDark karanlıkta) ve runtime karanlık modda paletteDark'a bakar — yazdığınız renkler ekranda HİÇ görünmez. Karanlık için paletteDark yazın ya da mode:'light' yapın."
191
+ : "'palette' yazıldı ama 'paletteDark' yok. Proje mode:'light' olsa bile ziyaretçi koyu moda GEÇEBİLİR (theme-toggle komponenti, tarayıcıda kalıcı; page_screenshot colorScheme:'dark' aynı yolu kullanır) ve o an runtime paletteDark'a bakar — markanız kaybolur, platformun kendi koyu paleti çizilir (ölçüldü 2026-09-15). Uygulama koyu modda hiç açılmayacaksa bu uyarı yok sayılabilir."));
192
+ }
193
+ return findings;
194
+ }
195
+ /** THEME-010 — silinen preset'i işaret eden düğümler sessizce kendi stiline düşer. */
196
+ export function unsetWarnings(base, paths) {
197
+ const removed = paths
198
+ .map(path => /^presets\.(.+)$/.exec(path)?.[1])
199
+ .filter((name) => name !== undefined && Object.hasOwn(base.presets ?? {}, name));
200
+ if (removed.length === 0)
201
+ return [];
202
+ return [warn('THEME-010', `Silinen preset (${removed.join(', ')}) sayfalardaki düğümlerde SESSİZCE kaybolur: çözümleyici eşleşmeyen bir $preset adını hata vermeden atlar (pages must not blank out), yani düğüm kendi stiline düşer ve kimse bir şey söylemez. Silmeden önce style.$preset ile bağlı düğümleri arayın (page_schema_get) — gateway bu taramayı henüz yapmıyor.`)];
203
+ }
204
+ /** THEME-011 — bileşen preset'i yalnız YERLEŞTİRİLEBİLİR bir tipe verilebilir. */
205
+ export function componentPresetTypeWarnings(theme, components) {
206
+ const known = new Map(components
207
+ .filter(component => typeof component.slug === 'string')
208
+ .map(component => [component.slug, component]));
209
+ const findings = [];
210
+ for (const type of Object.keys(theme.componentPresets ?? {})) {
211
+ const component = known.get(type);
212
+ if (component === undefined) {
213
+ findings.push(warn('THEME-011', `componentPresets.${type}: bu uygulamanın kataloğunda '${type}' diye bir komponent YOK — kayıt yazılır ama hiçbir düğüme uygulanmaz. Slug'ları component_list verir.`));
214
+ continue;
215
+ }
216
+ if (component.kind === 'widget' || component.isHide === true) {
217
+ findings.push(warn('THEME-011', `componentPresets.${type}: bu tipe preset verilemez (${component.kind === 'widget' ? 'widget kendi şemasını yönetir' : 'araç kutusunun gizlediği iç tip'}) — tasarımcının yerleştiremediği bir tipin varsayılanı da olmaz.`));
218
+ }
219
+ }
220
+ return findings;
221
+ }
@@ -0,0 +1,335 @@
1
+ import { PALETTE_GROUPS, SPACING_KEYS, THEME_TOP_LEVEL_KEYS } from './themeResolve.js';
2
+ import { normalizeStyleKeys } from './normalize.js';
3
+ import { z } from 'zod';
4
+ /**
5
+ * ÖZET — Tema kaydının OKUNABİLİR özeti: `theme_get(view:'summary')`'nin gövdesi ve
6
+ * `project_overview` / `page_runtime_get`'in kısa tema bloğu buradan çıkar.
7
+ *
8
+ * Özet "ne yazılmış" sorusunu yanıtlar, "ekranda ne çizilir" sorusunu değil — ikincisi
9
+ * `themeResolve.ts`'in işi. Ayrım bilinçli: bir projenin temasında NEYE dokunulduğunu görmek
10
+ * (marka rengi mi, yalnız preset mi) 190 değişkenlik çözümü okumaktan başka bir sorudur ve
11
+ * ajanın ilk sorduğu odur.
12
+ */
13
+ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ /** `{ accent: { primary } }` → `['accent.primary']`; palet override'ı bu kadarla anlatılır. */
15
+ function paletteEntries(palette) {
16
+ if (!isPlainObject(palette))
17
+ return [];
18
+ return Object.entries(palette).flatMap(([group, values]) => isPlainObject(values) ? Object.keys(values).map(key => `${group}.${key}`) : [group]);
19
+ }
20
+ /**
21
+ * Kayıtta BULUNAN üst anahtarların dökümü. Yokluk `null` ile anlatılmaz: `prune`
22
+ * (platform/responses.ts) null alanları yanıttan siliyor, yani "yazılmamış" ile "yok" aynı
23
+ * görünürdü. Yazılmayan alan `overrides`'a HİÇ girmez; giren her alan gerçekten kayıtta vardır.
24
+ */
25
+ export function summarizeTheme(theme) {
26
+ const overrides = {};
27
+ const record = theme;
28
+ for (const key of ['palette', 'paletteDark']) {
29
+ const entries = paletteEntries(record[key]);
30
+ if (entries.length > 0)
31
+ overrides[key] = entries;
32
+ }
33
+ for (const key of ['fontFamily', 'headingFontFamily', 'fontSize', 'fontWeight', 'spacing', 'borderRadius', 'defaultInputSize']) {
34
+ if (record[key] !== undefined)
35
+ overrides[key] = record[key];
36
+ }
37
+ // Ölçekler ve gölge olduğu gibi taşınır: yalnız SAPAN kademeler kayda giriyor, yani kaydın
38
+ // kendisi zaten özet (`theme-logic.ts` scaleToOverrides).
39
+ for (const key of ['typeScale', 'radiusScale', 'spacingScale', 'shadow']) {
40
+ if (isPlainObject(record[key]))
41
+ overrides[key] = record[key];
42
+ }
43
+ if (isPlainObject(record.colors))
44
+ overrides.colors = Object.keys(record.colors);
45
+ if (isPlainObject(record.actions))
46
+ overrides.actions = Object.keys(record.actions);
47
+ const presetNames = isPlainObject(theme.presets) ? Object.keys(theme.presets) : [];
48
+ const componentPresets = {};
49
+ for (const [type, preset] of Object.entries(theme.componentPresets ?? {})) {
50
+ if (!isPlainObject(preset))
51
+ continue;
52
+ const versions = isPlainObject(preset.versions) ? preset.versions : {};
53
+ componentPresets[type] = {
54
+ defaultVersion: String(preset.defaultVersion ?? ''),
55
+ versions: Object.keys(versions),
56
+ hasAppearance: Object.values(versions).some(version => isPlainObject(version) && isPlainObject(version.appearance))
57
+ };
58
+ }
59
+ const retiredTypes = isPlainObject(theme.componentStyles) ? Object.keys(theme.componentStyles) : [];
60
+ return {
61
+ mode: theme.mode === 'dark' ? 'dark' : 'light',
62
+ overrides,
63
+ presets: { count: presetNames.length, names: presetNames },
64
+ componentPresets,
65
+ ...(retiredTypes.length > 0 ? { retired: { componentStyles: retiredTypes } } : {}),
66
+ unknownKeys: Object.keys(record).filter(key => !THEME_TOP_LEVEL_KEYS.includes(key))
67
+ };
68
+ }
69
+ /** Projenin tema kaydı — nesne olmayan (null, metin, dizi) her değer "tema yok" demektir. */
70
+ export function readProjectTheme(payload) {
71
+ const data = isPlainObject(payload) ? payload.data : undefined;
72
+ const theme = isPlainObject(data) ? data.theme : undefined;
73
+ return isPlainObject(theme) ? theme : undefined;
74
+ }
75
+ /* ---- Yazma yolu: şema · birleştirme · silme · normalizasyon · fark ---- */
76
+ const length = z.union([z.number(), z.string()]);
77
+ /**
78
+ * Palet şeması paletin KENDİSİNDEN üretilir (`PALETTE_GROUPS`): grup ve anahtar listesini ikinci
79
+ * kez yazmak, frontend'e yeni bir rol eklendiğinde sessizce eskiyen bir kopya olurdu.
80
+ */
81
+ const paletteSchema = z.object(Object.fromEntries(Object.entries(PALETTE_GROUPS).map(([group, keys]) => [
82
+ group,
83
+ z.object(Object.fromEntries(keys.map(key => [key, z.string()]))).partial().strict().optional()
84
+ ]))).strict();
85
+ const ratioScale = (levels) => z.object(Object.fromEntries(levels.map(level => [level, z.number()]))).partial().strict();
86
+ /**
87
+ * `.strict()` her seviyede: tanınmayan anahtar İSTEK GİTMEDEN düşer. Gerekçe platformun kendi
88
+ * davranışı — `projects.theme` doğrulanmayan bir jsonb blob'u, yani `pallete` yazan istek 200
89
+ * döner, kayıt sessizce yanlış anahtarı taşır ve panel onu körce geri yazar. Şema bu sessizliğin
90
+ * tek kapısıdır (GW-002 sınıfı: tanınmayan anahtar kanalı yoktur, muhafız vardır).
91
+ */
92
+ export const themeInput = z.object({
93
+ mode: z.enum(['light', 'dark']),
94
+ palette: paletteSchema,
95
+ paletteDark: paletteSchema,
96
+ fontFamily: z.string().min(1),
97
+ headingFontFamily: z.string(),
98
+ typeScale: ratioScale(['h1', 'h2', 'h3', 'h4', 'label', 'p', 'small']),
99
+ fontSize: length,
100
+ fontWeight: z.union([z.string(), z.number()]),
101
+ borderRadius: length,
102
+ radiusScale: z.object({ sm: z.number(), lg: z.number(), pill: length }).partial().strict(),
103
+ spacing: length,
104
+ spacingScale: ratioScale(['xs', 'sm', 'md', 'lg', 'xl']),
105
+ shadow: z.object({
106
+ offsetX: z.number(),
107
+ offsetY: z.number(),
108
+ blur: z.number(),
109
+ spread: z.number(),
110
+ color: z.string(),
111
+ opacity: z.number()
112
+ }).partial().strict(),
113
+ defaultInputSize: z.enum(['sm', 'md', 'lg']),
114
+ colors: z.record(z.record(z.string())),
115
+ actions: z.object(Object.fromEntries(['gray', 'red', 'green', 'blue', 'teal', 'pink', 'purple', 'cyan', 'orange', 'yellow', 'white', 'black']
116
+ .map(name => [name, z.string()]))).partial().strict(),
117
+ presets: z.record(z.record(z.unknown())),
118
+ componentStyles: z.record(z.record(z.unknown())),
119
+ // Versiyon gövdesi TOLERANSLI: eski kayıtlar düz CSS nesnesiydi ve normalizasyon onu `props`
120
+ // zarfına alır (`normalizeVersion` ile aynı kural). Şemayı sıkmak eski gövdeyi reddederdi.
121
+ componentPresets: z.record(z.object({
122
+ defaultVersion: z.string(),
123
+ versions: z.record(z.record(z.unknown()))
124
+ }).strict())
125
+ }).partial().strict();
126
+ /**
127
+ * Panelin `themeDataToProjectTheme(data, base)` semantiği: nesneler ÖZYİNELEMELİ birleşir,
128
+ * dizi ve ilkel değer yerine geçer, base'in tanınmayan anahtarları korunur. Böylece bir rengi
129
+ * değiştirmek preset'leri ve bileşen preset'lerini düşürmez.
130
+ */
131
+ export function mergeTheme(base, patch) {
132
+ const result = { ...base };
133
+ for (const [key, value] of Object.entries(patch)) {
134
+ const current = result[key];
135
+ result[key] = isPlainObject(current) && isPlainObject(value)
136
+ ? mergeTheme(current, value)
137
+ : value;
138
+ }
139
+ return result;
140
+ }
141
+ /**
142
+ * Nokta yoluyla silme: `presets.kart`, `componentPresets.button.versions.V2`, `headingFontFamily`.
143
+ *
144
+ * `null` yazmak SİLME değildir — `prune` null'ı yanıttan düşürür ve platform onu kayda yazardı;
145
+ * silmenin açık bir kanalı olması bu yüzden şart. Bulunmayan yol hata değil, `missing`.
146
+ */
147
+ export function unsetThemePaths(theme, paths) {
148
+ const result = structuredClone(theme);
149
+ const missing = [];
150
+ for (const path of paths) {
151
+ const segments = path.split('.').filter(segment => segment !== '');
152
+ let cursor = result;
153
+ let found = true;
154
+ for (const segment of segments.slice(0, -1)) {
155
+ const next = cursor[segment];
156
+ if (!isPlainObject(next)) {
157
+ found = false;
158
+ break;
159
+ }
160
+ cursor = next;
161
+ }
162
+ const last = segments[segments.length - 1];
163
+ if (!found || last === undefined || !Object.hasOwn(cursor, last)) {
164
+ missing.push(path);
165
+ continue;
166
+ }
167
+ delete cursor[last];
168
+ }
169
+ // Son girdisi silinen ADLI KAYIT boş bir kabuk bırakmaz: panel de son preset kaldırılınca
170
+ // anahtarı tamamen siliyor (`themeDataToProjectTheme`), ve `presets: {}` taşıyan bir kayıt
171
+ // "preset'leri var ama boş" gibi okunuyordu (ölçüldü 2026-09-15 canlı koşu).
172
+ for (const key of ['presets', 'componentPresets', 'componentStyles']) {
173
+ const value = result[key];
174
+ if (isPlainObject(value) && Object.keys(value).length === 0)
175
+ delete result[key];
176
+ }
177
+ return { theme: result, missing };
178
+ }
179
+ const LENGTH_FIELDS = ['fontSize', 'spacing', 'borderRadius'];
180
+ /** Çıplak sayı ya da sayısal metin bir CSS uzunluğu DEĞİLDİR; panelin `appendPx`'i ile aynı kural. */
181
+ function withPx(value) {
182
+ if (typeof value === 'number')
183
+ return `${value}px`;
184
+ if (typeof value !== 'string')
185
+ return undefined;
186
+ const text = value.trim();
187
+ return text !== '' && /^-?\d+(\.\d+)?$/.test(text) ? `${text}px` : undefined;
188
+ }
189
+ /**
190
+ * Kural yerine normalizasyon (08 §Kural yerine normalizasyon): türetilebilir olan düzeltilir,
191
+ * yalnız karar gerektiren şey lint'e kalır. Hepsi `gatewayNormalized` ile bildirilir.
192
+ */
193
+ export function normalizeTheme(theme) {
194
+ const result = structuredClone(theme);
195
+ const fixes = [];
196
+ // N-1 — uzunluk alanları
197
+ for (const field of LENGTH_FIELDS) {
198
+ const fixed = withPx(result[field]);
199
+ if (fixed !== undefined) {
200
+ fixes.push(`${field}: ${JSON.stringify(result[field])} → '${fixed}' (CSS uzunluğu birimsiz yazılamaz)`);
201
+ result[field] = fixed;
202
+ }
203
+ }
204
+ if (isPlainObject(result.radiusScale)) {
205
+ const fixed = withPx(result.radiusScale.pill);
206
+ if (fixed !== undefined) {
207
+ fixes.push(`radiusScale.pill: ${JSON.stringify(result.radiusScale.pill)} → '${fixed}'`);
208
+ result.radiusScale.pill = fixed;
209
+ }
210
+ }
211
+ // N-2/N-3 — versiyon zarfı ve boşluk süzgeci
212
+ if (isPlainObject(result.componentPresets)) {
213
+ for (const [type, preset] of Object.entries(result.componentPresets)) {
214
+ if (!isPlainObject(preset) || !isPlainObject(preset.versions))
215
+ continue;
216
+ for (const [name, version] of Object.entries(preset.versions)) {
217
+ if (!isPlainObject(version))
218
+ continue;
219
+ if (!isPlainObject(version.props)) {
220
+ const { appearance, ...body } = version;
221
+ preset.versions[name] = {
222
+ props: Object.keys(body).length > 0 ? { style: body } : {},
223
+ ...(appearance === undefined ? {} : { appearance })
224
+ };
225
+ if (Object.keys(body).length > 0) {
226
+ fixes.push(`componentPresets.${type}.versions.${name}: düz CSS gövdesi { props: { style } } zarfına alındı — versiyon PROP varsayılanı taşır, stil onlardan biridir`);
227
+ }
228
+ }
229
+ const wrapped = preset.versions[name];
230
+ if (isPlainObject(wrapped.appearance)) {
231
+ const kept = Object.fromEntries(Object.entries(wrapped.appearance).filter(([key]) => SPACING_KEYS.includes(key)));
232
+ const dropped = Object.keys(wrapped.appearance).filter(key => !SPACING_KEYS.includes(key));
233
+ if (dropped.length > 0) {
234
+ fixes.push(`componentPresets.${type}.versions.${name}.appearance: ${dropped.join(', ')} düşürüldü — zarf yalnız BOŞLUK taşır, ölçü/hizalama o örneğin ızgaradaki geometrisidir`);
235
+ }
236
+ if (Object.keys(kept).length > 0)
237
+ wrapped.appearance = kept;
238
+ else
239
+ delete wrapped.appearance;
240
+ }
241
+ }
242
+ }
243
+ }
244
+ // N-4/N-5 — stil gövdelerinde kebab-case ve sızmış preset işaretleri
245
+ const styleBodies = [];
246
+ for (const key of ['presets', 'componentStyles']) {
247
+ if (!isPlainObject(result[key]))
248
+ continue;
249
+ for (const [name, body] of Object.entries(result[key])) {
250
+ if (isPlainObject(body))
251
+ styleBodies.push([`${key}.${name}`, body]);
252
+ }
253
+ }
254
+ if (isPlainObject(result.componentPresets)) {
255
+ for (const [type, preset] of Object.entries(result.componentPresets)) {
256
+ if (!isPlainObject(preset) || !isPlainObject(preset.versions))
257
+ continue;
258
+ for (const [name, version] of Object.entries(preset.versions)) {
259
+ const props = isPlainObject(version) ? version.props : undefined;
260
+ const style = isPlainObject(props) ? props.style : undefined;
261
+ if (isPlainObject(style))
262
+ styleBodies.push([`componentPresets.${type}.versions.${name}.props.style`, style]);
263
+ }
264
+ }
265
+ }
266
+ for (const [label, body] of styleBodies) {
267
+ normalizeStyleKeys(body, label, fixes);
268
+ for (const marker of ['$preset', '$componentPreset']) {
269
+ if (!Object.hasOwn(body, marker))
270
+ continue;
271
+ delete body[marker];
272
+ fixes.push(`${label}: '${marker}' düşürüldü — DÜĞÜMÜN kaydıdır, tema stilinin parçası değil (çözümleyici zaten ayıklar)`);
273
+ }
274
+ }
275
+ return { theme: result, fixes };
276
+ }
277
+ /** Yazmanın DOKUNDUĞU yollar; `presets` ve `componentPresets` ad düzeyinde açılır. */
278
+ export function diffThemePaths(base, next) {
279
+ const set = [];
280
+ const unset = [];
281
+ const baseRecord = base;
282
+ const nextRecord = next;
283
+ for (const key of new Set([...Object.keys(baseRecord), ...Object.keys(nextRecord)])) {
284
+ const before = baseRecord[key];
285
+ const after = nextRecord[key];
286
+ if (JSON.stringify(before) === JSON.stringify(after))
287
+ continue;
288
+ // Adlı kayıtlarda hangi PRESET'in değiştiği, "presets değişti"den çok daha bilgilendirici.
289
+ if ((key === 'presets' || key === 'componentPresets') && (isPlainObject(before) || isPlainObject(after))) {
290
+ const beforeEntries = isPlainObject(before) ? before : {};
291
+ const afterEntries = isPlainObject(after) ? after : {};
292
+ for (const name of new Set([...Object.keys(beforeEntries), ...Object.keys(afterEntries)])) {
293
+ if (JSON.stringify(beforeEntries[name]) === JSON.stringify(afterEntries[name]))
294
+ continue;
295
+ if (Object.hasOwn(afterEntries, name))
296
+ set.push(`${key}.${name}`);
297
+ else
298
+ unset.push(`${key}.${name}`);
299
+ }
300
+ continue;
301
+ }
302
+ if (Object.hasOwn(nextRecord, key))
303
+ set.push(key);
304
+ else
305
+ unset.push(key);
306
+ }
307
+ return { set, unset };
308
+ }
309
+ /**
310
+ * Bir TİPİN tema tarafından verilen başlangıç değerleri (S-3).
311
+ *
312
+ * Studio'da bırakılan düğüm ham katalogdan değil, temanın varsayılan versiyonu KAPLANMIŞ
313
+ * katalogdan kurulur (`withThemeComponentDefaults`, `component.functions.ts`): prop'lar
314
+ * `definition.defaults`'un üstüne biner, boşluk ayrı bir `definition.appearance` alanına yazılır
315
+ * ve düğüm `style.$componentPreset` işaretini alır. `component_get`'in `defaults` alanı bunu
316
+ * GÖSTERMİYORDU, yani "yeni bir button ne ile başlar" sorusunun cevabı yanlıştı.
317
+ */
318
+ export function componentPresetDefaults(theme, type) {
319
+ const preset = theme?.componentPresets?.[type];
320
+ if (!isPlainObject(preset))
321
+ return undefined;
322
+ const versions = isPlainObject(preset.versions) ? preset.versions : {};
323
+ const version = String(preset.defaultVersion ?? '');
324
+ const body = versions[version];
325
+ if (!isPlainObject(body))
326
+ return undefined;
327
+ const props = isPlainObject(body.props) ? body.props : {};
328
+ const appearance = isPlainObject(body.appearance) ? body.appearance : undefined;
329
+ return {
330
+ version,
331
+ props,
332
+ ...(appearance ? { appearance } : {}),
333
+ note: `Studio'da bu tipten bırakılan düğüm 'defaults' + BU değerlerle kurulur ve style.$componentPreset: '${version}' işaretini taşır. MCP'nin add_component'i ne katalog varsayılanını ne de bunu yazar — istediğiniz değeri düğüme açıkça verin. Tipin başlangıç değerini değiştirmek: theme_update(componentPresets.${type}).`
334
+ };
335
+ }