@appaflytech/zeyrek-ai-mcp 0.0.40 → 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 +95 -104
- package/dist/knowledge/componentEvents.js +34 -18
- package/dist/knowledge/mobileComponentCatalog.js +119 -20
- package/dist/knowledge/mobileComponentEvents.js +41 -0
- package/dist/knowledge/motionResolve.js +252 -0
- package/dist/knowledge/normalize.js +117 -71
- 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 +217 -24
- package/dist/knowledge/styleResolve.js +0 -1
- package/dist/knowledge/styleResolveNative.js +88 -4
- package/dist/platform/capture/actions.js +27 -1
- package/dist/platform/capture/capture.js +10 -13
- package/dist/platform/capture/persistent-browser.js +130 -0
- package/dist/platform/capture/summary.js +4 -1
- 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-app-shell-drawer.json +1 -0
- package/dist/resources/examples/mobile-list-detail-form.json +7 -3
- package/dist/resources/examples/mobile-scan-code.json +285 -0
- package/dist/resources/guides/component.md +3 -3
- package/dist/resources/guides/entity.md +16 -0
- package/dist/resources/guides/expressions.md +8 -7
- package/dist/resources/guides/merge.md +98 -18
- package/dist/resources/guides/microservice.md +3 -3
- package/dist/resources/guides/middleware.md +2 -0
- 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 +51 -70
- package/dist/resources/guides/page.md +229 -137
- package/dist/resources/guides/translation.md +67 -1
- package/dist/resources/guides/workflow.md +31 -1
- 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/entities.js +41 -4
- package/dist/tools/layouts.js +29 -9
- package/dist/tools/merges.js +29 -9
- package/dist/tools/mobile.js +15 -1
- package/dist/tools/pages.js +156 -168
- package/dist/tools/platformInfo.js +3 -2
- package/dist/tools/profileWrite.js +2 -2
- package/dist/tools/queries.js +1 -1
- package/dist/tools/rows.js +14 -3
- package/dist/tools/schemaOps.js +132 -34
- package/dist/tools/screenWrite.js +3 -3
- package/dist/tools/screens.js +42 -3
- package/dist/tools/translations.js +148 -6
- package/package.json +2 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { PlatformApiError } from '../../platform/errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Toplu yamanın zarfı (`{ op: 'batch', gestureId?, label?, ops: [...] }`) ARAÇ GİRİŞİNDE bir kez açılır: içindeki
|
|
4
|
+
* op'lar yerlerinde ve sırayla, tek tek yazılmış sayılır; gateway'in bütün geçişleri ve platforma giden gövde düz
|
|
5
|
+
* listeyi görür (sırayı platforma kadar `schemaOps.ts` `serverOpsInOrder` taşır). Platform için zarf yalnız
|
|
6
|
+
* gruplamadır — `PageSchemaMicroOpApplier.Apply` (backend) listeyi döngüsünün başında düzleştirir, `gestureId`/`label`
|
|
7
|
+
* hiçbir yerde okunmaz — bu yüzden düz liste aynı yazımdır.
|
|
8
|
+
*
|
|
9
|
+
* Girişte açılmadan önce her geçiş zarfa kendi bakıyordu ve çoğu bakmıyordu (TOOL-058, ölçüldü 2026-10-10, gerçek uç):
|
|
10
|
+
* zarf içindeki `replace_prop` sarılmadan gidip backend'in ham 422'sini aldı; kök kapsayıcıyı silen op, adsız düğüm
|
|
11
|
+
* ve gateway'in öykündüğü `resize_component` korumalardan geçmeden platforma ulaşıyordu.
|
|
12
|
+
*
|
|
13
|
+
* İç içe zarf reddedilir: platform onu `nested-batch` ile reddeder (`PageSchemaMicroOpApplier.ApplyOne`); gateway
|
|
14
|
+
* açarken kabul etseydi sözleşmeyi sessizce genişletirdi. `ops`'u liste olmayan zarf, op nesnesi olmayan öğe ve
|
|
15
|
+
* açılınca boş kalan yama da reddedilir — platform bunları sessizce yutar (`s.Ops ?? []`, `Where(w => w is not null)`)
|
|
16
|
+
* ve değişiklik olmadan sürüm ilerletir; gateway sessiz başarıyı geçirmez.
|
|
17
|
+
*/
|
|
18
|
+
export function openBatchEnvelopes(tool, ops) {
|
|
19
|
+
const envelopes = ops.filter(op => op.op === 'batch');
|
|
20
|
+
if (envelopes.length === 0)
|
|
21
|
+
return { ops: [...ops], envelopes: 0 };
|
|
22
|
+
const issues = ops.flatMap((op, index) => (op.op === 'batch' ? envelopeIssues(op, index) : []));
|
|
23
|
+
if (issues.length > 0)
|
|
24
|
+
throw new PlatformApiError(`${tool}: batch gövdesi geçersiz (istek gönderilmedi): ${issues.join(' · ')}`, 400);
|
|
25
|
+
const opened = ops.flatMap(op => (op.op === 'batch' ? op.ops : [op]));
|
|
26
|
+
if (opened.length === 0) {
|
|
27
|
+
throw new PlatformApiError(`${tool}: uygulanacak micro-op yok — batch zarfının 'ops' listesi boş (istek gönderilmedi).`, 400);
|
|
28
|
+
}
|
|
29
|
+
return { ops: opened, envelopes: envelopes.length };
|
|
30
|
+
}
|
|
31
|
+
/** `ops[<zarfın sırası>]` ile başlar: birden çok zarf varken hangisinin bozuk olduğu mesajdan okunur. */
|
|
32
|
+
function envelopeIssues(envelope, at) {
|
|
33
|
+
const where = `ops[${at}] (batch)`;
|
|
34
|
+
if (!Array.isArray(envelope.ops))
|
|
35
|
+
return [`${where} 'ops' taşımalı: { op: 'batch', ops: [ …op'lar… ] }`];
|
|
36
|
+
return envelope.ops.flatMap((entry, index) => {
|
|
37
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
38
|
+
return [`${where}.ops[${index}] bir op nesnesi değil`];
|
|
39
|
+
return entry.op === 'batch'
|
|
40
|
+
? [`${where}.ops[${index}] yine bir batch: iç içe batch yok (platform 'nested-batch' ile reddeder) — içteki op'ları dıştaki listeye taşıyın`]
|
|
41
|
+
: [];
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Zarf açıldıysa yanıta düşen not: ajan zarfın ne kazandırdığını (hiçbir şey) görür. */
|
|
45
|
+
export function batchEnvelopeNote(opened) {
|
|
46
|
+
return opened.envelopes === 0
|
|
47
|
+
? []
|
|
48
|
+
: [`batch zarfı açıldı (${opened.envelopes} zarf → ${opened.ops.length} op): op'lar tek tek yazılmış gibi işlendi ve platforma düz listeyle gitti — platform zarfı yalnız gruplama sayar (gestureId/label okunmaz).`];
|
|
49
|
+
}
|
|
50
|
+
/** Ret mesajına eklenen ipucu: `#<indeks>` ajanın GÖNDERDİĞİ listeye değil, zarf açıldıktan sonrakine göredir. */
|
|
51
|
+
export function batchIndexHint(opened) {
|
|
52
|
+
return opened.envelopes === 0
|
|
53
|
+
? ''
|
|
54
|
+
: ` İndeksler batch zarfı açıldıktan sonraki (${opened.envelopes} zarf → ${opened.ops.length} op) açılmış listeye göredir: zarfın içindeki op'lar, zarfın durduğu yerde sırayla sayılır.`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Zarfı açan SAF görünüm (iç içe dahil, hata vermez): op listesini okuyan bilgi kuralları için. Araç yolunda liste
|
|
58
|
+
* `openBatchEnvelopes`'tan geçtiği için zarf buraya ulaşmaz; kurallar yine de op sözlüğünün tamamını tanır — doğrudan
|
|
59
|
+
* çağrıldıklarında (testler, başka araçlar) zarfa kör kalmazlar (TOOL-057, ölçüldü 2026-10-10: doğrudan
|
|
60
|
+
* `MOB-001, MOB-028`, zarf içinde bulgu yok). Çağıranlar: `lint.ts` `profileFindings` ve `profileLintReadsSavedNodes`,
|
|
61
|
+
* `heroTag.ts`. Emsal: `normalize.ts` `orphanedFlowPlan`.
|
|
62
|
+
*/
|
|
63
|
+
export function flattenBatchOps(ops) {
|
|
64
|
+
return ops.flatMap(op => (op.op === 'batch' ? flattenBatchOps(batchBody(op.ops)) : [op]));
|
|
65
|
+
}
|
|
66
|
+
function batchBody(value) {
|
|
67
|
+
if (!Array.isArray(value))
|
|
68
|
+
return [];
|
|
69
|
+
return value.filter((entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry));
|
|
70
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paylaşılan öğe (Hero) etiketinin gateway aynası: değerin okunması, sayfanın etiketleri ve iki sayfanın eşlenmesi.
|
|
3
|
+
* Kaynak frontend'dedir (`zeyrek-frontend/packages/ng-apps-ui-native/src/motion/hero-tag.ts`, ADR
|
|
4
|
+
* `zeyrek-frontend/docs/adr/0022-hero-yolu.md`). Bu dosya onun kopyasıdır ve `test/hero-tag-parity.test.ts` ile
|
|
5
|
+
* kilitlidir: frontend'in altın vakaları (`hero-golden.json`) aynı girdiden aynı çıktıyı ve harfi harfine aynı uyarıyı
|
|
6
|
+
* vermeli. Test kırılınca test düzeltilmez, bu dosya senkronlanır.
|
|
7
|
+
*/
|
|
8
|
+
import { flattenBatchOps } from './batchOps.js';
|
|
9
|
+
function asRecord(value) {
|
|
10
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
11
|
+
}
|
|
12
|
+
/** Metin kırpılır, sayı metne çevrilir; boş metin ve başka her tür "etiket yok"tur. */
|
|
13
|
+
export function heroTagValue(raw) {
|
|
14
|
+
if (typeof raw === 'number')
|
|
15
|
+
return Number.isFinite(raw) ? String(raw) : null;
|
|
16
|
+
if (typeof raw !== 'string')
|
|
17
|
+
return null;
|
|
18
|
+
return raw.trim() || null;
|
|
19
|
+
}
|
|
20
|
+
/** Düğümün ham `heroTag` prop'u (sarmalıyla): ifadeye bağlıysa değersiz, statikse metniyle; etiket yoksa `null`. */
|
|
21
|
+
export function heroTagEntry(elementId, prop) {
|
|
22
|
+
const wrapped = asRecord(prop);
|
|
23
|
+
if (!wrapped)
|
|
24
|
+
return null;
|
|
25
|
+
if (wrapped.type === 'code' || wrapped.type === 'translation') {
|
|
26
|
+
return typeof wrapped.ref === 'string' && wrapped.ref.trim() ? { elementId, bound: true } : null;
|
|
27
|
+
}
|
|
28
|
+
const tag = heroTagValue(wrapped.data);
|
|
29
|
+
return tag === null ? null : { elementId, tag };
|
|
30
|
+
}
|
|
31
|
+
/** Sayfa şemasındaki etiketler, belge sırasıyla (önce düğüm, sonra çocukları). */
|
|
32
|
+
export function heroTags(schema) {
|
|
33
|
+
if (!Array.isArray(schema))
|
|
34
|
+
return [];
|
|
35
|
+
return schema.flatMap(entry => {
|
|
36
|
+
const node = asRecord(entry);
|
|
37
|
+
if (!node)
|
|
38
|
+
return [];
|
|
39
|
+
const own = typeof node.elementId === 'string' && node.elementId ? heroTagEntry(node.elementId, asRecord(node.props)?.heroTag) : null;
|
|
40
|
+
return [...(own ? [own] : []), ...heroTags(node.children)];
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function byTag(entries) {
|
|
44
|
+
const tags = new Map();
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (entry.tag !== undefined)
|
|
47
|
+
tags.set(entry.tag, [...(tags.get(entry.tag) ?? []), entry.elementId]);
|
|
48
|
+
}
|
|
49
|
+
return tags;
|
|
50
|
+
}
|
|
51
|
+
function countBound(entries) {
|
|
52
|
+
return entries.filter(entry => entry.bound).length;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Bir sayfada aynı statik etiketi birden çok düğüm taşıyorsa: sayfa bir yakınlaşmanın HEDEFİ olduğunda hizalama kutusu
|
|
56
|
+
* tektir ve ilk çizilen düğüm hizalanır. Kaynak sayfada yineleme sorun değildir (kaynak dokunulan düğümdür).
|
|
57
|
+
*/
|
|
58
|
+
export function heroTagProblems(entries, page) {
|
|
59
|
+
return [...byTag(entries)]
|
|
60
|
+
.filter(([, nodes]) => nodes.length > 1)
|
|
61
|
+
.map(([tag, nodes]) => `${page ? `${page}: ` : ''}heroTag '${tag}' ${nodes.length} düğümde (${nodes.join(', ')}) — ilk çizilen hizalanır`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* İki sayfanın etiketlerini eşler. Çift, statik etiketi iki sayfada da olan iki düğümdür. Yineleme yalnız HEDEF
|
|
65
|
+
* sayfada uyarıdır. Eşi olmayan statik etiket uyarıdır — karşı sayfada ifadeye bağlı etiket yoksa (varsa eş çalışma
|
|
66
|
+
* anında oluşabilir). Bugün yalnız parite testi çağırır: sayfalar arası eşleme yazımda yapılmaz (hedef sayfa henüz
|
|
67
|
+
* yazılmamış olabilir); ayna, frontend kuralıyla birlikte hazır dursun diye taşınır.
|
|
68
|
+
*/
|
|
69
|
+
export function heroPairs(source, target) {
|
|
70
|
+
const sourceTags = byTag(source);
|
|
71
|
+
const targetTags = byTag(target);
|
|
72
|
+
const dynamic = { source: countBound(source), target: countBound(target) };
|
|
73
|
+
const unpaired = (tags, others, othersBound, where) => othersBound > 0 ? [] : [...tags].filter(([tag]) => !others.has(tag)).map(([tag, nodes]) => `heroTag '${tag}' (${nodes[0]}): ${where} eşi yok`);
|
|
74
|
+
return {
|
|
75
|
+
pairs: [...sourceTags].flatMap(([tag, nodes]) => {
|
|
76
|
+
const match = targetTags.get(tag);
|
|
77
|
+
return match ? [{ tag, source: String(nodes[0]), target: String(match[0]) }] : [];
|
|
78
|
+
}),
|
|
79
|
+
dynamic,
|
|
80
|
+
warnings: [
|
|
81
|
+
...heroTagProblems(target, 'hedef sayfa'),
|
|
82
|
+
...unpaired(sourceTags, targetTags, dynamic.target, 'hedef sayfada'),
|
|
83
|
+
...unpaired(targetTags, sourceTags, dynamic.source, 'kaynak sayfada')
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function asList(value) {
|
|
88
|
+
return Array.isArray(value) ? value : value === undefined || value === null ? [] : [value];
|
|
89
|
+
}
|
|
90
|
+
function walkNodes(value, visit) {
|
|
91
|
+
for (const entry of asList(value)) {
|
|
92
|
+
const node = asRecord(entry);
|
|
93
|
+
if (!node)
|
|
94
|
+
continue;
|
|
95
|
+
if (typeof node.elementId === 'string' && node.elementId)
|
|
96
|
+
visit(node, node.elementId);
|
|
97
|
+
walkNodes(node.children, visit);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const HERO_PROP_OPS = new Set(['replace_prop', 'set_binding', 'set_prop_type']);
|
|
101
|
+
/**
|
|
102
|
+
* Yama bir `heroTag` yazıyor mu: prop'u yazan op (`replace_prop`, `set_binding`, `set_prop_type`) ya da etiketli düğüm
|
|
103
|
+
* ekleyen `add_component` — toplu yamanın (`batch`) içinde de. Yinelenen etiket denetimi (MOB-030) sayfanın öteki
|
|
104
|
+
* düğümlerine bakar; yama yolu kayıtlı ağacı yalnız bu durumda okur.
|
|
105
|
+
*/
|
|
106
|
+
export function writesHeroTag(ops) {
|
|
107
|
+
return flattenBatchOps(ops).some(op => (HERO_PROP_OPS.has(String(op.op)) && op.prop === 'heroTag') ||
|
|
108
|
+
(op.op === 'add_component' && heroTags(asList(op.component)).length > 0));
|
|
109
|
+
}
|
|
110
|
+
/** Op'un `heroTag`'e yazdığı prop, saklanacağı sarmalla. `set_prop_type` kayıtlı prop'un tipini yerinde değiştirir. */
|
|
111
|
+
function writtenHeroProp(op, saved) {
|
|
112
|
+
if (op.op === 'set_binding')
|
|
113
|
+
return { type: 'code', ref: typeof op.expression === 'string' ? op.expression : asRecord(op.value)?.ref ?? op.value };
|
|
114
|
+
if (op.op === 'set_prop_type') {
|
|
115
|
+
const data = op.data ?? op.value;
|
|
116
|
+
return { ...asRecord(saved), type: op.propType, ...(op.ref === undefined ? {} : { ref: op.ref }), ...(data === undefined ? {} : { data }) };
|
|
117
|
+
}
|
|
118
|
+
const wrapped = asRecord(op.value);
|
|
119
|
+
return wrapped && typeof wrapped.type === 'string' ? wrapped : { type: 'static', data: op.value };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Sayfanın yazımdan SONRAKİ etiketleri: kayıtlı ağaç + bu yazımın ekledikleri ve yazdıkları; kaldırılan düğümler alt
|
|
123
|
+
* ağaçlarıyla birlikte düşer. `written` bu yazımın dokunduğu düğümlerdir — MOB-030 yalnız onları içeren yinelemeyi
|
|
124
|
+
* söyler. Sıra "kayıtlılar, sonra yazılanlar"dır, belge sırası değil: yama sonrasının sırası burada bilinmez.
|
|
125
|
+
*/
|
|
126
|
+
export function heroTagsAfterWrite(input) {
|
|
127
|
+
const written = new Map();
|
|
128
|
+
const removed = new Set();
|
|
129
|
+
const savedProps = new Map();
|
|
130
|
+
const savedNodes = new Map();
|
|
131
|
+
walkNodes(input.savedNodes, (node, elementId) => {
|
|
132
|
+
savedNodes.set(elementId, node);
|
|
133
|
+
savedProps.set(elementId, asRecord(node.props)?.heroTag);
|
|
134
|
+
});
|
|
135
|
+
const write = (node, elementId) => written.set(elementId, heroTagEntry(elementId, asRecord(node.props)?.heroTag));
|
|
136
|
+
walkNodes(input.nodes, write);
|
|
137
|
+
for (const op of flattenBatchOps(input.ops ?? [])) {
|
|
138
|
+
const target = typeof op.target === 'string' ? op.target : '';
|
|
139
|
+
if (op.op === 'add_component')
|
|
140
|
+
walkNodes(op.component, write);
|
|
141
|
+
if (op.op === 'remove_component')
|
|
142
|
+
walkNodes(savedNodes.get(target), (_node, elementId) => removed.add(elementId));
|
|
143
|
+
if (HERO_PROP_OPS.has(String(op.op)) && op.prop === 'heroTag' && target)
|
|
144
|
+
written.set(target, heroTagEntry(target, writtenHeroProp(op, savedProps.get(target))));
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
entries: [
|
|
148
|
+
...heroTags(asList(input.savedNodes)).filter(entry => !written.has(entry.elementId) && !removed.has(entry.elementId)),
|
|
149
|
+
...[...written.values()].filter((entry) => entry !== null)
|
|
150
|
+
],
|
|
151
|
+
written: new Set(written.keys())
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -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']),
|
|
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']),
|
|
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
|
},
|