@appaflytech/zeyrek-ai-mcp 0.0.41 → 0.0.42
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.
- package/dist/knowledge/componentCatalog.js +27 -97
- package/dist/knowledge/componentEvents.js +1 -18
- package/dist/knowledge/mobileComponentCatalog.js +114 -15
- package/dist/knowledge/mobileComponentEvents.js +41 -0
- package/dist/knowledge/motionResolve.js +252 -0
- package/dist/knowledge/normalize.js +87 -59
- package/dist/knowledge/pageDiscover.js +2 -2
- package/dist/knowledge/platform/batchOps.js +70 -0
- package/dist/knowledge/platform/heroTag.js +153 -0
- package/dist/knowledge/platform/lint.js +128 -6
- package/dist/knowledge/platform/mobile.js +1 -1
- package/dist/knowledge/platform/mobileLayoutLint.js +8 -1
- package/dist/knowledge/platform/mobileRules.js +18 -0
- package/dist/knowledge/platform/pageTransition.js +57 -0
- package/dist/knowledge/platform/web.js +1 -1
- package/dist/knowledge/rules.js +52 -1
- package/dist/knowledge/styleResolveNative.js +88 -4
- package/dist/platform/capture/actions.js +4 -1
- package/dist/platform/capture/capture.js +10 -13
- package/dist/platform/capture/persistent-browser.js +130 -0
- package/dist/resources/examples/flow-list-to-form.json +8 -10
- package/dist/resources/examples/flow-load-table.json +1 -1
- package/dist/resources/examples/mobile-list-detail-form.json +4 -3
- package/dist/resources/examples/mobile-scan-code.json +285 -0
- package/dist/resources/guides/component.md +3 -3
- package/dist/resources/guides/expressions.md +2 -2
- package/dist/resources/guides/merge.md +98 -18
- package/dist/resources/guides/microservice.md +2 -2
- package/dist/resources/guides/mobile-layout.md +2 -1
- package/dist/resources/guides/mobile-publish.md +1 -0
- package/dist/resources/guides/mobile-steps.md +67 -0
- package/dist/resources/guides/mobile.md +27 -55
- package/dist/resources/guides/page.md +182 -121
- package/dist/resources/guides/translation.md +26 -0
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/resources/schemas/ResolveMergeConflictsCommand.json +30 -1
- package/dist/tools/activities.js +3 -1
- package/dist/tools/applications.js +16 -2
- package/dist/tools/components.js +12 -6
- package/dist/tools/designSchemaPatch.js +18 -13
- package/dist/tools/layouts.js +7 -2
- package/dist/tools/merges.js +29 -9
- package/dist/tools/pages.js +129 -127
- package/dist/tools/platformInfo.js +3 -2
- package/dist/tools/profileWrite.js +2 -2
- package/dist/tools/schemaOps.js +132 -34
- package/dist/tools/screenWrite.js +1 -1
- package/dist/tools/screens.js +3 -3
- package/package.json +2 -2
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { IMAGE_SOURCE_PROPS, IMAGE_SOURCE_PROP_NAMES, MOBILE_RULES, REDIRECT_PRESENTATIONS, fileIdImageExpression, missingComponentAdvice, styleReplacement } from './mobileRules.js';
|
|
2
|
+
import { flattenBatchOps } from './batchOps.js';
|
|
3
|
+
import { heroTagProblems, heroTagsAfterWrite, writesHeroTag } from './heroTag.js';
|
|
4
|
+
import { PAGE_TRANSITIONS, resolveTransition } from './pageTransition.js';
|
|
2
5
|
import { findingLine } from './profile.js';
|
|
3
6
|
import { SURFACE_KEYS } from '../styleResolveNative.js';
|
|
7
|
+
import { MOTION_BINDING_TYPES, MOTION_FIELDS, motionFieldBinding, motionValue } from '../motionResolve.js';
|
|
4
8
|
import { DOCUMENT_STYLE_TYPES } from '../styleResolve.js';
|
|
5
9
|
import { listThemeVarNames } from '../themeResolve.js';
|
|
6
10
|
import { closestArgument } from '../../platform/argumentAliases.js';
|
|
11
|
+
/**
|
|
12
|
+
* Uyarının yazımı durdurmadığını söyleyen cümle. OLAYI değil KURALI anlatır: aynı bulgu kuru koşuda
|
|
13
|
+
* (`page_schema_validate`) da döner ve orada hiçbir şey yazılmamıştır — "Yazım platforma gitti" kuru koşuda yanlıştı
|
|
14
|
+
* (TOOL-060, ölçüldü 2026-10-10, gerçek uç). Regresyon: `test/dry-run-wording.test.ts`.
|
|
15
|
+
*/
|
|
16
|
+
const NOT_BLOCKING = 'Yazım reddedilmez, platforma gider.';
|
|
7
17
|
function asRecord(value) {
|
|
8
18
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
9
19
|
}
|
|
@@ -49,6 +59,7 @@ function label(node) {
|
|
|
49
59
|
}
|
|
50
60
|
const GRID_LAYOUTS = new Set(['grid', 'cssgrid']);
|
|
51
61
|
const RELATIVE_UNIT = /\b\d*\.?\d+(em|rem)\b|calc\(/i;
|
|
62
|
+
const TEMPLATE_VALUE = /\{\{[\s\S]*\}\}/;
|
|
52
63
|
/**
|
|
53
64
|
* THEME-012: mobil çizici `var(--ng-…)`'yi yalnız temanın yaydığı değişkenlerden çözer
|
|
54
65
|
* (`compose-native-style.ts` `resolveThemeToken`); web'deki bileşen yerel değişkenleri (`--ng-nav-bg` …)
|
|
@@ -103,6 +114,7 @@ export function profileFindings(profile, input) {
|
|
|
103
114
|
const floorImages = [];
|
|
104
115
|
const namelessIconButtons = [];
|
|
105
116
|
const unknownTokens = [];
|
|
117
|
+
const motionSkips = [];
|
|
106
118
|
const hit = (map, key, target) => map.set(key, [...(map.get(key) ?? []), target]);
|
|
107
119
|
const inspectTokens = (target, value) => {
|
|
108
120
|
for (const name of unknownThemeTokens(value)) {
|
|
@@ -136,6 +148,42 @@ export function profileFindings(profile, input) {
|
|
|
136
148
|
hit(styleHits, `surfaceStyle.${key}`, target);
|
|
137
149
|
}
|
|
138
150
|
};
|
|
151
|
+
// MOB-028: hareket çözücüsü tanımadığı alt anahtarı ve okuyamadığı değeri ATLAR, nesne olmayan grubu hareket
|
|
152
|
+
// saymaz (frontend `resolve-motion.ts`) — yazım kabul edilir, hareket uyarısız eksik çıkar. Uyarı metni
|
|
153
|
+
// çözücününkidir. İfadeye bağlı GRUP (`code`/`translation` sarmalı; `data`sı yalnız tasarım yedeğidir) çalışma
|
|
154
|
+
// anında çözülür: burada okunamaz, denetlenmez. Grubun içindeki TEK alan ifadeye (`code`) bağlıysa o da çalışma
|
|
155
|
+
// anında değerlendirilir (frontend ADR 0020): uyarı değildir, yalnız tasarım yedeği denetlenir. Alanda çözülmeyen
|
|
156
|
+
// yazım (şablon metni, `code` dışı bağ) söylenir (`motionFieldBinding`).
|
|
157
|
+
const inspectMotion = (target, value) => {
|
|
158
|
+
if (boundWrapper(value))
|
|
159
|
+
return;
|
|
160
|
+
const raw = asRecord(value);
|
|
161
|
+
// Veri taşımayan sarmal yalnız BİLİNEN bir değer türüyse sarmaldır: alanı yanlışlıkla `type` diye yazılmış ham
|
|
162
|
+
// grup (`{ type: 'fade', duration: 300 }`) sarmal sanılıp denetimsiz geçiyordu.
|
|
163
|
+
if (raw && typeof raw.type === 'string' && (raw.type === 'static' || MOTION_BINDING_TYPES.includes(raw.type)) && !('data' in raw))
|
|
164
|
+
return;
|
|
165
|
+
const group = propValue(value);
|
|
166
|
+
if (group === undefined || group === null || group === '' || (typeof group === 'string' && TEMPLATE_VALUE.test(group)))
|
|
167
|
+
return;
|
|
168
|
+
const fields = asRecord(group);
|
|
169
|
+
if (!fields) {
|
|
170
|
+
motionSkips.push(`${target}: motionStyle: ${typeof group === 'string' ? `'${group}'` : JSON.stringify(group)} nesne değil — alanlar nesnenin içinde yazılır ({ enter: 'fade' }), hareket yok sayılır`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
for (const [key, wrapped] of Object.entries(fields)) {
|
|
174
|
+
const binding = motionFieldBinding(key, wrapped);
|
|
175
|
+
if (binding?.kind === 'unresolved')
|
|
176
|
+
motionSkips.push(`${target}: ${binding.warning}`);
|
|
177
|
+
const field = binding ? binding.fallback : propValue(wrapped);
|
|
178
|
+
if (field === undefined || field === null || field === '')
|
|
179
|
+
continue;
|
|
180
|
+
const warning = MOTION_FIELDS.includes(key)
|
|
181
|
+
? motionValue(key, field).warning
|
|
182
|
+
: `motionStyle.${key}: tanınmayan alan, atlandı`;
|
|
183
|
+
if (warning)
|
|
184
|
+
motionSkips.push(`${target}: ${warning}`);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
139
187
|
const inspectNode = (node) => {
|
|
140
188
|
const target = label(node);
|
|
141
189
|
const type = String(node.type ?? '');
|
|
@@ -154,6 +202,7 @@ export function profileFindings(profile, input) {
|
|
|
154
202
|
gridNodes.push(`${target} (layout:${layout})`);
|
|
155
203
|
inspectAppearance(target, asRecord(node.appearance));
|
|
156
204
|
inspectSurface(target, props.surfaceStyle);
|
|
205
|
+
inspectMotion(target, props.motionStyle);
|
|
157
206
|
for (const group of STYLE_GROUP_PROPS)
|
|
158
207
|
inspectTokens(target, props[group]);
|
|
159
208
|
if (type === 'image' && !imageHasDefiniteHeight(asRecord(node.appearance)))
|
|
@@ -182,7 +231,9 @@ export function profileFindings(profile, input) {
|
|
|
182
231
|
walk(input.nodes, inspectNode);
|
|
183
232
|
const savedTypes = new Map();
|
|
184
233
|
walk(input.savedNodes, node => savedTypes.set(label(node), String(node.type ?? '')));
|
|
185
|
-
|
|
234
|
+
// Toplu yamanın zarfı açılır: içindeki op'lar tek tek yazılmış gibi denetlenir (TOOL-057).
|
|
235
|
+
const ops = flattenBatchOps(input.ops ?? []);
|
|
236
|
+
for (const op of ops) {
|
|
186
237
|
const target = typeof op.target === 'string' ? op.target : '?';
|
|
187
238
|
if (op.op === 'add_component')
|
|
188
239
|
walk(op.component, inspectNode);
|
|
@@ -203,6 +254,8 @@ export function profileFindings(profile, input) {
|
|
|
203
254
|
hit(styleHits, 'hoverStyle', target);
|
|
204
255
|
if (prop === 'surfaceStyle')
|
|
205
256
|
inspectSurface(target, op.value);
|
|
257
|
+
if (prop === 'motionStyle')
|
|
258
|
+
inspectMotion(target, op.value);
|
|
206
259
|
if (STYLE_GROUP_PROPS.has(prop))
|
|
207
260
|
inspectTokens(target, op.value);
|
|
208
261
|
// MOB-011: var olan düğümün tipi op'ta yok — tip biliniyorsa (aynı yazımda eklendi) ona, yoksa prop adına bakılır.
|
|
@@ -220,7 +273,7 @@ export function profileFindings(profile, input) {
|
|
|
220
273
|
message: [
|
|
221
274
|
blockNodes.length > 0 ? `blok (12 sütun ızgara) mobil çizerde yok sayılır: ${[...new Set(blockNodes)].join(', ')}` : '',
|
|
222
275
|
gridNodes.length > 0 ? `grid/cssgrid yerleşimi yok, kap flex çizilir: ${gridNodes.join(', ')}` : ''
|
|
223
|
-
].filter(Boolean).join(' · ') + ". Sıra dizi indeksinden, yerleşim flex'ten gelir (direction/gap/horizontalAlign).
|
|
276
|
+
].filter(Boolean).join(' · ') + ". Sıra dizi indeksinden, yerleşim flex'ten gelir (direction/gap/horizontalAlign). " + NOT_BLOCKING,
|
|
224
277
|
guide: "platform_guide('mobile')"
|
|
225
278
|
});
|
|
226
279
|
}
|
|
@@ -229,7 +282,33 @@ export function profileFindings(profile, input) {
|
|
|
229
282
|
code: MOBILE_RULES['MOB-002'].id,
|
|
230
283
|
severity: 'warn',
|
|
231
284
|
target: [...new Set(targets)].join(', '),
|
|
232
|
-
message: `'${key}' mobilde çizilmez — ${styleReplacement(key.split(' ')[0] ?? key)}.
|
|
285
|
+
message: `'${key}' mobilde çizilmez — ${styleReplacement(key.split(' ')[0] ?? key)}. ${NOT_BLOCKING}`
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
// MOB-030: aynı sayfada aynı statik heroTag (frontend `hero-tag.ts`; ayna heroTag.ts). Sayfanın yazımdan SONRAKİ
|
|
289
|
+
// etiketleri okunur ve yalnız bu yazımın dokunduğu bir düğümü içeren yineleme söylenir — kayıtlı ağaçta zaten duran
|
|
290
|
+
// yineleme ilgisiz her yazımda yinelenmez. Bilgidir: yineleme yalnız sayfa bir yakınlaşmanın HEDEFİ olduğunda
|
|
291
|
+
// belirsizdir; kaynak sayfada aynı etiketli kartlar olağandır (kaynak dokunulan düğümdür).
|
|
292
|
+
const hero = heroTagsAfterWrite({ nodes: input.nodes, savedNodes: input.savedNodes, ops });
|
|
293
|
+
const heroDuplicates = [...new Set(hero.entries.filter(entry => entry.tag !== undefined).map(entry => entry.tag))]
|
|
294
|
+
.map(tag => hero.entries.filter(entry => entry.tag === tag))
|
|
295
|
+
.filter(group => group.length > 1 && group.some(entry => hero.written.has(entry.elementId)));
|
|
296
|
+
if (heroDuplicates.length > 0) {
|
|
297
|
+
findings.push({
|
|
298
|
+
code: MOBILE_RULES['MOB-030'].id,
|
|
299
|
+
severity: 'info',
|
|
300
|
+
target: heroDuplicates.flatMap(group => group.map(entry => entry.elementId)).join(', '),
|
|
301
|
+
message: `${heroDuplicates.flatMap(group => heroTagProblems(group)).join(' · ')}. Yalnız bu sayfa bir yakınlaşmanın HEDEFİ olduğunda önemlidir: hizalama kutusu tektir. Kaynak sayfada aynı etiketli kartlar ve liste satırları sorun değildir (kaynak dokunulan düğümdür); hedefte ayrı öğelerse ayrı etiket verin.`,
|
|
302
|
+
guide: "platform_guide('page') §Kural 1g-3"
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (motionSkips.length > 0) {
|
|
306
|
+
findings.push({
|
|
307
|
+
code: MOBILE_RULES['MOB-028'].id,
|
|
308
|
+
severity: 'warn',
|
|
309
|
+
target: [...new Set(motionSkips.map(entry => entry.split(':')[0] ?? entry))].join(', '),
|
|
310
|
+
message: `motionStyle'da çözücünün atladığı alan var, hareket eksik çıkar: ${[...new Set(motionSkips)].join(' · ')}. Alanlar: ${MOTION_FIELDS.join(', ')}. ${NOT_BLOCKING}`,
|
|
311
|
+
guide: "platform_guide('mobile')"
|
|
233
312
|
});
|
|
234
313
|
}
|
|
235
314
|
if (unknownTokens.length > 0) {
|
|
@@ -237,7 +316,7 @@ export function profileFindings(profile, input) {
|
|
|
237
316
|
code: 'THEME-012',
|
|
238
317
|
severity: 'warn',
|
|
239
318
|
target: [...new Set(unknownTokens.map(entry => entry.split(':')[0] ?? entry))].join(', '),
|
|
240
|
-
message: `temada tanımlı olmayan jeton cihazda çözülmez (zemin saydam kalır, iOS gölgesi içeriğe düşer): ${[...new Set(unknownTokens)].join(' · ')}. Geçerli adlar temanın yaydığı değişkenlerdir (theme_get); bilerek kullanıyorsanız yedek verin: var(--ad, #fff).
|
|
319
|
+
message: `temada tanımlı olmayan jeton cihazda çözülmez (zemin saydam kalır, iOS gölgesi içeriğe düşer): ${[...new Set(unknownTokens)].join(' · ')}. Geçerli adlar temanın yaydığı değişkenlerdir (theme_get); bilerek kullanıyorsanız yedek verin: var(--ad, #fff). ${NOT_BLOCKING}`,
|
|
241
320
|
guide: "platform_guide('theme')"
|
|
242
321
|
});
|
|
243
322
|
}
|
|
@@ -291,12 +370,14 @@ export function profileFindings(profile, input) {
|
|
|
291
370
|
code: MOBILE_RULES['MOB-011'].id,
|
|
292
371
|
severity: 'warn',
|
|
293
372
|
target: fileIdSources.map(source => source.target).join(', '),
|
|
294
|
-
message: `${[...new Set(fileIdSources.map(source => `'${source.expression}'`))].join(', ')} '.value' ile bitiyor — kayıttan okunan FILE alanı { type: 'ref', value: <dosya id>, absolute_path: <url> } döner ve '.value' dosya KİMLİĞİDİR (GUID), görsel çizilmez. Nesnenin kendisini verin ('${object}') ya da '.absolute_path' yazın. Gateway alan tipini sayfa şemasından bilemez: yol FILE alanı değilse (ör. URL taşıyan metin alanı) uyarıyı yok sayın.
|
|
373
|
+
message: `${[...new Set(fileIdSources.map(source => `'${source.expression}'`))].join(', ')} '.value' ile bitiyor — kayıttan okunan FILE alanı { type: 'ref', value: <dosya id>, absolute_path: <url> } döner ve '.value' dosya KİMLİĞİDİR (GUID), görsel çizilmez. Nesnenin kendisini verin ('${object}') ya da '.absolute_path' yazın. Gateway alan tipini sayfa şemasından bilemez: yol FILE alanı değilse (ör. URL taşıyan metin alanı) uyarıyı yok sayın. ${NOT_BLOCKING}`,
|
|
295
374
|
guide: "platform_guide('mobile') §Dosya ve fotoğraf"
|
|
296
375
|
});
|
|
297
376
|
}
|
|
298
377
|
const infoSteps = new Map();
|
|
299
378
|
const badPresentation = new Map();
|
|
379
|
+
const badTransition = new Map();
|
|
380
|
+
const unusedTransition = new Map();
|
|
300
381
|
walkSteps(input.steps, step => {
|
|
301
382
|
const type = String(step.type ?? '');
|
|
302
383
|
if (profile.steps.infoOnly.has(type))
|
|
@@ -307,10 +388,24 @@ export function profileFindings(profile, input) {
|
|
|
307
388
|
return;
|
|
308
389
|
const args = asRecord(step.args);
|
|
309
390
|
const presentation = args?.presentation;
|
|
391
|
+
const stepId = String(step.stepId ?? step.name ?? '?');
|
|
392
|
+
// MOB-029: geçişin kararını çalışma zamanının çözücüsü verir (ayna: pageTransition.ts) — lint ikinci bir kural
|
|
393
|
+
// yazmaz. Alan YOKSA bulgu yok: adım uygulamanın varsayılanını kullanır. Sekme hedefi burada bilinmez
|
|
394
|
+
// (lint sekme ayarını görmez); o durumda da geçiş kullanılmaz ve rehber bunu söyler.
|
|
395
|
+
// Akış araçları bu lint'i BAŞARILI yazımdan sonra koşar ve küme dışı değeri backend 422 ile önce reddeder: uyarı
|
|
396
|
+
// dalı bugün yalnız backend'in doğrulamadığı bir yazım yolunda konuşur (MOB-008 ile aynı durum); bilgi dalı canlıdır.
|
|
397
|
+
if (args?.transition !== undefined && args.transition !== null && args.transition !== '') {
|
|
398
|
+
const resolved = resolveTransition({ step: args.transition, presentation }, { reduceMotion: false });
|
|
399
|
+
const written = typeof args.transition === 'string' ? args.transition : JSON.stringify(args.transition);
|
|
400
|
+
if (resolved.warnings.length > 0)
|
|
401
|
+
hit(badTransition, written, stepId);
|
|
402
|
+
else if (resolved.fixedBy === 'modal')
|
|
403
|
+
hit(unusedTransition, written, stepId);
|
|
404
|
+
}
|
|
310
405
|
if (presentation === undefined || presentation === null || presentation === '')
|
|
311
406
|
return;
|
|
312
407
|
if (!REDIRECT_PRESENTATIONS.has(String(presentation))) {
|
|
313
|
-
hit(badPresentation, String(presentation),
|
|
408
|
+
hit(badPresentation, String(presentation), stepId);
|
|
314
409
|
}
|
|
315
410
|
});
|
|
316
411
|
for (const [value, stepIds] of badPresentation) {
|
|
@@ -322,6 +417,24 @@ export function profileFindings(profile, input) {
|
|
|
322
417
|
guide: "platform_guide('mobile')"
|
|
323
418
|
});
|
|
324
419
|
}
|
|
420
|
+
for (const [value, stepIds] of badTransition) {
|
|
421
|
+
findings.push({
|
|
422
|
+
code: MOBILE_RULES['MOB-029'].id,
|
|
423
|
+
severity: 'warn',
|
|
424
|
+
target: stepIds.join(', '),
|
|
425
|
+
message: `redirect.transition '${value}' tanınmıyor — ${PAGE_TRANSITIONS.join(', ')} bekleniyor. Backend doğrulayıcısı 422 döner; alanı yazmazsanız adım uygulamanın varsayılan geçişiyle açılır.`,
|
|
426
|
+
guide: "platform_guide('page') §Kural 1g-2"
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
for (const [value, stepIds] of unusedTransition) {
|
|
430
|
+
findings.push({
|
|
431
|
+
code: MOBILE_RULES['MOB-029'].id,
|
|
432
|
+
severity: 'info',
|
|
433
|
+
target: stepIds.join(', '),
|
|
434
|
+
message: `redirect.transition '${value}' modal sunumla birlikte yazılmış — modal sistemin kağıt animasyonuyla açılır, geçiş kullanılmaz (hedef bir sekme sayfasıysa da kullanılmaz: sekme değişir).`,
|
|
435
|
+
guide: "platform_guide('page') §Kural 1g-2"
|
|
436
|
+
});
|
|
437
|
+
}
|
|
325
438
|
for (const [type, stepIds] of infoSteps) {
|
|
326
439
|
findings.push({
|
|
327
440
|
code: MOBILE_RULES['MOB-005'].id,
|
|
@@ -345,6 +458,15 @@ function walkSteps(value, visit) {
|
|
|
345
458
|
walkSteps(step.steps, visit);
|
|
346
459
|
}
|
|
347
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Denetim bu yama için kayıtlı ağacı ister mi (`ProfileLintInput.savedNodes`): `style` yazımında hedefin tipi (belge
|
|
463
|
+
* CSS'i taşıyan düğüm mü — MOB-002), `heroTag` yazımında sayfanın öteki etiketleri (MOB-030). Çağıran ağacı yalnız bu
|
|
464
|
+
* durumda okur. Denetimle AYNI op listesine bakar (zarf açılır): 2026-10-10'da denetim zarfı açıp çağıran açmayınca
|
|
465
|
+
* kayıtlı `html` düğümünün belge CSS'i zarf içinde yanlış MOB-002 aldı (regresyon `test/batch-profile-lint.test.ts`).
|
|
466
|
+
*/
|
|
467
|
+
export function profileLintReadsSavedNodes(ops) {
|
|
468
|
+
return writesHeroTag(ops) || flattenBatchOps(ops).some(op => (op.op === 'replace_prop' || op.op === 'set_binding') && op.prop === 'style');
|
|
469
|
+
}
|
|
348
470
|
/** Bulguları `layoutWarnings[]` satırlarına çevirir. */
|
|
349
471
|
export function findingLines(findings) {
|
|
350
472
|
return findings.map(findingLine);
|
|
@@ -116,7 +116,7 @@ export const mobileProfile = Object.freeze({
|
|
|
116
116
|
},
|
|
117
117
|
style: { freeStyle: false, hoverStyle: false, resolver: 'native' },
|
|
118
118
|
steps: {
|
|
119
|
-
registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'navigate', 'storage', 'setcookie', 'download', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings', 'ad-show', 'back']),
|
|
119
|
+
registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'navigate', 'storage', 'setcookie', 'download', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings', 'ad-show', 'back', 'scan-code']),
|
|
120
120
|
aliases: STEP_ALIASES,
|
|
121
121
|
infoOnly: new Set(['download', 'setcookie'])
|
|
122
122
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MOBILE_DROPPABLE_TYPES, MOBILE_OUT_OF_FLOW_TYPES, MOBILE_TRANSPARENT_WRAPPER_TYPES } from '../mobileComponentCatalog.js';
|
|
1
|
+
import { MOBILE_DROPPABLE_TYPES, MOBILE_OPTIONAL_VIEW_INDEX, MOBILE_OUT_OF_FLOW_TYPES, MOBILE_TRANSPARENT_WRAPPER_TYPES } from '../mobileComponentCatalog.js';
|
|
2
2
|
import { PAGE_CONTAINER_ELEMENT_ID } from '../normalize.js';
|
|
3
3
|
import { OVERLAY_TYPES, firstDrawn, safeAreaEdges, topSafeAreaMissing } from './mobile.js';
|
|
4
4
|
import { MOBILE_RULES } from './mobileRules.js';
|
|
@@ -82,6 +82,8 @@ export function mobileLayoutFindings(input) {
|
|
|
82
82
|
return;
|
|
83
83
|
if (type === 'safe-area' && ancestors.length === 0)
|
|
84
84
|
return;
|
|
85
|
+
if (isOptionalView(node, ancestors.at(-1)))
|
|
86
|
+
return;
|
|
85
87
|
findings.push({ ...rule('MOB-018', label(node), `boş ${type}: ekranda boş şerit bırakır${type === 'safe-area' ? ` (edges: '${safeAreaEdges(node)}' payı kadar)` : ''}. İçeriğini ekleyin ya da silin`), severity: 'info' });
|
|
86
88
|
});
|
|
87
89
|
if (screenRoot)
|
|
@@ -282,6 +284,11 @@ function key(finding) {
|
|
|
282
284
|
function rule(code, target, message) {
|
|
283
285
|
return { code: MOBILE_RULES[code].id, severity: MOBILE_RULES[code].severity, target, message, guide: "platform_guide('mobile-layout')" };
|
|
284
286
|
}
|
|
287
|
+
/** Sabit görünümlü sahibinin bilerek boş bırakılabilen görünümü: veri yokken açılan görünüm (`MOBILE_OPTIONAL_VIEW_INDEX`). */
|
|
288
|
+
function isOptionalView(node, parent) {
|
|
289
|
+
const index = MOBILE_OPTIONAL_VIEW_INDEX[String(parent?.type ?? '')];
|
|
290
|
+
return index !== undefined && asNodes(parent?.children).filter(child => child.type === 'view')[index] === node;
|
|
291
|
+
}
|
|
285
292
|
function asNodes(value) {
|
|
286
293
|
return Array.isArray(value) ? value.filter((item) => !!item && typeof item === 'object') : [];
|
|
287
294
|
}
|
|
@@ -114,6 +114,24 @@ export const MOBILE_RULES = {
|
|
|
114
114
|
severity: 'info',
|
|
115
115
|
sourceRef: 'zeyrek-mcp guides/mobile-layout.md §Sözleşme S5/S7/S11',
|
|
116
116
|
summary: 'Ölçek dışı değer: özel boşluk ya da sayısal gap 0/4/8/16/24/32 dışında, ham yarıçap, px yazı boyu'
|
|
117
|
+
},
|
|
118
|
+
'MOB-028': {
|
|
119
|
+
id: 'MOB-028',
|
|
120
|
+
severity: 'warn',
|
|
121
|
+
sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/motion/resolve-motion.ts (tanınmayan alan ve okunamayan değer atlanır)',
|
|
122
|
+
summary: "Mobil motionStyle'da tanınmayan alt anahtar ya da küme/aralık dışı değer — çözücü atlar, hareket eksik çıkar"
|
|
123
|
+
},
|
|
124
|
+
'MOB-029': {
|
|
125
|
+
id: 'MOB-029',
|
|
126
|
+
severity: 'warn',
|
|
127
|
+
sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/motion/resolve-transition.ts (çözücü; ayna pageTransition.ts); zeyrek-backend FlowDtoValidator (StepTransition); simülatör ölçümü 2026-10-10',
|
|
128
|
+
summary: "redirect.transition ya da properties.mobile.transition kümede değil (default | none | fade | slide | slideUp) — adımda backend 422; modal sunumda yazılan geçiş kullanılmaz (bilgi)"
|
|
129
|
+
},
|
|
130
|
+
'MOB-030': {
|
|
131
|
+
id: 'MOB-030',
|
|
132
|
+
severity: 'info',
|
|
133
|
+
sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/motion/hero-tag.ts (kural; ayna heroTag.ts); zeyrek-frontend/docs/adr/0022-hero-yolu.md; simülatör ölçümü 2026-10-10',
|
|
134
|
+
summary: "Aynı sayfada aynı statik heroTag'i birden çok düğüm taşıyor — sayfa yakınlaşmanın hedefi olduğunda ilk çizilen hizalanır; kaynak sayfada sorun değildir"
|
|
117
135
|
}
|
|
118
136
|
};
|
|
119
137
|
/**
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobil sayfa geçişinin gateway aynası: sözlük ve çözücü. Kaynak frontend'dedir
|
|
3
|
+
* (`zeyrek-frontend/packages/ng-apps-ui-native/src/motion/` — `transition-fields.ts`, `resolve-transition.ts`;
|
|
4
|
+
* uygulama ayarının okuyucusu `src/shell/mobile-transition.ts`). Bu dosya onun kopyasıdır ve
|
|
5
|
+
* `test/page-transition-parity.test.ts` ile kilitlidir: frontend'in altın vakaları (`transition-golden.json`) aynı
|
|
6
|
+
* girdiden aynı çıktıyı ve harfi harfine aynı uyarıyı vermeli; aynı dosyanın `values` listesi backend enum'unun
|
|
7
|
+
* (`StepTransition`) kopyasına da kilitlidir. Test kırılınca test düzeltilmez, bu dosya senkronlanır.
|
|
8
|
+
*
|
|
9
|
+
* Değerler ürünün dilidir; yerel yığının adları (`slide_from_right`) burada geçmez. Kuralların ölçümü:
|
|
10
|
+
* simülatör, 2026-10-10 (`zeyrek-frontend/docs/features/mobile-runtime.md` §Sayfa geçişi).
|
|
11
|
+
*/
|
|
12
|
+
export const PAGE_TRANSITIONS = ['default', 'none', 'fade', 'slide', 'slideUp'];
|
|
13
|
+
/** Boş değer ne değer ne uyarıdır (seçilmemiş); kümede olmayan değer `value` taşımaz. */
|
|
14
|
+
export function transitionValue(raw, source) {
|
|
15
|
+
if (raw === undefined || raw === null || (typeof raw === 'string' && raw.trim() === ''))
|
|
16
|
+
return {};
|
|
17
|
+
if (typeof raw === 'string' && PAGE_TRANSITIONS.includes(raw))
|
|
18
|
+
return { value: raw };
|
|
19
|
+
return {
|
|
20
|
+
warning: `${source}: ${typeof raw === 'string' ? `'${raw}'` : JSON.stringify(raw)} geçersiz — ${PAGE_TRANSITIONS.join(', ')}, atlandı`
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export const TRANSITION_SOURCES = { step: 'redirect.transition', application: 'properties.mobile.transition' };
|
|
24
|
+
/**
|
|
25
|
+
* "Bu gezinmede hangi geçiş oynar." Öncelik: adım → uygulama varsayılanı → `default`; okunamayan değer uyarısıyla
|
|
26
|
+
* atlanır. Sekme hedefi ve `modal` sunum seçimi kullanmaz (sekme önce gelir). "Hareketi azalt"ta seçilmiş geçiş
|
|
27
|
+
* `none` olur, `default` sisteme kalır.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveTransition(input, context) {
|
|
30
|
+
const step = transitionValue(input.step, TRANSITION_SOURCES.step);
|
|
31
|
+
const application = transitionValue(input.application, TRANSITION_SOURCES.application);
|
|
32
|
+
const warnings = [step.warning, application.warning].filter((warning) => warning !== undefined);
|
|
33
|
+
if (input.tabTarget)
|
|
34
|
+
return { transition: 'default', fixedBy: 'tab', warnings };
|
|
35
|
+
if (input.presentation === 'modal')
|
|
36
|
+
return { transition: 'default', fixedBy: 'modal', warnings };
|
|
37
|
+
const chosen = step.value ?? application.value ?? 'default';
|
|
38
|
+
return { transition: context.reduceMotion && chosen !== 'default' ? 'none' : chosen, warnings };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `properties.mobile.transition`'ın HAM değeri; alan yoksa `undefined`. Frontend'in `readMobileTransition`'ı doğrulanmış
|
|
42
|
+
* değeri verir ve bozuk olanı sessizce düşürür; gateway uyarmak için ham değeri ister — adı bu yüzden farklı.
|
|
43
|
+
*/
|
|
44
|
+
export function rawMobileTransition(properties) {
|
|
45
|
+
const mobile = properties?.mobile;
|
|
46
|
+
return mobile && typeof mobile === 'object' ? mobile.transition : undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* MOB-029'un uygulama ayarı ayağı. Backend `properties`'i doğrulamaz (jsonb serbest) ve cihazın okuyucusu sözlük
|
|
50
|
+
* dışı değeri SESSİZCE düşürür — uyarının verildiği tek yer burasıdır.
|
|
51
|
+
*/
|
|
52
|
+
export function mobileTransitionProblems(properties) {
|
|
53
|
+
const { warning } = transitionValue(rawMobileTransition(properties), TRANSITION_SOURCES.application);
|
|
54
|
+
return warning
|
|
55
|
+
? [`${warning} — cihaz değeri okumaz, geçişini seçmemiş yönlendirme adımları sistemin geçişiyle açılır`]
|
|
56
|
+
: [];
|
|
57
|
+
}
|
|
@@ -20,7 +20,7 @@ export const webProfile = Object.freeze({
|
|
|
20
20
|
},
|
|
21
21
|
style: { freeStyle: true, hoverStyle: true, resolver: 'css' },
|
|
22
22
|
steps: {
|
|
23
|
-
registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'download', 'storage', 'setcookie', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings', 'ad-show', 'back']),
|
|
23
|
+
registered: new Set([...ENGINE_STEP_TYPES, 'alert', 'confirm', 'redirect', 'download', 'storage', 'setcookie', 'share', 'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'permission', 'open-settings', 'ad-show', 'back', 'scan-code']),
|
|
24
24
|
aliases: STEP_ALIASES,
|
|
25
25
|
infoOnly: new Set()
|
|
26
26
|
},
|
package/dist/knowledge/rules.js
CHANGED
|
@@ -173,6 +173,7 @@ const flowBranchNames = {
|
|
|
173
173
|
'open-settings': ['always'],
|
|
174
174
|
'ad-show': ['always', 'error'],
|
|
175
175
|
back: ['always'],
|
|
176
|
+
'scan-code': ['always', 'error'],
|
|
176
177
|
end: []
|
|
177
178
|
};
|
|
178
179
|
/**
|
|
@@ -185,7 +186,7 @@ export const knownFlowStepTypes = new Set([
|
|
|
185
186
|
'start', 'end', 'query', 'setter', 'download', 'condition', 'redirect', 'setcookie', 'alert', 'confirm',
|
|
186
187
|
'microservice', 'validation', 'flow-trigger', 'storage', 'navigate', 'code', 'function', 'share',
|
|
187
188
|
'haptic', 'location', 'notification-permission', 'notification-schedule', 'notification-cancel', 'bpm',
|
|
188
|
-
'permission', 'open-settings', 'ad-show', 'back'
|
|
189
|
+
'permission', 'open-settings', 'ad-show', 'back', 'scan-code'
|
|
189
190
|
]);
|
|
190
191
|
/**
|
|
191
192
|
* Cihaz yeteneği adımlarının kapalı kümeleri — `@ng-apps/flow` `tasks/device/device.functions.ts`
|
|
@@ -199,6 +200,12 @@ export const locationAccuracies = new Set(['low', 'balanced', 'high']);
|
|
|
199
200
|
* ve `adBanner.unit` bu adla mağaza ayarındaki yerleşimi anar.
|
|
200
201
|
*/
|
|
201
202
|
export const adPlacementNamePattern = /^[a-z][A-Za-z0-9]{0,39}$/;
|
|
203
|
+
/**
|
|
204
|
+
* `scan-code` adımının kod türleri — `@ng-apps/flow` `SCAN_CODE_TYPES`'ın aynası (kök ADR 0017); altın dosya
|
|
205
|
+
* `scan-code-golden.json` ile kilitlidir (`test/flow-step-parity.test.ts`), backend `ScanCodeType` aynı kümeyle 422 döner.
|
|
206
|
+
* `upc_a` bilerek yok: UPC-A etiketi iki platformda `ean13` ve on üç haneyle döner.
|
|
207
|
+
*/
|
|
208
|
+
export const scanCodeTypes = new Set(['aztec', 'codabar', 'code39', 'code93', 'code128', 'datamatrix', 'ean8', 'ean13', 'itf14', 'pdf417', 'qr', 'upc_e']);
|
|
202
209
|
/** `permission` adımının kipleri — `@ng-apps/flow` `PERMISSION_MODES`'un aynası (kök ADR 0010); tür adını gateway bilmez, backend doğrular. */
|
|
203
210
|
export const permissionStepModes = new Set(['request', 'check']);
|
|
204
211
|
/**
|
|
@@ -2710,6 +2717,50 @@ export const policyRules = [
|
|
|
2710
2717
|
return findings;
|
|
2711
2718
|
})
|
|
2712
2719
|
},
|
|
2720
|
+
{
|
|
2721
|
+
id: 'FLOW-042',
|
|
2722
|
+
module: 'flow',
|
|
2723
|
+
appliesTo: ['create', 'update'],
|
|
2724
|
+
sourceRef: "Kod tarama adımının argümanı StepManagementArgument'ın mevcut alanlarındadır (kök docs/adr/0017-kod-tarama-akis-adimi.md): scan-code.value = izinli kod türleri (sözlükten virgüllü küme, DÜZ METİN; boş → hepsi), scan-code.title = ekrandaki yönerge (metin şablonu, FLOW-022). Backend FlowDtoValidator her adı ScanCodeType kümesiyle BİREBİR karşılaştırır (büyük harf ve ifade sarmalı 422, FlowStepScanCodeTypeUnknown); çalışma zamanı sözlük dışı adda ekranı açmaz, `invalid` döner. upc_a sözlükte yoktur: UPC-A etiketi iki platformda ean13 ve on üç haneyle (başında 0) döner. types / formats / barcodeTypes anahtarları sözleşmede yok, 200 ile sessizce düşer ve adım bütün türleri okur.",
|
|
2725
|
+
check: definition => flowAllSteps(definition)
|
|
2726
|
+
.filter(step => step.type === 'scan-code')
|
|
2727
|
+
.flatMap((step) => {
|
|
2728
|
+
const args = (step.args ?? {});
|
|
2729
|
+
const raw = typeof args.value === 'string' ? args.value : '';
|
|
2730
|
+
const names = raw.split(',').map(name => name.trim()).filter(name => name !== '');
|
|
2731
|
+
const unknown = names.filter(name => !scanCodeTypes.has(name));
|
|
2732
|
+
const stray = ['types', 'formats', 'barcodeTypes', 'codeTypes', 'type'].filter(key => args[key] !== undefined && args[key] !== null && args[key] !== '');
|
|
2733
|
+
const allowed = [...scanCodeTypes].join(', ');
|
|
2734
|
+
const findings = [];
|
|
2735
|
+
if (unknown.length > 0) {
|
|
2736
|
+
const unquoted = raw.replace(/^['"`](.*)['"`]$/, '$1');
|
|
2737
|
+
const lowered = unknown.filter(name => scanCodeTypes.has(name.toLowerCase()));
|
|
2738
|
+
findings.push({
|
|
2739
|
+
ruleId: 'FLOW-042',
|
|
2740
|
+
severity: 'block',
|
|
2741
|
+
message: unquoted !== raw && unquoted.split(',').every(name => scanCodeTypes.has(name.trim()))
|
|
2742
|
+
? `'${flowStepLabel(step)}' scan-code adımının kod türleri tırnaklı yazılmış: ${raw}. Bu alan DÜZ METİNDİR, ifade değil — args.value: '${unquoted}' yazın.`
|
|
2743
|
+
: /\{\{|\}\}|[()[\]]|\bstate\b|\bSteps\b/.test(raw)
|
|
2744
|
+
? `'${flowStepLabel(step)}' scan-code adımının kod türleri ifade olarak yazılmış: ${raw}. Bu alan DÜZ METİNDİR ve olduğu gibi okunur (platform 422 döner) — türleri virgülle yazın ya da hepsi için boş bırakın. Kullanılabilir: ${allowed}.`
|
|
2745
|
+
: unknown.includes('upc_a')
|
|
2746
|
+
? `'${flowStepLabel(step)}' scan-code adımında 'upc_a' sözlükte yok: UPC-A etiketi iki platformda 'ean13' türüyle ve on üç haneyle (başında 0) döner — 'ean13' yazın (platform 422 döner). Kullanılabilir: ${allowed}.`
|
|
2747
|
+
: lowered.length > 0
|
|
2748
|
+
? `'${flowStepLabel(step)}' scan-code adımının kod türü büyük harfle yazılmış: ${lowered.join(', ')}. Adlar küçük harftir ve birebir karşılaştırılır (platform 422 döner) — ${lowered.map(name => `'${name.toLowerCase()}'`).join(', ')} yazın.`
|
|
2749
|
+
: `'${flowStepLabel(step)}' scan-code adımının kod türü tanınmıyor: ${unknown.join(', ')}; platform 422 döner. Kullanılabilir: ${allowed} (boş → hepsi).`,
|
|
2750
|
+
guide: 'ngapps://guides/mobile-steps'
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
if (stray.length > 0) {
|
|
2754
|
+
findings.push({
|
|
2755
|
+
ruleId: 'FLOW-042',
|
|
2756
|
+
severity: names.length > 0 ? 'warn' : 'block',
|
|
2757
|
+
message: `'${flowStepLabel(step)}' scan-code adımında args.${stray.join('/args.')} alanı var: bu anahtar StepManagementArgument sözleşmesinde YOK, platform onu 200 ile sessizce düşürür ve adım bütün kod türlerini okur. Türler args.value'ya virgüllü düz metin olarak yazılır (ör. 'qr,ean13'). Kullanılabilir: ${allowed}.`,
|
|
2758
|
+
guide: 'ngapps://guides/mobile-steps'
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
return findings;
|
|
2762
|
+
})
|
|
2763
|
+
},
|
|
2713
2764
|
{
|
|
2714
2765
|
id: 'FLOW-041',
|
|
2715
2766
|
module: 'flow',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { motionFieldBinding, resolveMotion } from './motionResolve.js';
|
|
1
2
|
import { TRANSFORM_RANGES, rawProp, resolveNodeStyleLayers, transformValue } from './styleResolve.js';
|
|
2
3
|
import { mergeTypeScale, resolveTheme } from './themeResolve.js';
|
|
3
4
|
/* ---- css-value.ts ---- */
|
|
@@ -440,6 +441,8 @@ const composeSurface = (surface, shell, theme, context) => {
|
|
|
440
441
|
context.warnings?.push(`surfaceStyle.${key}: mobilde karşılığı yok, atlandı`);
|
|
441
442
|
}
|
|
442
443
|
};
|
|
444
|
+
/** Tipografi grubunun `composeTypography`'nin okuduğu alanları (frontend derleyicisiyle parite: `test/style-resolve-native.test.ts`). */
|
|
445
|
+
export const TYPOGRAPHY_KEYS = new Set(['textColor', 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'textDecoration', 'textAlign', 'letterSpacing', 'lineHeight']);
|
|
443
446
|
const composeTypography = (typography, text, theme, context) => {
|
|
444
447
|
const color = asText(typography.textColor, theme, context.warnings, 'typographyStyle.textColor');
|
|
445
448
|
if (color)
|
|
@@ -510,20 +513,101 @@ export function composeNativeStyle(input, theme) {
|
|
|
510
513
|
* Bir düğümün mobil çizerdeki stili (`NativeRenderer` → `nativeNodeStyle`, `renderer.functions.ts`). Temanın katmanı
|
|
511
514
|
* (`componentStyles[tip]` ← `$preset`) web'in çözücüsünden gelir ve TABANDIR; panel grupları üstüne, düğümün serbest
|
|
512
515
|
* `style`'ı son söz; `$preset` işareti RN stiline inmez (plans/mobil-plantcare-acigi f-16 — 2026-09-28'e dek çizer bu
|
|
513
|
-
* katmanı okumuyordu, rapor da okumuyordu). İfadeye bağlı prop çalışma anında çözülür, burada yok
|
|
516
|
+
* katmanı okumuyordu, rapor da okumuyordu). İfadeye bağlı prop çalışma anında çözülür, burada yok: grubu bağlı düğümde
|
|
517
|
+
* grup (tasarım yedeği taşısa da) stile girmez; yüzey ve tipografi grubunun TEK alanı bağlıysa (`{ type: 'code', ref }`,
|
|
518
|
+
* frontend ADR 0020) o alan stile girmez ve `bound`'da `<grup>.<alan>` diye listelenir — yedeği çizerin verdiği değer
|
|
519
|
+
* değildir (yedeksiz sarmal `"[object Object]"` yazılıyordu, inceleme 2026-10-09). Çizerin TANIMADIĞI alan bağlı olsa da
|
|
520
|
+
* bağlı alan değildir (`bound`'a girmez; yüzeyde derleyicinin "karşılığı yok" uyarısı düşer). `ref`'i boş `code` sarmalı
|
|
521
|
+
* bağ değildir: verisi ham değer okunur; verisiz sarmal alanı yok sayar (çizer `mapSchemaPropToRawProp` ile aynı).
|
|
522
|
+
*
|
|
523
|
+
* `motion`, kabuğun `motionStyle`'dan uyguladığı harekettir (`NativeRenderer` → `nodeMotionOf`, `/dev` ve cihaz):
|
|
524
|
+
* yalnız hareketi olan düğümde yazılır. Kanvas ve "hareketi azalt" onu dondurur — rapor dondurulmamış çözümdür.
|
|
514
525
|
*/
|
|
515
526
|
export function nodeNativeStyle(node, theme, source) {
|
|
516
527
|
const props = node.props;
|
|
528
|
+
const bound = [];
|
|
517
529
|
const group = (name) => {
|
|
518
530
|
const value = rawProp(props, name);
|
|
519
531
|
return isPlainObject(value) ? value : null;
|
|
520
532
|
};
|
|
533
|
+
const evaluated = (name, known) => {
|
|
534
|
+
const value = isBound(props?.[name]) ? null : group(name);
|
|
535
|
+
if (!value)
|
|
536
|
+
return value;
|
|
537
|
+
const kept = { ...value };
|
|
538
|
+
for (const [field, wrapper] of fieldWrappers(props?.[name])) {
|
|
539
|
+
if (!isExpression(wrapper)) {
|
|
540
|
+
if (!('data' in wrapper))
|
|
541
|
+
delete kept[field];
|
|
542
|
+
}
|
|
543
|
+
else if (known.has(field)) {
|
|
544
|
+
delete kept[field];
|
|
545
|
+
bound.push(`${name}.${field}`);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
// Tanınmayan alan: sarmalı değer olarak kalır ki derleyicinin kendi "karşılığı yok" uyarısı düşsün.
|
|
549
|
+
kept[field] = wrapper;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return kept;
|
|
553
|
+
};
|
|
521
554
|
const layers = resolveNodeStyleLayers(rawProp(props, 'style'), source, node.type);
|
|
522
|
-
|
|
555
|
+
const composed = composeNativeStyle({
|
|
523
556
|
themeStyle: layers.theme,
|
|
524
|
-
surfaceStyle:
|
|
525
|
-
typographyStyle:
|
|
557
|
+
surfaceStyle: evaluated('surfaceStyle', SURFACE_KEYS),
|
|
558
|
+
typographyStyle: evaluated('typographyStyle', TYPOGRAPHY_KEYS),
|
|
526
559
|
hoverStyle: group('hoverStyle'),
|
|
527
560
|
style: layers.own
|
|
528
561
|
}, theme);
|
|
562
|
+
const style = bound.length > 0 ? { ...composed, bound } : composed;
|
|
563
|
+
const resolved = nodeMotion(props?.motionStyle);
|
|
564
|
+
if (!resolved)
|
|
565
|
+
return style;
|
|
566
|
+
const { warnings, ...motion } = resolved;
|
|
567
|
+
const reported = { ...style, warnings: [...style.warnings, ...warnings] };
|
|
568
|
+
// Hareket çıkmadıysa (yalnız atlanan alanlar) `motion` yazılmaz; uyarıları yine listededir.
|
|
569
|
+
return Object.keys(motion).length > 0 ? { ...reported, motion } : reported;
|
|
570
|
+
}
|
|
571
|
+
const RUNNING = { design: false, reduceMotion: false };
|
|
572
|
+
/**
|
|
573
|
+
* İfadeye bağlı GRUP zarfı (`code`, `translation`): değeri çalışma anında oluşur. Zarfın `data`'sı tasarım zamanı
|
|
574
|
+
* yedeğidir (kanvasın gösterdiği değer), çizerin uyguladığı değer değil — rapora girmez.
|
|
575
|
+
*/
|
|
576
|
+
const isBound = (value) => isPlainObject(value) && (value.type === 'code' || value.type === 'translation');
|
|
577
|
+
/**
|
|
578
|
+
* Statik grubun sarmallı alanları (`{ type, data?, ref? }`), ham hâlleriyle. Tasarım şeması statik sarmala
|
|
579
|
+
* `type: 'static'` yazar, yayınlanan sayfanın yükü yazmaz (backend `AppProp`, 2026-10-04'ten beri; statik alanda
|
|
580
|
+
* `type: null` gelir) — ikisi de statiktir (frontend `inner-bindings.ts` `fieldsOf` ile aynı kural). Yalnız yazılı
|
|
581
|
+
* `type`'ı tanıyan ilk sürüm gerçek yükte bağlı alanı hiç görmüyordu (ölçüldü 2026-10-09).
|
|
582
|
+
*/
|
|
583
|
+
const fieldWrappers = (prop) => Object.entries(isPlainObject(prop) && (prop.type ?? 'static') === 'static' && isPlainObject(prop.data) ? prop.data : {})
|
|
584
|
+
.filter((entry) => isPlainObject(entry[1]) && typeof entry[1].type === 'string');
|
|
585
|
+
/** İfadeye bağlı alan sarmalı: çizer onu çalışma anında değerlendirir (frontend ADR 0020, `inner-bindings.ts` `isCodeBinding`). */
|
|
586
|
+
const isExpression = (wrapper) => wrapper.type === 'code' && typeof wrapper.ref === 'string' && wrapper.ref !== '';
|
|
587
|
+
/**
|
|
588
|
+
* Düğümün ham `motionStyle` prop'undan (zarfıyla) çalışma anı çözümü. Bağlı grup atlanır. Grubun içindeki tek alan
|
|
589
|
+
* ifadeye bağlıysa (`code`) değeri çalışma anında oluşur: çözüme girmez, `bound`'da adıyla listelenir — tasarım yedeği
|
|
590
|
+
* çizerin uyguladığı değer değildir. Çözülmeyen yazım (şablon metni, `code` dışı bağ) uyarısıyla listenin başındadır;
|
|
591
|
+
* çizerin ham değer olarak okuduğu verisi varsa çözüme o girer (`motionFieldBinding`).
|
|
592
|
+
*/
|
|
593
|
+
function nodeMotion(prop) {
|
|
594
|
+
if (!isPlainObject(prop) || isBound(prop) || !isPlainObject(prop.data))
|
|
595
|
+
return null;
|
|
596
|
+
const unresolved = [];
|
|
597
|
+
const bound = [];
|
|
598
|
+
const written = Object.fromEntries(Object.entries(prop.data).flatMap(([field, value]) => {
|
|
599
|
+
const binding = motionFieldBinding(field, value);
|
|
600
|
+
if (!binding)
|
|
601
|
+
return [[field, value]];
|
|
602
|
+
if (binding.kind === 'expression') {
|
|
603
|
+
bound.push(field);
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
unresolved.push(binding.warning);
|
|
607
|
+
return binding.fallback === undefined ? [] : [[field, { type: 'static', data: binding.fallback }]];
|
|
608
|
+
}));
|
|
609
|
+
const resolved = resolveMotion(rawProp({ motionStyle: { type: 'static', data: written } }, 'motionStyle'), RUNNING);
|
|
610
|
+
if (!resolved && unresolved.length === 0 && bound.length === 0)
|
|
611
|
+
return null;
|
|
612
|
+
return { ...resolved, ...(bound.length > 0 ? { bound } : {}), warnings: [...unresolved, ...(resolved?.warnings ?? [])] };
|
|
529
613
|
}
|
|
@@ -20,6 +20,9 @@ export function actionScript(action, captureTarget = studioWebTarget) {
|
|
|
20
20
|
// Seçeneklerin arandığı kök: web'de kutunun kabı (`scope`), mobilde belge (liste `Modal` katmanında).
|
|
21
21
|
const optionRoot = captureTarget.optionsOutsideNode ? 'document' : 'scope';
|
|
22
22
|
if (action.action === 'click') {
|
|
23
|
+
// `<elementId>__press` basış yüzeyinin KENDİSİDİR (frontend K2 sözleşmesi): içinde düğme taşıyan yüzeyde
|
|
24
|
+
// (`flip-card` — web'de iç içe `<button>` olmasın diye `div` çizilir) ilk düğmeye inilmez.
|
|
25
|
+
const pressSurface = String(action.target ?? '').endsWith(`${INTERNAL_ROLE_SEPARATOR}press`);
|
|
23
26
|
return `(function () {
|
|
24
27
|
var count = performance.getEntriesByType('resource').length;
|
|
25
28
|
var node = document.querySelector(${selector});
|
|
@@ -29,7 +32,7 @@ export function actionScript(action, captureTarget = studioWebTarget) {
|
|
|
29
32
|
// çipin "kaldır" düğmesidir — eskiden ona basılıp seçim siliniyordu (37 S-18). Tek basışlı
|
|
30
33
|
// denetimin yüzeyi <id>__press'tir (ui-native press-role-guard), anahtarınki role="switch".
|
|
31
34
|
var press = document.querySelector(${JSON.stringify(`[data-element-id="${String(action.target ?? '')}${INTERNAL_ROLE_SEPARATOR}press"]`)});
|
|
32
|
-
var target = node.matches('button, a, [role="button"], [role="switch"], [role="combobox"]') ? node
|
|
35
|
+
var target = ${pressSurface} || node.matches('button, a, [role="button"], [role="switch"], [role="combobox"]') ? node
|
|
33
36
|
: (press || node.querySelector('button, a, [role="button"], [role="switch"]') || node);
|
|
34
37
|
// S-2: sekme grubunun KÖKÜNE click, DOM sırasındaki ilk sekmeye gider ve "uygulandı" der —
|
|
35
38
|
// ajan ikinci sekmeyi hiç seçemiyordu (2026-09-16 raporu madde 6). Sekme seçmek 'tab'in işi.
|