@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.
Files changed (60) hide show
  1. package/dist/knowledge/componentCatalog.js +95 -104
  2. package/dist/knowledge/componentEvents.js +34 -18
  3. package/dist/knowledge/mobileComponentCatalog.js +119 -20
  4. package/dist/knowledge/mobileComponentEvents.js +41 -0
  5. package/dist/knowledge/motionResolve.js +252 -0
  6. package/dist/knowledge/normalize.js +117 -71
  7. package/dist/knowledge/pageDiscover.js +2 -2
  8. package/dist/knowledge/platform/batchOps.js +70 -0
  9. package/dist/knowledge/platform/heroTag.js +153 -0
  10. package/dist/knowledge/platform/lint.js +128 -6
  11. package/dist/knowledge/platform/mobile.js +1 -1
  12. package/dist/knowledge/platform/mobileLayoutLint.js +8 -1
  13. package/dist/knowledge/platform/mobileRules.js +18 -0
  14. package/dist/knowledge/platform/pageTransition.js +57 -0
  15. package/dist/knowledge/platform/web.js +1 -1
  16. package/dist/knowledge/rules.js +217 -24
  17. package/dist/knowledge/styleResolve.js +0 -1
  18. package/dist/knowledge/styleResolveNative.js +88 -4
  19. package/dist/platform/capture/actions.js +27 -1
  20. package/dist/platform/capture/capture.js +10 -13
  21. package/dist/platform/capture/persistent-browser.js +130 -0
  22. package/dist/platform/capture/summary.js +4 -1
  23. package/dist/resources/examples/flow-list-to-form.json +8 -10
  24. package/dist/resources/examples/flow-load-table.json +1 -1
  25. package/dist/resources/examples/mobile-app-shell-drawer.json +1 -0
  26. package/dist/resources/examples/mobile-list-detail-form.json +7 -3
  27. package/dist/resources/examples/mobile-scan-code.json +285 -0
  28. package/dist/resources/guides/component.md +3 -3
  29. package/dist/resources/guides/entity.md +16 -0
  30. package/dist/resources/guides/expressions.md +8 -7
  31. package/dist/resources/guides/merge.md +98 -18
  32. package/dist/resources/guides/microservice.md +3 -3
  33. package/dist/resources/guides/middleware.md +2 -0
  34. package/dist/resources/guides/mobile-layout.md +2 -1
  35. package/dist/resources/guides/mobile-publish.md +1 -0
  36. package/dist/resources/guides/mobile-steps.md +67 -0
  37. package/dist/resources/guides/mobile.md +51 -70
  38. package/dist/resources/guides/page.md +229 -137
  39. package/dist/resources/guides/translation.md +67 -1
  40. package/dist/resources/guides/workflow.md +31 -1
  41. package/dist/resources/schema-fingerprint.json +2 -2
  42. package/dist/resources/schemas/ResolveMergeConflictsCommand.json +30 -1
  43. package/dist/tools/activities.js +3 -1
  44. package/dist/tools/applications.js +16 -2
  45. package/dist/tools/components.js +12 -6
  46. package/dist/tools/designSchemaPatch.js +18 -13
  47. package/dist/tools/entities.js +41 -4
  48. package/dist/tools/layouts.js +29 -9
  49. package/dist/tools/merges.js +29 -9
  50. package/dist/tools/mobile.js +15 -1
  51. package/dist/tools/pages.js +156 -168
  52. package/dist/tools/platformInfo.js +3 -2
  53. package/dist/tools/profileWrite.js +2 -2
  54. package/dist/tools/queries.js +1 -1
  55. package/dist/tools/rows.js +14 -3
  56. package/dist/tools/schemaOps.js +132 -34
  57. package/dist/tools/screenWrite.js +3 -3
  58. package/dist/tools/screens.js +42 -3
  59. package/dist/tools/translations.js +148 -6
  60. package/package.json +2 -2
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Mobil `motionStyle` grubunun gateway aynası: alan tablosu ve çözücü. Kaynak frontend'dedir
3
+ * (`zeyrek-frontend/packages/ng-apps-ui-native/src/motion/` — `motion-fields.ts`, `motion-presets.ts`,
4
+ * `resolve-motion.ts`); bu dosya onun kopyasıdır ve iki kilitle bağlıdır (`test/motion-resolve.test.ts`):
5
+ * frontend'in altın vakaları (`motion-golden.json`) aynı çıktıyı vermeli, mobil katalogdaki grubun alanları ve
6
+ * seçenekleri bu tabloyla aynı olmalı. Test kırılınca test düzeltilmez, bu dosya senkronlanır.
7
+ *
8
+ * Çözüm reanimated'in girdisinin serileştirilebilir tanımıdır: giriş/çıkış/yerleşim oluşturucu adıyla,
9
+ * döngü ve değer geçişi CSS API alanlarıyla (frontend ADR 0006).
10
+ */
11
+ export const MOTION_ENTER = ['none', 'fade', 'fadeUp', 'fadeDown', 'slideLeft', 'slideRight', 'zoomIn'];
12
+ export const MOTION_EXIT = ['none', 'fade', 'fadeUp', 'fadeDown', 'slideLeft', 'slideRight', 'zoomOut'];
13
+ export const MOTION_EASING = ['ease', 'easeIn', 'easeOut', 'easeInOut', 'linear'];
14
+ export const MOTION_LOOP = ['none', 'pulse', 'spin', 'bounce'];
15
+ export const MOTION_TRANSITION = ['none', 'colors', 'opacity', 'transform', 'all'];
16
+ export const MOTION_PRESS = ['none', 'scale', 'opacity'];
17
+ /**
18
+ * Grubu TAŞIMAYAN mobil tipler: görünen içeriğini düğüm kabuğunun dışında çizerler (katman, bildirim, ekran katmanına
19
+ * taşınan), kabukları çalışma anında boş kutudur — frontend `catalog-schema.ts` `DRAWN_OUTSIDE_SHELL`. Katalogla
20
+ * kilitli (`test/motion-resolve.test.ts`).
21
+ */
22
+ export const MOTION_OUTSIDE_SHELL = ['drawer', 'fab', 'floating-container', 'modal', 'photo-viewer', 'toast'];
23
+ /** ms cinsinden kabul aralıkları. */
24
+ export const MOTION_RANGES = {
25
+ duration: [100, 2000],
26
+ delay: [0, 5000]
27
+ };
28
+ /** Alan düğümde yokken çizerin okuduğu değerler; şema bunları düğüme YAZMAZ (grubun kayıtlı varsayılanı yoktur). */
29
+ export const MOTION_DEFAULTS = {
30
+ enter: 'none',
31
+ exit: 'none',
32
+ duration: 300,
33
+ delay: 0,
34
+ easing: 'easeOut',
35
+ layout: false,
36
+ loop: 'none',
37
+ transition: 'none',
38
+ press: 'none'
39
+ };
40
+ export const MOTION_FIELDS = Object.keys(MOTION_DEFAULTS);
41
+ /** Seçenekli alanların değer kümeleri; katalog paritesi ve sözlük notu buradan okur. */
42
+ export const MOTION_CHOICES = {
43
+ enter: MOTION_ENTER,
44
+ exit: MOTION_EXIT,
45
+ easing: MOTION_EASING,
46
+ loop: MOTION_LOOP,
47
+ transition: MOTION_TRANSITION,
48
+ press: MOTION_PRESS
49
+ };
50
+ const DECIMAL_TEXT = /^\d+(\.\d+)?$/;
51
+ const isEmpty = (raw) => raw === undefined || raw === null || (typeof raw === 'string' && raw.trim() === '');
52
+ /**
53
+ * Alanın değeri. Boş değer ne değer ne uyarıdır (alan yazılmamış). Okunamayan değer `value` taşımaz; uyarı
54
+ * frontend'inkiyle harfi harfine aynıdır (altın vaka onu karşılaştırır).
55
+ */
56
+ export const motionValue = (field, raw) => {
57
+ if (isEmpty(raw))
58
+ return {};
59
+ const reject = (expected) => ({
60
+ warning: `motionStyle.${field}: ${typeof raw === 'string' ? `'${raw}'` : JSON.stringify(raw)} geçersiz — ${expected}, atlandı`
61
+ });
62
+ const name = field;
63
+ if (name === 'duration' || name === 'delay') {
64
+ const [min, max] = MOTION_RANGES[name];
65
+ // Metin yalnız ondalık yazımla sayıdır: `Number()` üstel ('1e3') ve onaltılık ('0x1F4') metni de sayıya çevirir.
66
+ const number = typeof raw === 'number' ? raw : typeof raw === 'string' && DECIMAL_TEXT.test(raw.trim()) ? Number(raw) : NaN;
67
+ return Number.isFinite(number) && number >= min && number <= max
68
+ ? { value: number }
69
+ : reject(`${min}…${max} arası sayı (ms)`);
70
+ }
71
+ if (name === 'layout') {
72
+ if (raw === true || raw === 'true')
73
+ return { value: true };
74
+ if (raw === false || raw === 'false')
75
+ return { value: false };
76
+ return reject('true ya da false');
77
+ }
78
+ const choices = MOTION_CHOICES[name] ?? [];
79
+ return typeof raw === 'string' && choices.includes(raw)
80
+ ? { value: raw }
81
+ : reject(choices.join(', '));
82
+ };
83
+ const ENTER_PRESETS = {
84
+ fade: 'FadeIn',
85
+ fadeUp: 'FadeInUp',
86
+ fadeDown: 'FadeInDown',
87
+ slideLeft: 'SlideInLeft',
88
+ slideRight: 'SlideInRight',
89
+ zoomIn: 'ZoomIn'
90
+ };
91
+ const EXIT_PRESETS = {
92
+ fade: 'FadeOut',
93
+ fadeUp: 'FadeOutUp',
94
+ fadeDown: 'FadeOutDown',
95
+ slideLeft: 'SlideOutLeft',
96
+ slideRight: 'SlideOutRight',
97
+ zoomOut: 'ZoomOut'
98
+ };
99
+ const LAYOUT_PRESET = 'LinearTransition';
100
+ const EASING_CSS = {
101
+ ease: 'ease',
102
+ easeIn: 'ease-in',
103
+ easeOut: 'ease-out',
104
+ easeInOut: 'ease-in-out',
105
+ linear: 'linear'
106
+ };
107
+ /** Döngülerin kendi süresi ve eğrisi vardır; düğümde `duration`/`easing` YAZILMAMIŞSA bunlar geçer, 300 ms değil. */
108
+ const LOOP_PRESETS = {
109
+ spin: {
110
+ keyframes: { from: { transform: [{ rotate: '0deg' }] }, to: { transform: [{ rotate: '360deg' }] } },
111
+ duration: 1000,
112
+ easing: 'linear'
113
+ },
114
+ pulse: {
115
+ keyframes: { '0%': { opacity: 1 }, '50%': { opacity: 0.5 }, '100%': { opacity: 1 } },
116
+ duration: 2000,
117
+ easing: 'cubic-bezier(0.4, 0, 0.6, 1)'
118
+ },
119
+ bounce: {
120
+ keyframes: {
121
+ '0%': { transform: [{ translateY: '-25%' }], animationTimingFunction: 'cubic-bezier(0.8, 0, 1, 1)' },
122
+ '50%': { transform: [{ translateY: '0%' }], animationTimingFunction: 'cubic-bezier(0, 0, 0.2, 1)' },
123
+ '100%': { transform: [{ translateY: '-25%' }], animationTimingFunction: 'cubic-bezier(0.8, 0, 1, 1)' }
124
+ },
125
+ duration: 1000,
126
+ easing: 'linear'
127
+ }
128
+ };
129
+ const TRANSITION_PROPERTIES = {
130
+ colors: ['backgroundColor', 'borderColor', 'color'],
131
+ opacity: ['opacity'],
132
+ transform: ['transform'],
133
+ all: 'all'
134
+ };
135
+ const PRESS_PRESETS = {
136
+ scale: { property: 'scale', to: 0.95 },
137
+ opacity: { property: 'opacity', to: 0.7 }
138
+ };
139
+ /** Dokunma tepkisinin süresi kendinindir; düğümün süre/gecikme/eğrisi ona geçmez. */
140
+ const PRESS_TIMING = { duration: 150, easing: 'easeOut' };
141
+ const isActive = (settings) => settings.enter !== 'none' ||
142
+ settings.exit !== 'none' ||
143
+ settings.layout ||
144
+ settings.loop !== 'none' ||
145
+ settings.transition !== 'none' ||
146
+ settings.press !== 'none';
147
+ /**
148
+ * Frontend `resolveMotion` ile aynı kurallar: dondurma önce gelir (tasarım kipi ya da "hareketi azalt" → `null`),
149
+ * hiçbir hareket alanı dolu değilse `null` (yalnız süre yazılmış ayar hareket değildir), okunamayan alan ve
150
+ * tanınmayan anahtar atlanıp `warnings`'e yazılır.
151
+ */
152
+ export const resolveMotion = (input, context) => {
153
+ if (context.design || context.reduceMotion)
154
+ return null;
155
+ if (!input || typeof input !== 'object' || Array.isArray(input))
156
+ return null;
157
+ const warnings = [];
158
+ const settings = { ...MOTION_DEFAULTS };
159
+ const given = new Set();
160
+ for (const [key, raw] of Object.entries(input)) {
161
+ if (!MOTION_FIELDS.includes(key)) {
162
+ warnings.push(`motionStyle.${key}: tanınmayan alan, atlandı`);
163
+ continue;
164
+ }
165
+ const field = key;
166
+ const read = motionValue(field, raw);
167
+ if (read.warning)
168
+ warnings.push(read.warning);
169
+ if (read.value === undefined)
170
+ continue;
171
+ Object.assign(settings, { [field]: read.value });
172
+ given.add(field);
173
+ }
174
+ // Hareket çıkmadıysa sarmalayıcı takılmaz; atlanan alanın uyarısı yine de döner (yalnız `warnings` taşıyan sonuç).
175
+ if (!isActive(settings))
176
+ return warnings.length > 0 ? { warnings } : null;
177
+ const result = { warnings };
178
+ const timing = { duration: settings.duration, delay: settings.delay, easing: settings.easing };
179
+ if (settings.enter !== 'none')
180
+ result.entering = { preset: ENTER_PRESETS[settings.enter], ...timing };
181
+ if (settings.exit !== 'none')
182
+ result.exiting = { preset: EXIT_PRESETS[settings.exit], ...timing };
183
+ if (settings.layout)
184
+ result.layout = { preset: LAYOUT_PRESET, duration: settings.duration, easing: settings.easing };
185
+ if (settings.press !== 'none')
186
+ result.press = { ...PRESS_PRESETS[settings.press], ...PRESS_TIMING };
187
+ const style = {};
188
+ if (settings.loop !== 'none') {
189
+ const loop = LOOP_PRESETS[settings.loop];
190
+ Object.assign(style, {
191
+ animationName: loop.keyframes,
192
+ animationDuration: `${given.has('duration') ? settings.duration : loop.duration}ms`,
193
+ animationDelay: `${settings.delay}ms`,
194
+ animationTimingFunction: given.has('easing') ? EASING_CSS[settings.easing] : loop.easing,
195
+ animationIterationCount: 'infinite'
196
+ });
197
+ }
198
+ if (settings.transition !== 'none') {
199
+ Object.assign(style, {
200
+ transitionProperty: TRANSITION_PROPERTIES[settings.transition],
201
+ transitionDuration: `${settings.duration}ms`,
202
+ transitionDelay: `${settings.delay}ms`,
203
+ transitionTimingFunction: EASING_CSS[settings.easing]
204
+ });
205
+ }
206
+ if (Object.keys(style).length > 0)
207
+ result.style = style;
208
+ return result;
209
+ };
210
+ /*
211
+ * Buradan aşağısı AYNA DEĞİLDİR: gateway'in saklanan biçimi (değer sarmalları) okumasıdır.
212
+ */
213
+ /**
214
+ * Statik olmayan değer sarmalları — `normalize.ts` `PROP_VALUE_TYPES`'ın `static` dışındakileri. O modül bu dosyayı
215
+ * (katalog notları üzerinden) içe aktarır; döngü olmasın diye liste burada yinelenir, `motion-resolve.test.ts` kilitler.
216
+ */
217
+ export const MOTION_BINDING_TYPES = ['file', 'flow', 'widget', 'element', 'page', 'code', 'translation', 'connectData'];
218
+ const TEMPLATE_TEXT = /\{\{[\s\S]*\}\}/;
219
+ /**
220
+ * Grubun İÇİNDEKİ tek alanın ifadeye bağı; bağ yoksa `null`. `code` sarmalı çalışma anında DEĞERLENDİRİLİR (frontend
221
+ * ADR 0020; ölçüldü 2026-10-09, web `/dev` ve mobil oyun alanı — ondan önce hiçbir şey uygulamıyordu): uyarı değildir,
222
+ * değeri burada bilinemez. Alan düzeyinde çözülmeyen iki yazım kalır: sarmalsız düz metindeki `{{ … }}` (üst düzey
223
+ * prop'ta da ifade değildir) ve `code` dışındaki bağ türleri. Çözücü TANIMADIĞI alanı bağlı olsa da atlar: orada bağın
224
+ * türü değil alanın adı söylenir (yazım hatası `looop` ifadeye bağlıyken sessiz geçiyordu — inceleme 2026-10-09). `ref`'i
225
+ * boş `code` sarmalı ifade değildir: çizer sarmalın verisini ham değer okur, bağ toplamaz. Rapor
226
+ * (`nativeStyle.motion`) ve yazım denetimi (MOB-028) bu ayrımı buradan okur.
227
+ */
228
+ export const motionFieldBinding = (field, raw) => {
229
+ const wrapper = typeof raw === 'object' && raw !== null && !Array.isArray(raw) ? raw : undefined;
230
+ const bound = wrapper !== undefined && typeof wrapper.type === 'string' && MOTION_BINDING_TYPES.includes(wrapper.type);
231
+ // Statik sarmal: tasarım şemasında `type: 'static'`, yayınlanan sayfanın yükünde `type` yok ya da `null`.
232
+ const fixed = wrapper !== undefined && (wrapper.type === 'static' || ((wrapper.type ?? null) === null && 'data' in wrapper));
233
+ const inner = fixed ? wrapper.data : raw;
234
+ if (!bound && !(typeof inner === 'string' && TEMPLATE_TEXT.test(inner)))
235
+ return null;
236
+ if (!MOTION_FIELDS.includes(field))
237
+ return { kind: 'unresolved', warning: `motionStyle.${field}: tanınmayan alan, atlandı` };
238
+ if (!bound) {
239
+ return { kind: 'unresolved', warning: `motionStyle.${field}: '{{ … }}' düz metindir, ifade olarak çalışmaz — alan yok sayılır; ifade için alanı { type: 'code', ref } sarmalıyla yazın` };
240
+ }
241
+ const candidate = wrapper?.data;
242
+ const fallback = isEmpty(candidate) || (typeof candidate === 'string' && TEMPLATE_TEXT.test(candidate)) ? undefined : candidate;
243
+ const carried = fallback === undefined ? {} : { fallback };
244
+ const outcome = fallback === undefined ? 'alan yok sayılır' : `sarmalın verisi (${typeof fallback === 'string' ? `'${fallback}'` : JSON.stringify(fallback)}) ham değer olarak okunur`;
245
+ if (wrapper?.type === 'code') {
246
+ if (typeof wrapper.ref === 'string' && wrapper.ref !== '')
247
+ return { kind: 'expression', ...carried };
248
+ // Çizer ifadesi boş sarmalı bağ saymaz (`inner-bindings.ts` `isCodeBinding`; panelde fx açık, metin silinmiş).
249
+ return { kind: 'unresolved', ...carried, warning: `motionStyle.${field}: 'code' sarmalının ifadesi (ref) boş, bağ değildir — ${outcome}` };
250
+ }
251
+ return { kind: 'unresolved', ...carried, warning: `motionStyle.${field}: alan düzeyinde yalnız ifade (code) çalışır — '${String(wrapper?.type)}' bağı çözülmez, ${outcome}` };
252
+ };
@@ -3,8 +3,9 @@ import { PlatformApiError } from '../platform/errors.js';
3
3
  import { ensureWorkflowErrorPath } from './errorPath.js';
4
4
  import { DOCUMENT_STYLE_TYPES, TYPED_STYLE_COMPONENTS } from './styleResolve.js';
5
5
  import { STATE_STORAGE_KINDS, STEP_ALIASES } from './platform/profile.js';
6
- import { inlineStyleFields } from './componentCatalog.js';
6
+ import { MOBILE_OPTIONAL_VIEW_INDEX, MOBILE_ROW_LIST_TYPES } from './mobileComponentCatalog.js';
7
7
  import { FUNCTIONS_NAMESPACE } from './functionGuards.js';
8
+ import { retiredComponent } from './componentCatalog.js';
8
9
  const SORT_DEFAULT = { name: 'sortField', type: 'sortType', source: 'query-string' };
9
10
  const LIMIT_DEFAULT = { name: 'limit', offset: 'offset', source: 'query-string' };
10
11
  function asArray(value) {
@@ -634,16 +635,12 @@ export function codePropTemplateErrors(ops) {
634
635
  *
635
636
  * Seçici anahtarlara (`&__body`, `:hover`, `--token`) DOKUNULMAZ; yalnız `a-b` biçimli düz CSS
636
637
  * özellik adları camelCase'e çevrilir ve çevrim `gatewayNormalized` ile bildirilir.
637
- *
638
- * TB-129 — `inlineFields` verilirse o ÜST DÜZEY alanların altına hiç girilmez: `table.style.th`
639
- * gibi dallar satır içi `style` attribute'una basılır ve kebab-case zorunludur
640
- * (`INLINE_STYLE_FIELDS_BY_TYPE`). Çeviri orada stili öldürüyordu.
641
638
  */
642
639
  const styleKeyPattern = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
643
640
  export function isStylePropName(name) {
644
641
  return name === 'style' || /Style$/.test(name);
645
642
  }
646
- export function normalizeStyleKeys(value, label, fixed, inlineFields) {
643
+ export function normalizeStyleKeys(value, label, fixed) {
647
644
  if (Array.isArray(value)) {
648
645
  for (const entry of value)
649
646
  normalizeStyleKeys(entry, label, fixed);
@@ -652,10 +649,7 @@ export function normalizeStyleKeys(value, label, fixed, inlineFields) {
652
649
  if (!value || typeof value !== 'object')
653
650
  return;
654
651
  const record = value;
655
- const skipped = new Set(inlineFields ?? []);
656
652
  for (const key of Object.keys(record)) {
657
- if (skipped.has(key))
658
- continue;
659
653
  normalizeStyleKeys(record[key], label, fixed);
660
654
  if (!styleKeyPattern.test(key))
661
655
  continue;
@@ -798,10 +792,8 @@ export function normalizeComponentDefaults(nodes) {
798
792
  if (node.props && typeof node.props === 'object' && !Array.isArray(node.props)) {
799
793
  const props = node.props;
800
794
  const styleKeys = [];
801
- // Satır içi stil dalları komponent TİPİNE göre atlanır; tip bilinmiyorsa eski davranış.
802
- const inlineFields = (name) => name === 'style' ? inlineStyleFields(typeof node.type === 'string' ? node.type : undefined) : undefined;
803
795
  for (const name of Object.keys(props).filter(isStylePropName)) {
804
- normalizeStyleKeys(props[name], `${String(node.elementId ?? node.name ?? '?')}.${name}`, styleKeys, inlineFields(name));
796
+ normalizeStyleKeys(props[name], `${String(node.elementId ?? node.name ?? '?')}.${name}`, styleKeys);
805
797
  }
806
798
  if (styleKeys.length > 0)
807
799
  missing.push(`stil anahtarları camelCase'e çevrildi: ${styleKeys.join(' · ')}`);
@@ -878,6 +870,40 @@ export function assertAddComponentIds(ops) {
878
870
  if (errors.length > 0)
879
871
  throw new PlatformApiError(`PAGE-037 — ${errors.join(' · ')} (istek gönderilmedi). ${COMPONENT_BODY_RULE}`, 400);
880
872
  }
873
+ /**
874
+ * PAGE-051 — frontend kataloğundan kaldırılmış tipteki düğümleri (çocuklar dahil) toplar. Kayıt
875
+ * defteri eşitlemesi kaldırılan bileşeni silmediği için platform o tipi kabul eder; runtime düğümü
876
+ * yerine geçen bileşenle çizer ve eski olaylar hiç yayılmaz (`RETIRED_COMPONENTS`). Ölçüldü
877
+ * 2026-10-13, lokal `test`: `add_component { type: 'table' }` + `set_event doubleClick` kuru koşuda
878
+ * `valid: true`. Yalnız EKLENEN gövdeye bakar: o tiple kayıtlı düğüme yazan op'lar durdurulmaz.
879
+ */
880
+ export function retiredTypeNodes(nodes) {
881
+ const retired = [];
882
+ for (const node of asObjects(nodes)) {
883
+ const type = typeof node.type === 'string' ? node.type : undefined;
884
+ const record = retiredComponent(type);
885
+ if (record)
886
+ retired.push(`'${String(node.elementId ?? node.name ?? '?')}': '${type}' tipi katalogdan kaldırıldı — '${record.replacedBy}' kullanın`);
887
+ retired.push(...retiredTypeNodes(node.children));
888
+ }
889
+ return retired;
890
+ }
891
+ export const RETIRED_TYPE_RULE = "Kaldırılan tip kayıt defterinde durduğu için platform onu kabul eder, ama runtime düğümü yerine geçen bileşenle çizer ve eski olayları yaymaz: bağ kaydedilir, sessizce ölü kalır. Yerine geçenin sözlüğü component_get ile okunur; tablo için platform_guide('page') Kural 1e. O tiple KAYITLI düğüme yazan op'lar (replace_prop, move_component…) reddedilmez.";
892
+ export function assertNoRetiredTypes(ops) {
893
+ const retired = asObjects(ops).flatMap(op => op.op === 'add_component' ? retiredTypeNodes([op.component]) : []);
894
+ if (retired.length > 0)
895
+ throw new PlatformApiError(`PAGE-051 — katalogdan kaldırılan tipte add_component (istek gönderilmedi): ${retired.join(' · ')}. ${RETIRED_TYPE_RULE}`, 400);
896
+ }
897
+ /**
898
+ * PAGE-051'in OLUŞTURMA ayağı (`page_create`, `layout_create`): orada ağacın tamamı yenidir.
899
+ * Güncelleme yolları (`page_update`, `layout_update`) bunu ÇAĞIRMAZ — tam şema yazımında yeni
900
+ * düğüm kayıtlıdan ayrılamaz, eski sayfa kaydedilemez olurdu.
901
+ */
902
+ export function assertNoRetiredNodes(nodes) {
903
+ const retired = retiredTypeNodes(nodes);
904
+ if (retired.length > 0)
905
+ throw new PlatformApiError(`PAGE-051 — katalogdan kaldırılan tipte düğüm (istek gönderilmedi): ${retired.join(' · ')}. ${RETIRED_TYPE_RULE}`, 400);
906
+ }
881
907
  /**
882
908
  * add_component micro-op'unun taşıdığı komponent ağacı da aynı ToDomain yolundan geçer.
883
909
  */
@@ -895,7 +921,8 @@ export function assertAddComponentIds(ops) {
895
921
  * `replace_prop` `expression` alanını HİÇ OKUMAZ — yalnız set_binding okur; eskiden sarmalı
896
922
  * `expression`'a taşıyorduk ve prop 200 dönerken `{type:'static', data:null}` kaydediliyordu.
897
923
  * İfade `ref`'tedir (PG-117); ref yoksa string `data`'dan alınır, ikisi de yoksa çağrı durur.
898
- * Platform code prop'unda yalnız `Ref`'i saklar (`Prop` kurucusu), `data` kalıcı değildir; ama
924
+ * Platform code prop'unda ifadeyi `Ref`'te saklar; `data` tasarım yedeğidir — yalnız ilkel değerken ve
925
+ * ifadeyi yinelemiyorsa kalır (2026-10-09, `DesignComponentProperty.SetRef`; öncesinde hiç saklanmıyordu).
899
926
  * `CreateProp` veri tipini `Data`'dan seçtiği için nesne/dizi `data` 500 verir — ref ile değiştirilir.
900
927
  * Aynı sebeple `value`'suz `expression` code sarmalına çevrilir, `value`'nun yanındaki `expression`
901
928
  * silinir. `translation` sarmalı da aynı gerekçeyle olduğu gibi gider (G-04): applier
@@ -947,7 +974,7 @@ function normalizeReplacePropValue(op) {
947
974
  notes.push(`${label}: code sarmalında ref yoktu, data'daki ifade ref'e kopyalandı — runtime ifadeyi ref'ten okur`);
948
975
  }
949
976
  if (code.data !== null && typeof code.data === 'object') {
950
- notes.push(`${label}: code sarmalının nesne/dizi data'sı ref ile değiştirildi — platform code prop'unda data'yı saklamaz, nesne data'yı Dictionary<string, Prop> diye çözüp 500 verir`);
977
+ notes.push(`${label}: code sarmalının nesne/dizi data'sı ref ile değiştirildi — platform code prop'unda yalnız ilkel data'yı (tasarım yedeği) saklar, nesne data'yı Dictionary<string, Prop> diye çözüp 500 verir`);
951
978
  code.data = code.ref;
952
979
  }
953
980
  if (code.data === undefined)
@@ -974,12 +1001,7 @@ function normalizeReplacePropValue(op) {
974
1001
  // düğümü yeniden kurmak ona yazan setter iteratee'lerinin element'ini siler (G-04, FLOW-018).
975
1002
  throw new PlatformApiError(`${label}: '${propType}' tipli Prop replace_prop ile kurulmaz — bu op static, code ve translation sarmalını taşır. Komponenti silip yeniden EKLEMEYİN: ona yazan setter adımlarının hedefi silinir (FLOW-018). Akış bağı için set_event, diğer tipler için page_update(schema) ile tam şema yazın.`, 400);
976
1003
  }
977
- /**
978
- * `typesByElementId` — TB-129: `replace_prop` op'u yalnız elementId taşır, stil çevirisinin
979
- * komponent tipine bakabilmesi için haritayı çağıran (okuduğu şemadan) verir. Verilmezse çeviri
980
- * tip ayrımı yapmaz; bu, harita okunamadığında yazmayı engellememek için bilinçli düşüştür.
981
- */
982
- export function normalizeSchemaOps(ops, typesByElementId) {
1004
+ export function normalizeSchemaOps(ops) {
983
1005
  const components = [];
984
1006
  const props = [];
985
1007
  const validations = [];
@@ -987,10 +1009,7 @@ export function normalizeSchemaOps(ops, typesByElementId) {
987
1009
  for (const op of asObjects(ops)) {
988
1010
  if (op.op === 'replace_prop') {
989
1011
  if (isStylePropName(String(op.prop ?? ''))) {
990
- const inline = String(op.prop) === 'style'
991
- ? inlineStyleFields(typesByElementId?.get(String(op.target ?? '')))
992
- : undefined;
993
- normalizeStyleKeys(op.value, `'${String(op.target ?? '?')}.${String(op.prop)}'`, props, inline);
1012
+ normalizeStyleKeys(op.value, `'${String(op.target ?? '?')}.${String(op.prop)}'`, props);
994
1013
  }
995
1014
  props.push(...normalizeReplacePropValue(op));
996
1015
  continue;
@@ -1342,7 +1361,7 @@ export function spacingLayerWarnings(nodes, unusedCode, numberCode) {
1342
1361
  const hint = node.type === 'slot'
1343
1362
  ? "Slot'un sayfa içeriğinden uzaklığı dış boşluğudur: appearance.marginType:'custom' + marginValue (sayı) yaz — slot'un zemini/çerçevesi olmadığı için görsel sonuç iç boşlukla aynıdır."
1344
1363
  : "İç boşluk yalnız droppable kaplarda ve tipli stil modelindeki yapraklarda (box · text · container · statcard · icon · form) çizilir; kenardan uzaklık için marginType/marginValue, iç boşluk için sarmalayıcı container kullan.";
1345
- warnings.push(`${unusedCode}: '${label}' (${String(node.type ?? '?')}) düğümüne custom iç boşluk yazıldı ama bu düğümde iç boşluk HİÇBİR kutuya çizilmez — platform 200 döner, ekran değişmez. ${hint}`);
1364
+ warnings.push(`${unusedCode}: '${label}' (${String(node.type ?? '?')}) düğümü custom iç boşluk taşıyor ama bu düğümde iç boşluk HİÇBİR kutuya çizilmez — platform 200 döner, ekran değişmez. ${hint}`);
1346
1365
  }
1347
1366
  walk(asObjects(node.children));
1348
1367
  }
@@ -1643,12 +1662,34 @@ function gapWalk(parentLabel, children, code, onlyParents, isRoot, warnings, par
1643
1662
  * sayfadan ayrılmadan koşar (`Runtime.svelte initDestroyGuard`). Platform uydurma adı 200 ile
1644
1663
  * kaydeder ama runtime eşleştiremez ve bağ sessizce ölü kalır (PAGE-008'in sayfa/layout ikizi).
1645
1664
  */
1646
- const SCREEN_EVENT_NAMES = new Set(['mount', 'destroy']);
1647
- export function invalidScreenEventNames(events, code) {
1665
+ const SCREEN_EVENT_NAMES = ['mount', 'destroy'];
1666
+ /**
1667
+ * 38 S-15 / kök ADR 0015: mobil SAYFA bir olay daha taşır, `focus` — ekran ilk göründüğünde ve her geri
1668
+ * dönüşte (yığından geri, sekme değişimi) koşar; yığındaki ekran bağlı kaldığı için `mount` geri dönüşte
1669
+ * koşmaz. Web her gezinmede sayfayı baştan kurar (`Runtime.svelte` focus'u tanımaz), layout'un focus'u
1670
+ * yoktur. Ayna: `ng-apps-ui-native` `SCREEN_LIFECYCLE_EVENTS` (parite: `test/page-meta.test.ts`).
1671
+ */
1672
+ const MOBILE_PAGE_EVENT_NAMES = [...SCREEN_EVENT_NAMES, 'focus'];
1673
+ const SCREEN_EVENT_HINTS = {
1674
+ mount: 'mount (yüklendiğinde)',
1675
+ destroy: 'destroy (ayrılırken; temizlik akışları için)',
1676
+ focus: 'focus (ekran her göründüğünde — ilk açılış ve geri dönüş; listeyi yeniden okumak için)'
1677
+ };
1678
+ export function screenEventNames(kind, platform) {
1679
+ return kind === 'page' && platform === 'mobile' ? MOBILE_PAGE_EVENT_NAMES : SCREEN_EVENT_NAMES;
1680
+ }
1681
+ /** Olaylardan biri yalnız mobil sayfada geçerliyse karar uygulamanın platformunu ister (page_update şemasız yazımı). */
1682
+ export function screenEventsNeedPlatform(events) {
1683
+ return asObjects(events).some(event => !SCREEN_EVENT_NAMES.includes(String(event.name ?? '')) && MOBILE_PAGE_EVENT_NAMES.includes(String(event.name ?? '')));
1684
+ }
1685
+ export function invalidScreenEventNames(events, code, names = SCREEN_EVENT_NAMES) {
1686
+ const valid = names.map(name => SCREEN_EVENT_HINTS[name] ?? name).join(' · ');
1648
1687
  return asObjects(events)
1649
1688
  .map(event => String(event.name ?? ''))
1650
- .filter(name => name !== '' && !SCREEN_EVENT_NAMES.has(name))
1651
- .map(name => `${code}: sayfa/layout düzeyinde '${name}' diye bir olay YOK — geçerli adlar: mount (yüklendiğinde) · destroy (ayrılırken; temizlik akışları için). Platform kaydı kabul eder ama bağ sessizce ölü kalır.`);
1689
+ .filter(name => name !== '' && !names.includes(name))
1690
+ .map(name => MOBILE_PAGE_EVENT_NAMES.includes(name)
1691
+ ? `${code}: '${name}' yalnız mobil uygulamanın SAYFASINDA vardır (layout'ta ve web sayfasında yok; web sayfayı her gezinmede baştan kurar, mount yeter) — burada geçerli adlar: ${valid}.`
1692
+ : `${code}: sayfa/layout düzeyinde '${name}' diye bir olay YOK — geçerli adlar: ${valid}. Platform kaydı kabul eder ama bağ sessizce ölü kalır.`);
1652
1693
  }
1653
1694
  const GUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1654
1695
  /**
@@ -1716,9 +1757,9 @@ function collectDesktopPositions(nodes, into) {
1716
1757
  * görünen sıraya yerleştir, x=0, birikimli y). Burada aynısı yapılır: akış kabında `index` GÖRÜNEN
1717
1758
  * sıradaki yerdir (eski kayıtlarda dizi sırası çizimle aynı değildir, Studio sürüklerken diziyi
1718
1759
  * değil y'yi yazar). Op'lar okunan şemanın kopyasına sırayla uygulanır; eklenen düğümün son konumu
1719
- * kendi gövdesine, var olan kardeşlerinki yamanın sonuna `move_component {target, position}` olarak
1720
- * yazılır. Platform position'ı düğümün İLK bloğuna yazar: ilk bloğu desktop olmayan kardeş atlanır ve
1721
- * uyarılır. Izgara kapta çizim mutlak koordinattır; sona eklemeyen `index` orada PAGE-033 uyarısıdır.
1760
+ * kendi gövdesine, var olan kardeşlerinki yamanın sonuna `move_component {target, breakpoint: 1024,
1761
+ * position}` olarak yazılır. `breakpoint` açık yazılır: ilk bloğu mobil kırılım olan kardeşte mobil
1762
+ * konum ezilmez (PAGE-033, K9). Izgara kapta çizim mutlak koordinattır; sona eklemeyen `index` orada PAGE-033 uyarısıdır.
1722
1763
  * `index` verilmemiş op'a dokunulmaz.
1723
1764
  */
1724
1765
  export function flexIndexOrder(schema, ops) {
@@ -1831,11 +1872,7 @@ export function flexIndexOrder(schema, ops) {
1831
1872
  const before = original.get(elementId);
1832
1873
  if (!body && !moveTargets.has(elementId) && before && before.x === final.x && before.y === final.y)
1833
1874
  continue;
1834
- if (Number(asObjects(node.blocks)[0]?.breakpoint) !== DESKTOP_BREAKPOINT) {
1835
- warnings.push(`PAGE-033 (uyarı): '${elementId}' flex sırasına göre y:${final.y}'e taşınmalıydı ama ilk bloğu desktop (1024) değil — platform move_component'in position'ını İLK bloğa yazar, yanlış kırılımı bozmamak için konum yazılmadı. Düğümü page_update(schema) ile düzeltin.`);
1836
- continue;
1837
- }
1838
- trailing.push({ op: 'move_component', target: elementId, position: { x: final.x, y: final.y } });
1875
+ trailing.push({ op: 'move_component', target: elementId, breakpoint: DESKTOP_BREAKPOINT, position: { x: final.x, y: final.y } });
1839
1876
  moved.push(`${elementId} y:${before?.y ?? '?'}→${final.y}`);
1840
1877
  }
1841
1878
  return { ops: [...sent, ...trailing], moved, warnings };
@@ -2018,51 +2055,60 @@ export function splitJunctionFields(definition, isJunction) {
2018
2055
  return { base: { ...definition, fields: fields.filter(field => !isJunction(field)) }, deferred };
2019
2056
  }
2020
2057
  /**
2021
- * `remove_component` şemadan bir dalı çıkarır ama etkisi dalla sınırlı DEĞİLDİR: sayfa
2022
- * kaydedilirken artık hiçbir komponent olayının, prop'unun ya da sayfa olayının referans vermediği
2023
- * akışlar platformca KALICI olarak silinir (`PageService.UpdateAsync` → `flowRepository
2024
- * .DeleteRangeAsync`). Micro-op hattının "yalnız hedefe dokunur" garantisinin tek istisnası budur;
2025
- * çoğu zaman istenen davranıştır, istenmediğinde ise uyarı tek erken sinyaldir.
2026
- */
2027
- /**
2028
- * Repeater içinde repeater arar. ÖLÇÜLDÜ (2026-08-19, anket-admin probe sayfası): iç repeater
2029
- * OKUMADA çalışır (dış satır başına doğru sayıda iç satır çizilir) ama YAZMA ölüdür —
2030
- * iç girdiyi değiştirince dış satırın alanı değişmez ve iç repeater'ın `_repeaterValues`'ü null
2031
- * kalır. Sebep yapısal: şemada iç repeater TEK düğümdür, dış satır başına ayrı düğüm yoktur;
2032
- * `_repeaterValues` anahtarı iç satır id'sine göredir ve dış satırlar arasında çakışır.
2033
- * Üstelik tek bir düzenlemeden sonra DOM bozuluyor (ölçümde girdi sayısı 3 → 19) ve ekranda
2034
- * sürekli "Expression error: Cannot read properties of null" toast'ı çıkıyor.
2058
+ * Satır listesinin alt ağacındaki satır listesini arar (`MOBILE_ROW_LIST_TYPES`; web'de yalnız `repeater` vardır).
2059
+ * ÖLÇÜLDÜ (2026-08-19, anket-admin probe sayfası, repeater içinde repeater): iç liste OKUMADA çalışır (dış satır
2060
+ * başına doğru sayıda iç satır çizilir) ama YAZMA ölüdür — iç girdiyi değiştirince dış satırın alanı değişmez ve iç
2061
+ * listenin `_repeaterValues`'ü null kalır. Sebep yapısal: şemada iç liste TEK düğümdür, dış satır başına ayrı düğüm
2062
+ * yoktur; `_repeaterValues` anahtarı iç satır id'sine göredir ve dış satırlar arasında çakışır. Üstelik tek bir
2063
+ * düzenlemeden sonra DOM bozuluyor (ölçümde girdi sayısı 3 → 19) ve ekranda sürekli "Expression error: Cannot read
2064
+ * properties of null" toast'ı çıkıyor. Öteki iki tip aynı satır kapsamını kullanır (frontend `ROW_LIST_TYPES`), bu
2065
+ * yüzden kural üçünün çaprazına bakar (2026-10-09, plans/mobil-animasyon-sistemi faz 04 F-3).
2066
+ * Satır kapsamı satır ŞABLONUDUR: MOBİL listenin boş görünümü (`MOBILE_OPTIONAL_VIEW_INDEX`) sayfa kapsamındadır
2067
+ * (frontend `row-list-type.ts` `rowTemplateNodes`) — içindeki liste satır başına çoğalmaz ve iç içe sayılmaz; o alt ağaç
2068
+ * listenin kendi dış kapsamını taşır. Web `repeater`ının görünümü yoktur (şemasında `views` yok, çizeri bütün
2069
+ * çocukları satır başına çizer): orada `view` sıradan bir çocuktur, istisna uygulanmaz.
2035
2070
  * Doğru desen: iç listeyi tek bir `textarea`'da satır başına bir öğe olarak topla.
2036
2071
  */
2037
- export function nestedRepeaterNodes(nodes) {
2072
+ export function nestedRepeaterNodes(nodes, platform = 'web') {
2038
2073
  const found = [];
2039
- const walk = (list, insideRepeater) => {
2074
+ const walk = (list, insideType) => {
2040
2075
  for (const node of list) {
2041
- const isRepeater = node.type === 'repeater';
2042
- if (isRepeater && insideRepeater)
2043
- found.push(String(node.elementId ?? node.name ?? '?'));
2044
- walk(asObjects(node.children), insideRepeater || isRepeater);
2076
+ const type = String(node.type ?? '');
2077
+ const isRowList = MOBILE_ROW_LIST_TYPES.includes(type);
2078
+ if (isRowList && insideType !== undefined)
2079
+ found.push({ id: String(node.elementId ?? node.name ?? '?'), type, insideType });
2080
+ const children = asObjects(node.children);
2081
+ const emptyView = isRowList && platform === 'mobile' ? children.filter(child => child.type === 'view')[MOBILE_OPTIONAL_VIEW_INDEX[type] ?? -1] : undefined;
2082
+ for (const child of children)
2083
+ walk([child], isRowList && child !== emptyView ? type : insideType);
2045
2084
  }
2046
2085
  };
2047
- walk(asObjects(nodes), false);
2086
+ walk(asObjects(nodes), undefined);
2048
2087
  return found;
2049
2088
  }
2050
2089
  /**
2051
- * Mobil profilde gruplu listenin (gün/kategori başlığı altında satırlar) hazır bileşeni vardır:
2052
- * `grouped-list` (ui-native, ADR 0007). Web'de karşılığı yok — web mesajı textarea tarifinde kalır.
2090
+ * Mesaj hangi tipin hangi tipin içinde durduğunu söyler. Ölçüm cümlesi ve gruplu liste önerisi yalnız ölçülen ve
2091
+ * önerinin karşıladığı şekilde kalır (`repeater` içinde `repeater`): mobil profilde gruplu listenin (gün/kategori
2092
+ * başlığı altında satırlar) hazır bileşeni vardır, `grouped-list` (ui-native, ADR 0007); web'de karşılığı yok —
2093
+ * web mesajı textarea tarifinde kalır.
2053
2094
  */
2054
- export function nestedRepeaterMessage(ids, platform = 'web') {
2055
- const measured = `PAGE-013 — repeater içinde repeater (istek gönderilmedi): ${ids.join(' · ')}. `
2056
- + 'Ölçüldü (2026-08-19): iç repeater OKUMADA çalışır ama girilen değer dış satıra GERİ YAZILMAZ '
2057
- + '(_repeaterValues null kalır) ve tek düzenlemeden sonra DOM bozulur. Sebep yapısal: şemada iç '
2058
- + 'repeater tek düğümdür, dış satır başına ayrı düğüm yoktur. ';
2095
+ export function nestedRepeaterMessage(found, platform = 'web') {
2096
+ const pairs = [...new Set(found.map(entry => `${entry.insideType} içinde ${entry.type}`))];
2097
+ const located = pairs.map(pair => `${pair}: ${found.filter(entry => `${entry.insideType} içinde ${entry.type}` === pair).map(entry => entry.id).join(' · ')}`);
2098
+ const repeaterInRepeater = found.some(entry => entry.type === 'repeater' && entry.insideType === 'repeater');
2059
2099
  const textarea = "Satır içi listeyi tek bir textarea ile topla (satır başına bir öğe) ve kaydederken split('\n') ile böl — platform_guide('page') Kural 1g.";
2060
- if (platform !== 'mobile')
2061
- return measured + textarea;
2062
- return measured
2063
- + 'Başlıklar altında gruplanmış liste (güne/kategoriye göre) için mobilde tek `grouped-list` kur: düz `rows` + '
2064
- + '`groupBy` alan yolu; çocuk 0 bölüm başlığı (`{{ Section.title }}`), çocuk 1 öğe şablonu (`Row`, `{{ item.<alan> }}`) — '
2065
- + "platform_guide('mobile') §Güne göre görev listesi. Satır içinde düzenlenen alt liste gerekiyorsa: " + textarea;
2100
+ return `PAGE-013 — ${located.join('; ')} (istek gönderilmedi). `
2101
+ + 'Satır kapsamı tek seviyedir: şemada iç liste tek düğümdür, dış satır başına ayrı düğüm yoktur. '
2102
+ + (repeaterInRepeater
2103
+ ? 'Ölçüldü (2026-08-19): iç repeater OKUMADA çalışır ama girilen değer dış satıra GERİ YAZILMAZ '
2104
+ + '(_repeaterValues null kalır) ve tek düzenlemeden sonra DOM bozulur. '
2105
+ : 'İç listeyi dış listenin şablonundan çıkarın (kardeşi yapın ya da ayrı sayfaya/modala taşıyın). ')
2106
+ + (repeaterInRepeater && platform === 'mobile'
2107
+ ? 'Başlıklar altında gruplanmış liste (güne/kategoriye göre) için mobilde tek `grouped-list` kur: düz `rows` + '
2108
+ + '`groupBy` alan yolu; çocuk 0 bölüm başlığı (`{{ Section.title }}`), çocuk 1 öğe şablonu (`Row`, `{{ item.<alan> }}`) — '
2109
+ + "platform_guide('mobile') §Güne göre görev listesi. Satır içinde düzenlenen alt liste gerekiyorsa: "
2110
+ : '')
2111
+ + textarea;
2066
2112
  }
2067
2113
  /**
2068
2114
  * `style` prop'u düz CSS STRING verilmiş düğümler. Çalışır (emotion string'i CSS metni sayar) ama
@@ -37,7 +37,7 @@ function listSkeleton() {
37
37
  return [
38
38
  { op: 'add_component', parent: 'page_container', component: node('list_toolbar', 'container', { layout: staticProp('flex'), direction: staticProp('row') }, true, { h: 4, w: 12, x: 0, y: 0 }) },
39
39
  { op: 'add_component', parent: 'list_toolbar', component: node('btn_new', 'button', { text: staticProp('Yeni') }, false, { h: 4, w: 3, x: 0, y: 0 }) },
40
- { op: 'add_component', parent: 'page_container', component: node('records_table', 'table', {}, false, { h: 40, w: 12, x: 0, y: 4 }) }
40
+ { op: 'add_component', parent: 'page_container', component: node('records_table', 'datatable', {}, false, { h: 40, w: 12, x: 0, y: 4 }) }
41
41
  ];
42
42
  }
43
43
  function pageArgs(screenName, base, options, caveats) {
@@ -120,7 +120,7 @@ export function discoverPage(snapshot, screenRef, options = {}) {
120
120
  const events = readExample(LIST_EVENTS_EXAMPLE);
121
121
  const ops = [...listSkeleton(), ...(events.schemaPatchOps ?? [])];
122
122
  const presetBindings = bindPresets(ops, presets, caveats);
123
- caveats.push("records_table props boş — kolonları ve veri kaynağını component_get('table') + flow-load-table örneğine göre verin; set_event op'larındaki '<… flow id>' yer tutucuları flow_create sonrası gerçek id ile değiştirilir");
123
+ caveats.push("records_table props boş — kolonları ve veri kaynağını component_get('datatable') + flow-load-table örneğine göre verin (tek mount akışı: süzme/sıralama/sayfa değişimi mount'u yeniden tetikler); set_event op'larındaki '<… flow id>' yer tutucuları flow_create sonrası gerçek id ile değiştirilir");
124
124
  return {
125
125
  ...head,
126
126
  archetype: LIST_ARCHETYPE,