@appaflytech/zeyrek-ai-mcp 0.0.48 → 0.0.50

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 (70) hide show
  1. package/.env.example +5 -0
  2. package/dist/config.js +2 -0
  3. package/dist/knowledge/access.js +4 -3
  4. package/dist/knowledge/assistantRun.js +165 -0
  5. package/dist/knowledge/componentCatalog.js +31 -0
  6. package/dist/knowledge/componentEvents.js +4 -1
  7. package/dist/knowledge/mobileComponentCatalog.js +21 -3
  8. package/dist/knowledge/mobileComponentEvents.js +9 -0
  9. package/dist/knowledge/mobileStore.js +14 -0
  10. package/dist/knowledge/motionResolve.js +1 -1
  11. package/dist/knowledge/platform/lint.js +36 -3
  12. package/dist/knowledge/platform/mobileRules.js +6 -0
  13. package/dist/knowledge/styleResolve.js +7 -0
  14. package/dist/platform/capture/actions.js +67 -0
  15. package/dist/platform/capture/capture.js +26 -21
  16. package/dist/platform/errors.js +9 -0
  17. package/dist/platform/http.js +7 -2
  18. package/dist/platform/storeArt/compose.js +114 -0
  19. package/dist/platform/storeArt/deck.js +133 -0
  20. package/dist/platform/storeArt/gallery.js +331 -0
  21. package/dist/platform/storeArt/layouts.js +121 -0
  22. package/dist/platform/storeArt/styles.js +54 -0
  23. package/dist/platform/tokenVault.js +15 -0
  24. package/dist/resources/examples/mobile-reorder-persist.json +491 -0
  25. package/dist/resources/guides/assistant.md +194 -0
  26. package/dist/resources/guides/deployment.md +19 -1
  27. package/dist/resources/guides/microservice.md +7 -0
  28. package/dist/resources/guides/mobile-publish.md +13 -0
  29. package/dist/resources/guides/mobile.md +4 -0
  30. package/dist/resources/guides/page.md +62 -0
  31. package/dist/resources/prompts/mobile-publish-prepare.md +3 -1
  32. package/dist/resources/schema-fingerprint.json +4 -4
  33. package/dist/resources/schemas/AccessMatrixDto.json +2 -1
  34. package/dist/resources/schemas/AccessPermissionDto.json +2 -1
  35. package/dist/resources/schemas/AccessResourceDto.json +2 -1
  36. package/dist/resources/schemas/AssistantCapabilitiesDto.json +256 -0
  37. package/dist/resources/schemas/AssistantPublishStateDto.json +57 -0
  38. package/dist/resources/schemas/AssistantScenarioDto.json +91 -0
  39. package/dist/resources/schemas/AssistantScenarioRunDto.json +62 -0
  40. package/dist/resources/schemas/AssistantSettingsDto.json +52 -0
  41. package/dist/resources/schemas/AssistantSkillDto.json +78 -0
  42. package/dist/resources/schemas/AssistantSummaryDto.json +100 -0
  43. package/dist/resources/schemas/CreateAssistantScenarioCommand.json +81 -0
  44. package/dist/resources/schemas/CreateAssistantSkillCommand.json +83 -0
  45. package/dist/resources/schemas/CreateMicroserviceCommand.json +16 -0
  46. package/dist/resources/schemas/DescribeAssistantCapabilityCommand.json +145 -0
  47. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +5 -1
  48. package/dist/resources/schemas/MicroserviceDto.json +16 -0
  49. package/dist/resources/schemas/PublishAssistantCommand.json +15 -0
  50. package/dist/resources/schemas/RunAssistantScenariosCommand.json +39 -0
  51. package/dist/resources/schemas/SimulateAssistantCommand.json +106 -0
  52. package/dist/resources/schemas/UpdateAssistantScenarioCommand.json +79 -0
  53. package/dist/resources/schemas/UpdateAssistantSettingsCommand.json +52 -0
  54. package/dist/resources/schemas/UpdateAssistantSkillCommand.json +81 -0
  55. package/dist/resources/schemas/UpdateMicroserviceCommand.json +16 -0
  56. package/dist/resources/store-art/editor.css +389 -0
  57. package/dist/resources/store-art/editor.html +123 -0
  58. package/dist/resources/store-art/editor.js +990 -0
  59. package/dist/server.js +2 -0
  60. package/dist/tools/applications.js +4 -4
  61. package/dist/tools/assistant.js +382 -0
  62. package/dist/tools/deployments.js +9 -3
  63. package/dist/tools/microservices.js +1 -1
  64. package/dist/tools/mobile.js +2 -2
  65. package/dist/tools/mobileStore.js +56 -19
  66. package/dist/tools/pages.js +21 -5
  67. package/dist/tools/projects.js +2 -2
  68. package/dist/tools/templates.js +1 -1
  69. package/dist/tools/themes.js +2 -2
  70. package/package.json +2 -2
package/.env.example CHANGED
@@ -78,3 +78,8 @@ TENANT_SWAGGER_URL=
78
78
  MODULES=
79
79
 
80
80
  MCP_PORT=5100
81
+
82
+ # Uygulama asistanı (Anthropic). docker-compose*.yml Assistant__ApiKey / Assistant__WorkspaceId buradan okur;
83
+ # anahtar appsettings.json'a ya da compose dosyasına yazılmaz. Kullanıcı anahtarı (sk-ant-usr-…) çalışma alanı kimliği ister.
84
+ ASSISTANT_API_KEY=
85
+ ANTHROPIC_WORKSPACE_ID=
package/dist/config.js CHANGED
@@ -48,6 +48,7 @@ export const moduleNames = [
48
48
  "caseRuntime",
49
49
  "module",
50
50
  "mobile",
51
+ "assistant",
51
52
  ];
52
53
  export const modulePresets = {
53
54
  core: [
@@ -103,6 +104,7 @@ export const modulePresets = {
103
104
  "revision",
104
105
  "connectData",
105
106
  "mobile",
107
+ "assistant",
106
108
  ],
107
109
  admin: ["platform", "template", "organization", "deployment", "environment", "merge", "activity"],
108
110
  };
@@ -53,7 +53,7 @@ export function systemManagedEntity(ref) {
53
53
  }
54
54
  /**
55
55
  * İzin kataloğu (`AccessPermissionConstants`, b-22 — 43 slug; BPM b-01 beşini ekledi, 2026-09-24'te
56
- * `automation.trigger` çıktı: otomasyon yetki kataloğunda değil — K-2). `supportsScope` altı entity
56
+ * `automation.trigger` çıktı: otomasyon yetki kataloğunda değil — K-2; 2026-10-22'de tür kapsamlı `assistant.use` girdi). `supportsScope` altı entity
57
57
  * eyleminde ve `process.view-all`'da, `supportsFieldAccess` yalnız `field.read`'te. Katalog yalnız kompleks projede
58
58
  * tohumlanır; kaynağı `access_permission_list`'tir, bu kopya istek gitmeden denetim içindir.
59
59
  */
@@ -66,10 +66,11 @@ export const accessPermissionSlugs = [
66
66
  'file.read', 'file.upload', 'file.delete', 'folder.read', 'folder.upload', 'folder.delete',
67
67
  'organization-unit.read', 'organization-unit.manage', 'organization-unit.assign',
68
68
  'system.access.manage', 'system.access.explain', 'system.user.read', 'system.user.manage',
69
- 'process.start', 'process.manage', 'process.view-all', 'task.complete', 'task.reassign'
69
+ 'process.start', 'process.manage', 'process.view-all', 'task.complete', 'task.reassign',
70
+ 'assistant.use'
70
71
  ];
71
72
  export const scopedPermissions = new Set(['entity.read', 'entity.update', 'entity.delete', 'entity.export', 'entity.reveal', 'entity.reassign', 'process.view-all']);
72
- export const accessResourceTypes = ['entity', 'field', 'row', 'microservice', 'page', 'workflow', 'automation', 'ui-flow', 'function', 'microfunction', 'query', 'view', 'file', 'folder', 'organization-unit', 'system', 'process', 'task'];
73
+ export const accessResourceTypes = ['entity', 'field', 'row', 'microservice', 'page', 'workflow', 'automation', 'ui-flow', 'function', 'microfunction', 'query', 'view', 'file', 'folder', 'organization-unit', 'system', 'process', 'task', 'assistant'];
73
74
  export const accessScopes = ['own', 'unit', 'unit-tree', 'subordinates', 'function', 'company', 'all'];
74
75
  const knownPermissions = new Set(accessPermissionSlugs);
75
76
  const conditionSubjects = new Set(['userId', 'unitIds', 'positionIds', 'companyIds', 'roleSlugs', 'functionCodes']);
@@ -0,0 +1,165 @@
1
+ import { maskSecrets } from '../platform/secrets.js';
2
+ /** `event: x\ndata: {...}\n\n` bloklarını olay listesine çevirir; bozuk bir veri satırı atlanır */
3
+ export function parseSseEvents(text) {
4
+ const events = [];
5
+ for (const block of text.split(/\r?\n\r?\n/)) {
6
+ let type = 'message';
7
+ const dataLines = [];
8
+ for (const line of block.split(/\r?\n/)) {
9
+ if (line.startsWith('event:'))
10
+ type = line.slice(6).trim();
11
+ else if (line.startsWith('data:'))
12
+ dataLines.push(line.slice(5).trim());
13
+ }
14
+ if (dataLines.length === 0)
15
+ continue;
16
+ try {
17
+ events.push({ type, data: JSON.parse(dataLines.join('\n')) });
18
+ }
19
+ catch {
20
+ events.push({ type, data: { raw: dataLines.join('\n') } });
21
+ }
22
+ }
23
+ return events;
24
+ }
25
+ const BLOCK_SAMPLE_ROWS = 20;
26
+ const BLOCK_ROW_FIELDS = ['id', 'label', 'status', 'statusLabel', 'href'];
27
+ /** Senaryo koşusunun ve simülasyonun olay akışını tek özet nesneye indirir; metin yalnız cevap olayından gelir */
28
+ export function summarizeAssistantRun(events) {
29
+ const summary = {};
30
+ const scenarios = [];
31
+ const calls = [];
32
+ const sources = [];
33
+ let text = '';
34
+ for (const event of events) {
35
+ const data = event.data;
36
+ switch (event.type) {
37
+ case 'scenario':
38
+ scenarios.push({ code: String(data.code ?? ''), status: String(data.status ?? ''), reason: data.reason ?? null, calledCapabilities: data.calledCapabilities ?? [], durationMs: Number(data.durationMs ?? 0), ...(data.answer !== undefined ? { answer: data.answer ?? null } : {}), ...(data.refusal !== undefined ? { refusal: data.refusal ?? null } : {}) });
39
+ break;
40
+ case 'skill':
41
+ summary.skill = { slug: String(data.slug ?? ''), name: String(data.name ?? '') };
42
+ break;
43
+ case 'tool_start':
44
+ calls.push({ slug: String(data.slug ?? ''), statusCode: null, bytes: 0, denied: false, ...(data.arguments != null ? { arguments: maskSecrets(data.arguments).payload } : {}) });
45
+ break;
46
+ case 'tool_result': {
47
+ const call = calls.find(candidate => candidate.slug === String(data.slug ?? '') && candidate.statusCode === null);
48
+ if (call)
49
+ Object.assign(call, { statusCode: typeof data.statusCode === 'number' ? data.statusCode : null, bytes: Number(data.bytes ?? 0), denied: data.denied === true });
50
+ break;
51
+ }
52
+ case 'source':
53
+ sources.push(String(data.slug ?? ''));
54
+ break;
55
+ case 'suggestion': {
56
+ const args = data.arguments ?? {};
57
+ const choices = Array.isArray(data.choices) ? data.choices : [];
58
+ const properties = (data.inputSchema?.properties) ?? {};
59
+ summary.suggestion = {
60
+ suggestionId: String(data.suggestionId ?? ''),
61
+ slug: String(data.slug ?? ''),
62
+ httpMethod: String(data.httpMethod ?? ''),
63
+ arguments: args,
64
+ missing: data.missing ?? [],
65
+ fields: Object.keys(args),
66
+ choices: choices.map(choice => ({ field: String(choice.field ?? ''), total: Number(choice.total ?? 0), searchable: choice.searchable === true, preselected: typeof choice.preselected === 'string' ? choice.preselected : null, options: (choice.options ?? []).map(option => String(option.label ?? '')) })),
67
+ choicesError: data.choicesError ?? {},
68
+ display: data.display ?? {},
69
+ titles: Object.fromEntries(Object.entries(properties).filter(([, property]) => typeof property?.title === 'string').map(([key, property]) => [key, String(property.title)])),
70
+ formHref: typeof data.formHref === 'string' ? data.formHref : null
71
+ };
72
+ break;
73
+ }
74
+ case 'refusal':
75
+ summary.refusal = String(data.kind ?? '');
76
+ break;
77
+ case 'text':
78
+ text += String(data.text ?? '');
79
+ break;
80
+ case 'clarify':
81
+ summary.clarify = String(data.question ?? '');
82
+ break;
83
+ case 'usage':
84
+ summary.usage = data;
85
+ break;
86
+ case 'error':
87
+ summary.error = { code: String(data.code ?? ''), message: String(data.message ?? '') };
88
+ break;
89
+ case 'block': {
90
+ const block = data.data ?? {};
91
+ const rows = Array.isArray(block.rows) ? block.rows.length : block.fields ? 1 : 0;
92
+ const sample = Array.isArray(block.rows)
93
+ ? block.rows.slice(0, BLOCK_SAMPLE_ROWS).map(row => Object.fromEntries(BLOCK_ROW_FIELDS.filter(field => row[field] !== undefined && row[field] !== null).map(field => [field, row[field]])))
94
+ : undefined;
95
+ summary.blocks = [...(summary.blocks ?? []), { kind: String(data.kind ?? ''), capability: String(block.capability ?? ''), rows, total: Number(block.total ?? rows), truncated: block.truncated === true, ...(sample ? { sample } : {}), ...(block.href ? { path: block.path, href: block.href } : {}) }];
96
+ break;
97
+ }
98
+ case 'stage':
99
+ summary.stages = [...(summary.stages ?? []), { stage: String(data.stage ?? ''), status: String(data.status ?? ''), ...(data.detail != null ? { detail: data.detail } : {}) }];
100
+ break;
101
+ case 'published':
102
+ summary.published = data;
103
+ break;
104
+ }
105
+ }
106
+ if (scenarios.length > 0) {
107
+ summary.scenarios = scenarios;
108
+ summary.passed = scenarios.filter(scenario => scenario.status === 'passed').length;
109
+ summary.failed = scenarios.filter(scenario => scenario.status === 'failed').length;
110
+ summary.skipped = scenarios.filter(scenario => scenario.status === 'skipped').length;
111
+ }
112
+ if (calls.length > 0)
113
+ summary.calls = calls;
114
+ if (sources.length > 0)
115
+ summary.sources = sources;
116
+ if (text)
117
+ summary.text = text;
118
+ return summary;
119
+ }
120
+ const filledLanguages = (texts) => Object.entries(texts ?? {}).filter(([, text]) => String(text ?? '').trim().length > 0).map(([language]) => language);
121
+ /** Sunucu sayfasını özetler; kind/origin/includeExcluded yalnız bu sayfanın satırlarını süzer, total sunucunun süzgecinden gelir (41 §5) */
122
+ export function summarizeCapabilityPage(page, filter) {
123
+ const total = Number(page.meta?.totalData ?? page.totalData ?? 0);
124
+ const pageIndex = Number(page.pageIndex ?? 0);
125
+ const pageLength = Number(page.pageLength ?? 0);
126
+ const received = page.data ?? [];
127
+ const rows = received.filter(row => {
128
+ const kind = String(row.kind ?? '').toLowerCase();
129
+ if (filter.includeExcluded === false && kind === 'excluded')
130
+ return false;
131
+ if (filter.kind && kind !== filter.kind)
132
+ return false;
133
+ if (filter.origin === 'module' && !row.templateId)
134
+ return false;
135
+ if (filter.origin === 'own' && row.templateId)
136
+ return false;
137
+ return true;
138
+ });
139
+ return {
140
+ total,
141
+ pageIndex,
142
+ pageLength,
143
+ hasMore: (pageIndex + 1) * pageLength < total,
144
+ ...(rows.length < received.length ? { hiddenOnPage: received.length - rows.length } : {}),
145
+ capabilities: rows.map(row => {
146
+ const hasDescription = String(row.description ?? '').trim().length > 0;
147
+ return filter.detail === true
148
+ ? { ...row, kind: String(row.kind ?? '').toLowerCase(), hasDescription }
149
+ : { id: row.id, slug: row.slug, name: row.name, kind: String(row.kind ?? '').toLowerCase(), origin: row.templateId ? 'module' : 'own', moduleName: row.moduleName ?? null, exclusion: row.exclusion ?? null, hasDescription, descriptionLanguages: filledLanguages(row.descriptions) };
150
+ })
151
+ };
152
+ }
153
+ /** describe gövdesini mevcut hâlin üstüne kurar: verilmeyen alan korunur, null temizler, dil haritaları dil dil birleşir (boş metin o dili siler) */
154
+ export function mergeCapabilityTexts(current, input) {
155
+ return {
156
+ assistantDescription: { ...(current.descriptions ?? {}), ...(input.assistantDescription ?? {}) },
157
+ assistantCaution: input.assistantCaution === null ? {} : { ...(current.cautions ?? {}), ...(input.assistantCaution ?? {}) },
158
+ recordPageId: input.recordPageId === undefined ? current.recordPageId ?? null : input.recordPageId,
159
+ recordQuery: input.recordQuery === undefined ? current.recordQuery ?? null : input.recordQuery,
160
+ formPageId: input.formPageId === undefined ? current.formPageId ?? null : input.formPageId,
161
+ formQuery: input.formQuery === undefined ? current.formQuery ?? null : input.formQuery,
162
+ hints: input.hints === undefined ? current.hints ?? null : input.hints,
163
+ kindOverride: input.kindOverride === undefined ? current.kindOverride ?? null : input.kindOverride
164
+ };
165
+ }
@@ -1958,6 +1958,37 @@ export const componentKnowledge = {
1958
1958
  autofocus: 'Sayfa açılınca odak gruba gelir (kilitli/yükleniyor grupta ve tasarım kanvasında çalışmaz). Grubun tek alanı olmadığı için odak ilk kilitsiz kutuya düşer: seç-tümü çiziliyse o, yoksa ilk seçenek. Sayfada tek alana ver.'
1959
1959
  }
1960
1960
  },
1961
+ assistant: {
1962
+ summary: 'Uygulama asistanı (frontend ADR 0030): tasarımcının layout’a ya da sayfaya koyduğu bileşen. mode launcher (sağ altta düğme + çekmece), panel (gömülü) ya da inline (yalnız soru kutusu). Yetenekleri runtime’dan bir kez ister; asistan uygulamada kapalıysa (404) ya da oturum yoksa (401) HİÇBİR ŞEY çizmez. Cevaplar SSE ile akar: kayıt listesi bloğu (kayıt sayfasına Aç bağlantısı), yazma için onay kartı, ret rozeti.',
1963
+ behavior: [
1964
+ 'HANGİ SAYFADA: layout’a konursa o layout’un bütün sayfalarında, sayfaya konursa yalnız o sayfada görünür; giriş ve hata sayfaları layout dışı olduğu için bileşensizdir — kabuk artık asistanı kendiliğinden eklemez (RuntimeShellSlots kaldırıldı, 2026-10-22).',
1965
+ "mode 'launcher': sağ altta (placement ile sol altta) yuvarlak düğme; tıklama ya da hotkey (varsayılan Ctrl+J) genişliği width px olan sabit çekmeceyi açar, Esc kapatır; çekmece açıkken kabuk `--assistant-panel-width` kadar daralır (dock 'push') ya da çekmece sayfanın üstünde açılır (dock 'overlay'; varsayılan 'auto' sayfaya 1024 px'ten az kalacaksa üstte açar); başlatıcı yerleşimde hücre kaplamaz (düğüm sarmalayıcısız çizilir, düğümün boyutu ve kabın flex/grid yeri etkisizdir), tasarımcıda yalnız düğmesi görünür. mode 'panel': düğümün kutusunu dolduran gömülü panel (yüksekliği düğümün yüksekliğidir). mode 'inline': yalnız soru kutusu, cevaplar kutunun ALTINA açılır (ana sayfa arama kutusu deseni).",
1966
+ 'Studio önizlemesinde (kanvas) yalnız kabuk çizilir ve sohbet çalışmaz; soru kutusu kapalıdır, günlükte previewNote kelimesi görünür. Gerçek davranış yalnız runtime’da (/dev, /branch, /view) ölçülür.',
1967
+ "KAYIT LİSTESİ: backend `block` olayı (kind 'records') geldiğinde satırlar etiket + durum + tarih + 'Aç' bağlantısıyla çizilir; bağlantı backend’in verdiği href’tir (yetenek `recordPageId` ile eşlenmişse) ve openRecords 'new-tab' (varsayılan) yeni sekmede, 'same-tab' aynı sekmede açar. same-tab’de sohbet sekme içinde saklanır (persistConversation, sessionStorage) ve geri dönünce kaldığı yerden sürer.",
1968
+ 'Varsayılan olarak beceri çipi, kaynak çipleri, araç durum satırları ve alt bilgi (model adı, “yetkin kadar görür”) KAPALIDIR; showSkillChip / showSources / showToolStatus / showFooter ile açılır.',
1969
+ 'Onay kartı (yazma önerisi) önceki panelle aynıdır: Onayla kullanıcının kendi oturumuyla /m/{app}/{slug} çağırır, Alanları düzenle sonrası “Yeniden öner” olur, Vazgeç kartı kapatır, 30 dk sonra kart solar.',
1970
+ 'Çocuk düğümler panel başlığının sağına yerleşir (isDroppable): tasarımcı kendi düğmelerini oraya koyar. inline modda başlık çizilmediği için çocuklar görünmez.',
1971
+ 'Bütün metinler bileşenin kelimeleridir (compProp.assistant.<kelime>; title, placeholder, empty, send, openRecord, moreRecords, approve, reject …): düğümün texts ayarıyla ya da uygulama sözlüğüyle değişir, prop değildir.'
1972
+ ],
1973
+ propNotes: {
1974
+ mode: "'launcher' (varsayılan) | 'panel' | 'inline'. placement, width, dock, hotkey ve openOnLoad yalnız launcher’da okunur (panelde de o koşulla görünür).",
1975
+ width: 'Çekmece genişliği px (280–900, varsayılan 400); kabuğun daralma payı da budur. 768px altında çekmece tam ekrandır.',
1976
+ dock: "'auto' (varsayılan) | 'push' | 'overlay' — açık çekmecenin yeri (frontend ADR 0034). push sayfayı width kadar iter; overlay sayfanın üstünde gölgeli açılır, sayfa genişliği değişmez; auto pencere − width < 1024 px ise overlay, değilse push (12 sütunlu ızgara itilince harf genişliğine iner). Yalnız launcher'da; mobil ikizde yok.",
1977
+ hotkey: "'Ctrl+J' biçimi: Ctrl/Cmd, Shift, Alt ile tek harf; boş bırakılırsa kısayol yok.",
1978
+ openRecords: "'new-tab' (varsayılan) | 'same-tab' — kayıt listesindeki Aç bağlantısının hedefi.",
1979
+ pageContext: "Sayfa bağlamı (frontend ADR 0032, ASI-019): ekranda seçili değerlere BAĞLANAN nesne, ör. set_binding { prop: 'pageContext', expression: '{{ ({ company_id: state.app.selectedCompanyId }) }}' }. Ad bilerek 'context' değil: renderer her bileşene kendi context'ini ({ children }) verir. Her soruda o anki değer isteğe gider; yalnız metin/sayı/mantıksal değerler, anahtar ^[a-z][a-z0-9_]{0,39}$, en çok 10 anahtar, değer 200 karakterde kesilir — geçmeyen anahtar sessizce düşer. Model bunu araç parametresi için kullanır (firmayı yeniden sormaz); yetki vermez.",
1980
+ persistConversation: 'Açıkken (varsayılan) sohbet sekme içinde saklanır (sessionStorage, düğüm başına anahtar); kapalıyken her sayfa yükü boş başlar.',
1981
+ accent: 'Vurgu rengi (başlatıcı, gönder düğmesi, kullanıcı balonu, Aç bağlantısı); boşsa --ng-accent-primary.',
1982
+ bubbleUser: 'Kullanıcı balonunun zemini; boşsa accent.',
1983
+ bubbleAssistant: 'Asistan balonunun zemini; boşsa tema yüzeyi.',
1984
+ radius: 'Balon, giriş kutusu ve kayıt satırı köşe yarıçapı (tema kademesi ya da CSS uzunluğu); boşsa bileşenin 8–12px tabanı.',
1985
+ density: "'comfortable' (varsayılan) | 'compact' — mesajlar arası boşluk.",
1986
+ surfaceStyle: NODE_SURFACE_NOTE,
1987
+ typographyStyle: STYLE_GROUP_NOTES.typographyStyle + ' ' + STYLE_GROUP_NOTES.colorTokens,
1988
+ style: "Köke uygulanır. Parçalar: '&__launcher', '&__panel', '&__header', '&__log', '&__message', '&__records', '&__composer', '&__send', '&__footer'.",
1989
+ isHidden: 'Ortak katman görünürlüğü: true iken bileşen DOM’a girmez.'
1990
+ }
1991
+ },
1961
1992
  emptystate: {
1962
1993
  summary: 'Boş ekran/liste bloğu: görsel ya da ikon + başlık + açıklama + aksiyon düğmeleri. Olayları mount ve actionClick.',
1963
1994
  behavior: [
@@ -5,7 +5,7 @@ export const componentEventContracts = {
5
5
  mount: { payload: {}, reads: 'self.filters · self.sortField · self.sortType · self.offset · self.limit' },
6
6
  save: {
7
7
  payload: {},
8
- reads: "self.changeset = { inserted:[{ rowId, values }], updated:[{ rowId, id, patch }], deleted:[{ rowId, id }] } (komponent olaydan ÖNCE yazar). Bağlı tabloda (rows connectData) yazmayı runtime bağlantısı yapar, akış ardından koşar; hatalar self.rowErrors [{ rowId, field?, message }]. İşlemin bağlamı da olaydan ÖNCE yazılır (2026-09-21): self.operation (rowAdd|rowEdit|cellEdit|rowDelete|bulkEdit|bulkDelete|batchSave), self.editedRows (tam satırlar), self.deletedRows (silinen tam satırlar), self.editedCell ({ rowId, field, value, previousValue }, yalnız cellEdit), self.bulkPatch (yalnız bulkEdit). Her işlem için ayrı olay yoktur; tek save akışı self.operation ile ayırır"
8
+ reads: "self.changeset = { inserted:[{ rowId, values }], updated:[{ rowId, id, patch }], deleted:[{ rowId, id }] } (komponent olaydan ÖNCE yazar). Bağlı tabloda (rows connectData) yazmayı runtime bağlantısı yapar, akış ardından koşar; hatalar self.rowErrors [{ rowId, field?, message }]. İşlemin bağlamı da olaydan ÖNCE yazılır (2026-09-21): self.operation (rowAdd|rowEdit|cellEdit|rowDelete|bulkEdit|bulkDelete|batchSave|rowReorder), self.editedRows (tam satırlar), self.deletedRows (silinen tam satırlar), self.editedCell ({ rowId, field, value, previousValue }, yalnız cellEdit), self.bulkPatch (yalnız bulkEdit). Her işlem için ayrı olay yoktur; tek save akışı self.operation ile ayırır"
9
9
  },
10
10
  discard: { payload: {}, reads: 'self.discardedRowIds (vazgeçilen taslak satır kimlikleri)' },
11
11
  rowClick: { payload: {}, reads: 'self.selectedRow (tıklanan satırın sunucu satırı; kaydedilmemiş yeni satırda taslak değerleri). Veri hücresine tıklamada yayılır — onay kutusu, ✏/🗑 ve aksiyon düğmeleri tetiklemez; çift tık önce iki rowClick üretir' },
@@ -475,6 +475,9 @@ export const componentEventContracts = {
475
475
  retry: NO_DATA
476
476
  },
477
477
  checkboxgroup: { mount: NO_DATA, change: valueChange('string[] (seçili değerler)'), focus: NO_DATA, blur: NO_DATA },
478
+ assistant: {
479
+ mount: NO_DATA
480
+ },
478
481
  emptystate: {
479
482
  mount: NO_DATA,
480
483
  actionClick: { payload: { text: 'string (aksiyonun metni)', index: 'number' }, reads: "event.data.text / event.data.index (2026-10-01, QA ES-005; self.text / self.index de yazılır). Aksiyonun kendi flow'u varsa tıklamada YALNIZ o akış koşar, bu olaya bağlı akış kendi flow'u olmayan aksiyonlarda koşar; index GÖRÜNEN aksiyonların sırasıdır (isHidden olanlar elenmiş). href dolu satırda olay YİNE yayılır ama aynı sekmede gezinme sayfayı değiştirir ve olayın sayfa-içi sonucu görünmeden kaybolur — ikisini birlikte görmek için satırda isNewTabLink aç" }
@@ -350,8 +350,9 @@ export const mobileComponentKnowledge = {
350
350
  'GRUPLAMA bu bileşende YOKTUR: başlıklar altında liste (türe/zamana göre görevler) `grouped-list`tir — sayaç başlık şablonunda `Section.count`, toplu eylem başlıktaki bir `button` (akışı `Section.items` okur).',
351
351
  'Düşenler: `cardFill` (web ızgarasında kartı en uzun kartın boyuna uzatır; mobil satır içeriğinin boyundadır).',
352
352
  'Çoklu seçim (MOBİLE ÖZGÜ): `selectionMode: multiple` satır başına onay kutusu çizer, dokunuş seçimi değiştirir ve `itemPress`/`change`/`select` YAYILMAZ; `selectionChange { selectedIndices, count }` yayılır. Seçim düğüme yazılır: `{{ <liste>.selectedItems.length }} seçildi`, `<liste>.selectedIndices`. `longPressSelects: true` uzun basışla kipi açar (basılan satır seçili). Kipi kapatmak: setter ile `selectionMode: \'single\'` + `selectedIndices: []` + `selectedItems: []`. Seçim başlığı ayrı bir `app-bar`dır, iki çubuk `isHidden` bağıyla yer değiştirir.',
353
- '`swipeActions` (eylemin `icon`u etiketin üstünde glif olarak çizilir) ve `itemPressable` MOBİLE ÖZGÜDÜR (webde yok). Jest RN `Animated` + `PanResponder` ile kurulur — paket v1`de `reanimated`/`gesture-handler` yok — ve yalnız YATAY harekette yakalanır, böylece dikey kaydırma kaba kalır. Tasarım kipinde satırlar şablon olarak çizildiği için jest hiç bağlanmaz.',
353
+ '`swipeActions` (eylemin `icon`u etiketin üstünde glif olarak çizilir) ve `itemPressable` MOBİLE ÖZGÜDÜR (webde yok). Jest RN `Animated` + `PanResponder` ile kurulur (`gesture-handler` kullanılmaz) ve yalnız YATAY harekette yakalanır, böylece dikey kaydırma kaba kalır. Tasarım kipinde satırlar şablon olarak çizildiği için jest hiç bağlanmaz.',
354
354
  '`swipe` ve `itemPress` yükleri satırı `row` alanında TAŞIR; akış ifadesindeki `Row`/`RowIndex` bağı ayrı bir konudur (ARC-121).',
355
+ 'SÜRÜKLE-SIRALA (MOBİLE ÖZGÜ, 2026-10-27): `reorderable: true` satır sonuna bir tutamaç (⠿) çizer; tutamaçtan sürüklenen satırı kardeşler kayarak yer açar, bırakınca `reorder { fromIndex, toIndex, row, patches }` yayılır. YALNIZ dikey ve tek sütunlu listede (yatay şerit ya da `columnCount` > 1 iken tutamaç çizilmez — MOB-035). `orderField` verilirse satırlar o sayı alanına göre çizilir (boşlar sonda) ve `patches` o alana yazılacak 1 tabanlı yeni sıraları taşır. Liste bırakılan sırayı FARKLI satırlar gelene kadar tutar: aynı satırlar yeniden okunursa (yenileme, aynı yanıt) sıra bozulmaz; kalıcılık akışın işidir ve servis hatasında liste eski sıraya kendiliğinden DÖNMEZ (`platform_guide(\'page\')` §Kural 1g-5). Kenar bandında sayfa kendiliğinden kayar; modal/alt sayfa içinde kaymaz. Tasarım kipinde tutamaç çizilir ama sürüklenmez. Tutamaç gövdeden ayrıdır: gövdedeki dokunuş `itemPress`, yatay kaydırma `swipe` olarak kalır. Erişilebilirlik: tutamaçta "yukarı taşı / aşağı taşı" eylemleri. Tutamacın rol adresi `<elementId>__row-<i>__handle` — yalnız çalışma anında; tasarım ve Düzenle kipinde kimlik basılmaz.',
355
356
  'Düşenler: `autoRowSize` (mobil satır her zaman içeriğinin boyundadır), `size` (mobil kart hücresi çocuklarını büyütmez; 2026-09-30), `cardFill` (kart hücre boyuna gerilmez, içeriğinin boyundadır; 2026-10-14).'
356
357
  ],
357
358
  propNotes: {
@@ -362,7 +363,9 @@ export const mobileComponentKnowledge = {
362
363
  snap: '`none` · `item` — kaydırma kart + aralık adımına oturur.',
363
364
  views: "Sabit iki görünüm: 'item' (satır şablonu) ve 'empty' (boş görünüm). Değer YAZILMAZ; görünümler children'daki iki view düğümüdür, başka şekil 422 `fixed-views`.",
364
365
  selectionMode: '`single` (varsayılan, bugünkü `selectedIndex`) · `multiple` (onay kutulu çoklu seçim, `selectionChange`) · `none` (seçim izlenmez).',
365
- longPressSelects: 'Uzun basış çoklu seçim kipini açar ve basılan satırı seçer.'
366
+ longPressSelects: 'Uzun basış çoklu seçim kipini açar ve basılan satırı seçer.',
367
+ reorderable: 'Tutamaçtan sürükleyerek sıralama (yalnız dikey, tek sütun); bırakınca `reorder` yayılır.',
368
+ orderField: "Sırayı tutan sayı alanının adı (ör. 'sira'); satırlar buna göre çizilir, `reorder.patches[].value` buraya yazılacak sıradır. Boşsa `patches` boş gelir."
366
369
  }
367
370
  },
368
371
  modal: {
@@ -609,6 +612,21 @@ export const mobileComponentKnowledge = {
609
612
  peek: 'Komşu sayfaların kenardan görünen payı (dp); en çok genişliğin dörtte biri, sayfalar araya küçük boşluk alır. 0 (varsayılan) tam genişlik sayfa.'
610
613
  }
611
614
  },
615
+ assistant: {
616
+ summary: "Uygulama asistanı (web ikizi, frontend ADR 0033): sohbet sözleşmesi, durum makinesi ve kelimeler web paketiyle ortak; mobil uygulamada `application` ile web ikizinin yayındaki asistanına bağlanır.",
617
+ differences: [
618
+ "`application` mobile özgü: cevaplayan asistanın uygulaması. Mobil uygulamaların kendi ucu yoktur; web ikizinin slug'ı yazılır (`ik-mobil` → `ik`), boşsa sayfanın uygulaması. İstekler sayfanın kendi başlıklarıyla gider (uygulama jetonu `X-Flow-Cookie`, bearer, ortam şeridi).",
619
+ '`launcher` (varsayılan): `placement` köşesinde yüzen düğme (ekran katmanı, `fab` ile aynı yerleşim) tam ekran sohbet katmanını açar; `panel` ve `inline` sohbeti hücresine gömer. Kanvasta yalnız kabuk çizilir, düğme basılamaz.',
620
+ "Kayıt bloğu satırları ve menü yolu metin olarak görünür; 'Aç' ve 'Sayfaya git' düğmesi yoktur (backend web rotası üretir). Sohbet uygulama oturumunda bellekte kalır. Akışlı gövde vermeyen runtime'da cevap sonda bir kerede görünür.",
621
+ 'Düşenler: `width`, `dock`, `hotkey`, `openRecords`, `persistConversation`, `showFooter`, `bubbleUser`, `bubbleAssistant`, `density`, `surfaceStyle`, `typographyStyle`.',
622
+ WEB_WORDS_NOTE
623
+ ],
624
+ propNotes: {
625
+ application: "Web ikizinin uygulama slug'ı (`ik`, `jira`); asistan ayarı, yetki, yayın ve senaryolar o uygulamanındır.",
626
+ pageContext: "Web'deki gibi: ekranda seçili değerlere bağlanır (`{{ ({ company_id: … }) }}`), her soruyla gider.",
627
+ placement: "`bottom-right` (varsayılan) · `bottom-left`. Uygulamanın bir sayfasında aynı köşede `fab` varsa öbür köşeyi seçin: ikisi aynı ekran katmanında üst üste biner ve fab'ın bir kısmına dokunmak asistanı açar (ölçüldü 2026-10-24, jira-mobil). Köşe uygulamanın bütün sayfalarında aynı kalmalı."
628
+ }
629
+ },
612
630
  fab: {
613
631
  summary: 'Yüzen eylem düğmesi (FlutterFlow FloatingActionButton): ekranın alt köşesinde durur, sayfa kaydırılırken yerinde kalır. Web`de karşılığı YOKTUR.',
614
632
  differences: [
@@ -982,7 +1000,7 @@ export const mobileComponentKnowledge = {
982
1000
  '`variant: table` (varsayılan) yatay kaydırılan tablodur: başlık satırı, sıralanabilir kolonda başlığa dokunuş (`<elementId>__sort-<kolon>`, yön oku başlıkta), eylem sütunu satırın sonunda; dikey kaydırma sayfanındır, tablonun üstünden başlayan dikey kaydırma sayfayı kaydırır; kolonların toplamı çerçeveden darsa kolonlar web gibi çerçeveye yayılır (`table-layout: fixed`). `variant: cards` satır başına kart çizer: ilk görünür görsel kolon kartın başına, ilk görsel olmayan kolon başlığa, kalanlar etiket-değer satırlarına; kart kipinde sıralama başlığı yoktur (sıralama akışla yazılır).',
983
1001
  'Eylemler web paletiyle (`solid` birincil, `information` kenarlı, `transparent` hayalet, `error`/`danger` yıkıcı, `warning`, `dark`): `actionType: button` satırda düğmeler (`<elementId>__action-<satır>-<sıra>`), `dropdown` ⋯ düğmesi (`<elementId>__more-<satır>`) alttan açılan eylem sayfası (`<elementId>__sheet-action-<sıra>`; cihazda `modal` ile aynı katman). Satırın `_actions` dizisi web gibi eylemi ezer, `isHidden` gizler, `isDisabled` kilitler.',
984
1002
  'Sayfalama (`isPaginated`): ‹ `1–10 / 57` › çubuğu (`<elementId>__page-prev`, `__page-next`); metin dil bağımsızdır. Web`in sayfa numarası kutusu ve sayfa başına satır seçimi yok (`limit` panelden ya da akıştan; düğümde hiç yazılmamışsa web gibi 10). Başlık doluyken yanındaki yenile düğmesi (`<elementId>__refresh`) `mount`u koşturur; `showRefresh: false` düğmeyi kaldırır.',
985
- 'Düşenler: `style`, `actionsPinned`, `advancedFilter`, `filterPosition`, `showFilterSummary`, `showExport`, `showColumnManager`, `toolsPosition`, `footerPosition`, `stickyHeader`, `stickyFooter`, `paginationPosition`, `addRowShortcut`, `canReorderRows`, `orderField`, `revealEntity`, `rowDoubleClick`, `cellSelectionChange`; kolonların `relationTypeField` ve `relationTargets` alanı. Gelişmiş filtre oluşturucu, dışa aktarma, kolon menüsü, sürükle-sırala ve korumalı alanı gösterme telefonda yok; uzun veri için yol sayfalama ya da sonsuz kaydırmadır (satır sanallaştırma yok).',
1003
+ 'Düşenler: `style`, `actionsPinned`, `advancedFilter`, `filterPosition`, `showFilterSummary`, `showExport`, `showColumnManager`, `toolsPosition`, `footerPosition`, `stickyHeader`, `stickyFooter`, `paginationPosition`, `addRowShortcut`, `canReorderRows`, `orderField`, `revealEntity`, `rowDoubleClick`, `cellSelectionChange`; kolonların `relationTypeField` ve `relationTargets` alanı. Gelişmiş filtre oluşturucu, dışa aktarma, kolon menüsü, sürükle-sırala ve korumalı alanı gösterme telefonda yok (sıralanabilir liste: `repeater` `reorderable`); uzun veri için yol sayfalama ya da sonsuz kaydırmadır (satır sanallaştırma yok).',
986
1004
  'Telefonda filtre (plan mobil-tasarimci-kabuk-ekranlari 34): `isFilterable` kolonlar (bağlı tabloda sunucunun kabiliyeti son sözü söyler) üstte çip şerididir (`<elementId>__chip-<kolon>`, başta rozetli `__filters`: etkin çip sayısı, ✕ hepsini temizler). Çipe dokunmak alt sayfa açar: metin kolonunda işleç (sunucunun açtıkları) + değer, seçenekli kolonda liste (`in` açıksa çoklu), anahtarda Evet/Hayır, ilişki kolonunda seçim bileşenlerinin lookup alt sayfası. Uygula `filters`a kök `and` tekil kural yazar (web filtre çubuğuyla aynı sözleşme) ve `offset: 0` ile `mount` koşturur; etkin çip mavi. Bağlı tabloda çipler bağlamanın `behavior.mobile.filterChips`ıyla açılır; `filterMode: none` her durumda kapatır.',
987
1005
  'Telefonda düzenleme (34): bağlantının yazdığı `editMode`/`canEditRow`/`canAddRow`/`canDeleteRow`/`bulkActions` okunur. Satıra dokunmak `rowClick`i yayar (`selectedRow` sessiz yazılır) ve düzenlenebilir kolon varsa "Satırı düzenle" alt sayfasını açar (alan türüne göre denetim: metin/sayı/tarih girişi, anahtar, seçenek çipleri, ilişkide lookup). `canAddRow` + `addRowButton` ≠ `none` "Yeni satır" düğmesi (`__add-row`). `bulkActions` açıksa uzun basış seçim kipidir (`rowSelectionChange`, `selectedRows`): çubukta Toplu düzenle (tek alan, bütün seçililere) ve Sil. Bağlamanın `behavior.mobile.swipeDelete`ı kartı kaydırınca Sil (onaylı) açar. Kayıt `save` olayıyla gider (changeset + `operation`, web f-22 ile aynı); `row`/`cell` kipi anında, `batch` alt çubuktaki Kaydet ile (`discard` Vazgeç). Kısmi başarıda hatalı satır kırmızı çerçeve + satır hatası, özet şeridi "2 satır yazıldı, 1 düşmedi" (`__save-outcome`); düşen satırlar seçili kalır.',
988
1006
  'Kart alanları ve sayfa (34): `variant: cards` iken kart bağlamanın `behavior.mobileCard`ından çizilir (başlık, sıralı etiket-değer alanları, sağ değer); eşleme yoksa ilk görsel/ilk metin kuralı. `paginationMode: infinite` ya da bağlamanın `behavior.mobile.infiniteScroll`ı (yazılmamışsa açık) sayfaları birleştirir: sayfa kaydırıcısı sona yaklaşınca sonraki `offset` kendiliğinden istenir, yeni sayfa sona eklenir, altta "Yükleniyor…"; sayfa çubuğu çizilmez (sayfa kaydırıcısı olmayan kapta "Daha fazla" `__load-more`). `behavior.mobile.pullToRefresh` sayfayı aşağı çekince listeyi yeniler (web önizlemede çekme yok: başlıkta yenile düğmesi). `showRefresh: false` yenile düğmesini kaldırır. `showRecordSummary: false` kayıt aralığı metnini kaldırır. Bunların ve yukarıdaki çip/düzenleme davranışlarının hepsi yalnız `variant: cards` kipindedir: `variant: table` salt okunur ızgaradır (dokunuş, düzenleme, seçim, çip yok).',
@@ -295,6 +295,15 @@ export const mobileComponentEventContracts = {
295
295
  count: 'number — seçili satır sayısı'
296
296
  },
297
297
  reads: 'yalnız selectionMode: multiple (ya da longPressSelects ile açılınca); akış event.data.selectedIndices / event.data.count okur, düğümün selectedIndices/selectedItems değeri de yazılır'
298
+ },
299
+ reorder: {
300
+ payload: {
301
+ fromIndex: 'number — satırın bırakılmadan önceki sırası (ekrandaki, 0 tabanlı)',
302
+ toIndex: 'number — bırakıldığı sıra',
303
+ row: 'object — taşınan satırın verisi',
304
+ patches: "Array<{ id, value }> — sırası değişen satırlar: id satırın _id'si (yoksa id'si), value orderField'a yazılacak yeni 1 tabanlı adım; eş değerli satırlar tek adımdır (aynı value), iki eş satırın arasına bırakılan satır o adıma katılır. Yalnız değeri değişenler gelir"
305
+ },
306
+ reads: "self'e yazılmaz — event.data.patches ile oku. Yalnız reorderable açık, dikey ve tek sütunlu listede; satır kendi yerine bırakılırsa olay yayılmaz. orderField yoksa ya da bir satırda _id/id yoksa patches BOŞ dizidir (sıra yalnız ekranda tutulur). Seçim varsa düğümün selectedIndices/selectedItems/selectedIndex değeri yeni sıraya taşınarak yazılır"
298
307
  }
299
308
  }
300
309
  };
@@ -82,6 +82,20 @@ export const SCREENSHOT_CAPTURE_DEVICES = {
82
82
  tablet7: { width: 600, height: 960, scale: 2 },
83
83
  tablet10: { width: 800, height: 1280, scale: 2 }
84
84
  };
85
+ /**
86
+ * Pazarlama karesinin cihaz çerçevesi (`mobile_asset_preview`, ADR 0004). Tuval mağaza ölçüsüdür
87
+ * (`SCREENSHOT_CAPTURE_DEVICES` × scale), çerçeve CSS ile çizilir: Apple'ın cihaz görselleri yeniden
88
+ * dağıtılamaz. Cihaz sınıfları arasındaki fark yalnız bu satırlardır, kodda dallanma yoktur.
89
+ */
90
+ export const STORE_FRAME_LAYOUTS = {
91
+ // Gerçek ekran köşesi ~0.125'tir; runtime karesinde alt güvenli alan olmadığı için o köşe sekme çubuğu
92
+ // etiketlerini kırpıyordu (2026-10-26, mobiltest `ana`) — köşe biraz daraltıldı.
93
+ iphone69: { deviceWidth: 0.78, bezel: 0.03, radius: 0.12, cutout: 'island' },
94
+ ipad13: { deviceWidth: 0.8, bezel: 0.03, radius: 0.05, cutout: 'none' },
95
+ phone: { deviceWidth: 0.76, bezel: 0.028, radius: 0.11, cutout: 'punch' },
96
+ tablet7: { deviceWidth: 0.8, bezel: 0.035, radius: 0.055, cutout: 'none' },
97
+ tablet10: { deviceWidth: 0.8, bezel: 0.03, radius: 0.05, cutout: 'none' }
98
+ };
85
99
  /** Beyan anahtarları (`MobileStoreSettings.*Declaration`); iOS appPrivacy, ötekiler Android */
86
100
  export const DECLARATION_KEYS = ['appPrivacy', 'dataSafety', 'contentRating', 'targetAudience', 'appAccess'];
87
101
  /**
@@ -19,7 +19,7 @@ export const MOTION_PRESS = ['none', 'scale', 'opacity'];
19
19
  * taşınan), kabukları çalışma anında boş kutudur — frontend `catalog-schema.ts` `DRAWN_OUTSIDE_SHELL`. Katalogla
20
20
  * kilitli (`test/motion-resolve.test.ts`).
21
21
  */
22
- export const MOTION_OUTSIDE_SHELL = ['drawer', 'fab', 'floating-container', 'modal', 'photo-viewer', 'toast'];
22
+ export const MOTION_OUTSIDE_SHELL = ['assistant', 'drawer', 'fab', 'floating-container', 'modal', 'photo-viewer', 'toast'];
23
23
  /** ms cinsinden kabul aralıkları. */
24
24
  export const MOTION_RANGES = {
25
25
  duration: [100, 2000],
@@ -67,6 +67,8 @@ const TEMPLATE_VALUE = /\{\{[\s\S]*\}\}/;
67
67
  * düşer (koşu 37 K37-c, `var(--ng-surface)`). Web profili bu kuralı koşmaz.
68
68
  */
69
69
  const STYLE_GROUP_PROPS = new Set(['surfaceStyle', 'typographyStyle', 'headerStyle']);
70
+ // MOB-035'in kararını veren liste prop'ları: birine tek prop yazımı kayıtlı ağacı okutur.
71
+ const REORDER_PROPS = new Set(['reorderable', 'direction', 'columnCount']);
70
72
  const UNRESOLVED_TOKEN = /var\(\s*(--ng-[\w-]+)\s*\)/g;
71
73
  let themeVarNames;
72
74
  function nearestThemeVar(name) {
@@ -115,6 +117,8 @@ export function profileFindings(profile, input) {
115
117
  const namelessIconButtons = [];
116
118
  const unknownTokens = [];
117
119
  const motionSkips = [];
120
+ const flatReorder = [];
121
+ const repeaterProps = new Map();
118
122
  const hit = (map, key, target) => map.set(key, [...(map.get(key) ?? []), target]);
119
123
  const inspectTokens = (target, value) => {
120
124
  for (const name of unknownThemeTokens(value)) {
@@ -205,6 +209,8 @@ export function profileFindings(profile, input) {
205
209
  inspectMotion(target, props.motionStyle);
206
210
  for (const group of STYLE_GROUP_PROPS)
207
211
  inspectTokens(target, props[group]);
212
+ if (type === 'repeater')
213
+ repeaterProps.set(target, props);
208
214
  if (type === 'image' && !imageHasDefiniteHeight(asRecord(node.appearance)))
209
215
  floorImages.push(target);
210
216
  if (type === 'button' && propValue(props.size) === 'icon' && !String(propValue(props.ariaLabel) ?? '').trim() && !String(propValue(props.text) ?? '').trim()) {
@@ -230,7 +236,12 @@ export function profileFindings(profile, input) {
230
236
  };
231
237
  walk(input.nodes, inspectNode);
232
238
  const savedTypes = new Map();
233
- walk(input.savedNodes, node => savedTypes.set(label(node), String(node.type ?? '')));
239
+ const savedRepeaterProps = new Map();
240
+ walk(input.savedNodes, node => {
241
+ savedTypes.set(label(node), String(node.type ?? ''));
242
+ if (node.type === 'repeater')
243
+ savedRepeaterProps.set(label(node), asRecord(node.props) ?? {});
244
+ });
234
245
  // Toplu yamanın zarfı açılır: içindeki op'lar tek tek yazılmış gibi denetlenir (TOOL-057).
235
246
  const ops = flattenBatchOps(input.ops ?? []);
236
247
  for (const op of ops) {
@@ -258,6 +269,9 @@ export function profileFindings(profile, input) {
258
269
  inspectMotion(target, op.value);
259
270
  if (STYLE_GROUP_PROPS.has(prop))
260
271
  inspectTokens(target, op.value);
272
+ const listProps = REORDER_PROPS.has(prop) ? repeaterProps.get(target) ?? savedRepeaterProps.get(target) : undefined;
273
+ if (listProps)
274
+ repeaterProps.set(target, { ...listProps, [prop]: op.value });
261
275
  // 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.
262
276
  const knownType = nodeTypes.get(target);
263
277
  const imageProp = knownType === undefined ? IMAGE_SOURCE_PROP_NAMES.has(prop) : (IMAGE_SOURCE_PROPS[knownType] ?? []).includes(prop);
@@ -266,6 +280,15 @@ export function profileFindings(profile, input) {
266
280
  fileIdSources.push({ target: `${target}.${prop}`, expression });
267
281
  }
268
282
  }
283
+ // MOB-035: sıralama yalnız dikey, tek sütunlu listededir (frontend `reorderEnabled`); yatay şeritte ve ızgarada
284
+ // `reorderable` yazım kabul edilir ama tutamaç çizilmez — sessiz başarı. Denetlenen, yazımdan SONRAKİ prop'lardır:
285
+ // eklenen düğümün kendi prop'ları, var olan düğüme tek prop yazımında kayıtlı prop'larla birleşimi. Dokunulmayan
286
+ // kayıtlı liste denetlenmez. Sütun sayısı çizerinki gibi tam sayıya kesilir (`repeaterColumnCount`: 1.5 tek sütundur).
287
+ for (const [target, props] of repeaterProps) {
288
+ const columns = Math.trunc(Number(propValue(props.columnCount)));
289
+ if (propValue(props.reorderable) === true && (propValue(props.direction) === 'horizontal' || (Number.isFinite(columns) && columns > 1)))
290
+ flatReorder.push(target);
291
+ }
269
292
  if (blockNodes.length > 0 || gridNodes.length > 0) {
270
293
  findings.push({
271
294
  code: MOBILE_RULES['MOB-001'].id,
@@ -302,6 +325,15 @@ export function profileFindings(profile, input) {
302
325
  guide: "platform_guide('page') §Kural 1g-3"
303
326
  });
304
327
  }
328
+ if (flatReorder.length > 0) {
329
+ findings.push({
330
+ code: MOBILE_RULES['MOB-035'].id,
331
+ severity: 'warn',
332
+ target: flatReorder.join(', '),
333
+ message: `reorderable yalnız dikey, tek sütunlu listede çalışır — yatay şeritte (direction: horizontal) ve çok sütunda (columnCount > 1) tutamaç çizilmez, satırlar sürüklenmez. Sıralama gerekiyorsa listeyi dikey ve tek sütun yapın. ${NOT_BLOCKING}`,
334
+ guide: "platform_guide('page') §Kural 1g-5"
335
+ });
336
+ }
305
337
  if (motionSkips.length > 0) {
306
338
  findings.push({
307
339
  code: MOBILE_RULES['MOB-028'].id,
@@ -483,12 +515,13 @@ function walkSteps(value, visit) {
483
515
  }
484
516
  /**
485
517
  * Denetim bu yama için kayıtlı ağacı ister mi (`ProfileLintInput.savedNodes`): `style` yazımında hedefin tipi (belge
486
- * 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
518
+ * CSS'i taşıyan düğüm mü — MOB-002), `heroTag` yazımında sayfanın öteki etiketleri (MOB-030), sıralama prop'u yazımında
519
+ * listenin öteki prop'ları (MOB-035). Çağıran ağacı yalnız bu
487
520
  * durumda okur. Denetimle AYNI op listesine bakar (zarf açılır): 2026-10-10'da denetim zarfı açıp çağıran açmayınca
488
521
  * kayıtlı `html` düğümünün belge CSS'i zarf içinde yanlış MOB-002 aldı (regresyon `test/batch-profile-lint.test.ts`).
489
522
  */
490
523
  export function profileLintReadsSavedNodes(ops) {
491
- return writesHeroTag(ops) || flattenBatchOps(ops).some(op => (op.op === 'replace_prop' || op.op === 'set_binding') && op.prop === 'style');
524
+ return writesHeroTag(ops) || flattenBatchOps(ops).some(op => (op.op === 'replace_prop' || op.op === 'set_binding') && (op.prop === 'style' || REORDER_PROPS.has(String(op.prop))));
492
525
  }
493
526
  /** Bulguları `layoutWarnings[]` satırlarına çevirir. */
494
527
  export function findingLines(findings) {
@@ -133,6 +133,12 @@ export const MOBILE_RULES = {
133
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
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"
135
135
  },
136
+ 'MOB-035': {
137
+ id: 'MOB-035',
138
+ severity: 'warn',
139
+ sourceRef: 'zeyrek-frontend/packages/ng-apps-ui-native/src/components/repeater/repeater-reorder-logic.ts (reorderEnabled; plans/mobil-jest-katmani faz 02)',
140
+ summary: 'Sıralanabilir liste (`reorderable`) yatay şeritte ya da çok sütunda — çizer tutamaç çizmez, sürükleme yok'
141
+ },
136
142
  'MOB-033': {
137
143
  id: 'MOB-033',
138
144
  severity: 'warn',
@@ -550,6 +550,13 @@ export const TYPED_STYLE_COMPONENTS = {
550
550
  ext: { overflowTarget: ROOT_TARGET },
551
551
  omit: { surfaceStyle: TRANSFORM_FIELDS }
552
552
  },
553
+ assistant: {
554
+ // Çekmece / panel kökü: yüzey ve yazı grubu panelin köküne iner (frontend
555
+ // `Assistant.svelte`, composeTypedStyle ile aynı derleyici).
556
+ groups: ['surfaceStyle', 'typographyStyle'],
557
+ ext: { overflowTarget: ROOT_TARGET },
558
+ omit: { surfaceStyle: TRANSFORM_FIELDS }
559
+ },
553
560
  drawer: {
554
561
  // Panel portal ile düğüm kutusunun DIŞINA çizilir; yüzey panelin KÖKÜNDE
555
562
  // derlenir, taşma da panelin köküne iner (drawer ölçümü, 2026-10-05).
@@ -1,5 +1,17 @@
1
1
  import { INTERNAL_ROLE_SEPARATOR } from '../../knowledge/componentCatalog.js';
2
2
  import { studioWebTarget } from './targets/studio-web.js';
3
+ /**
4
+ * `swipe` jestinin adım sayısı ve aralığı: react-native-web yanıtlayıcısı tek seferde atılan hareketi jest saymaz
5
+ * (`ui-native.md` §flip-card ve swipeable-stack); 12 × 16 ms oyun alanında tutamaç sıralamasını sürdü (g-02, 2026-10-27).
6
+ */
7
+ export const SWIPE_STEPS = 12;
8
+ export const SWIPE_INTERVAL_MS = 16;
9
+ /**
10
+ * `to` verilen jest hedefin içinde, hareket yönündeki kenarına bu kadar (CSS pikseli) kala biter. Merkezde bitseydi
11
+ * sürüklenen satırın merkezi hedefinkine EŞİT olurdu; çizer kardeşi kesin `<` ile saydığından (`reorderTarget`) aşağı
12
+ * taşıma bir satır eksik inerdi (inceleme bulgusu 2, oyun alanında `row-0 → row-2` → `reorder 0→1`).
13
+ */
14
+ export const SWIPE_EDGE_INSET = 2;
3
15
  /**
4
16
  * G-24 — gateway sayfayı kurabiliyor, okuyabiliyor ve çizdirebiliyordu ama üzerinde OLAY
5
17
  * TETİKLEYEMİYORDU: "filtre değişince veri yenileniyor" iddiası yalnız şema okumasıyla
@@ -273,6 +285,61 @@ export function actionScript(action, captureTarget = studioWebTarget) {
273
285
  return { found: true, applied: true, requests: count };
274
286
  })()`;
275
287
  }
288
+ // g-02 (plans/mobil-jest-katmani) — mobil jestler (tutamaçtan sıralama, satır kaydırma, deste, kaydırıcı) işaretçiyi
289
+ // dinler; HTML5 `drag` onları süremez. Gerçek tarayıcı gibi her anda pointer + mouse birlikte atılır: oyun alanında
290
+ // yalnız `pointer*` react-native-web yanıtlayıcısını SÜRMEDİ, `mouse*` sürdü (ölçüldü 2026-10-27). Hareket olayı
291
+ // parmağın altındaki öğeye gider; basış kaynağa.
292
+ if (action.action === 'swipe') {
293
+ const destination = action.to === undefined ? 'null' : JSON.stringify(`[data-element-id="${action.to}"]`);
294
+ const webNote = captureTarget.name === 'studio-mobile'
295
+ ? ''
296
+ : "swipe dokunmatik jesttir: web sayfasında yalnız işaretçiyi dinleyen öğeyi sürer — HTML5 sürükle-bırak (kanban, takvim) için 'drag'";
297
+ return `(function () {
298
+ var count = performance.getEntriesByType('resource').length;
299
+ var node = document.querySelector(${selector});
300
+ if (!node) return { found: false, applied: false, requests: count };
301
+ var center = function (element) { var rect = element.getBoundingClientRect(); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; };
302
+ var from = center(node);
303
+ var to;
304
+ var destination = ${destination};
305
+ if (destination !== null) {
306
+ var goal = document.querySelector(destination);
307
+ if (!goal) return { found: false, applied: false, note: "jest kaynağı bulundu ama bitiş hedefi ('to') sayfada yok: " + ${JSON.stringify(String(action.to ?? ''))} + " — satır <elementId>__row-<i>, tutamaç <elementId>__row-<i>__handle", requests: count };
308
+ var box = goal.getBoundingClientRect();
309
+ var middle = center(goal);
310
+ var edge = function (start, mid, low, high) { return mid > start ? high - ${SWIPE_EDGE_INSET} : mid < start ? low + ${SWIPE_EDGE_INSET} : mid; };
311
+ to = { x: edge(from.x, middle.x, box.left, box.left + box.width), y: edge(from.y, middle.y, box.top, box.top + box.height) };
312
+ } else {
313
+ to = { x: from.x + ${Number(action.dx ?? 0)}, y: from.y + ${Number(action.dy ?? 0)} };
314
+ }
315
+ var fire = function (type, at, element) {
316
+ var init = { bubbles: true, cancelable: true, composed: true, clientX: at.x, clientY: at.y, screenX: at.x, screenY: at.y, button: 0, buttons: /up$/.test(type) ? 0 : 1, pointerId: 1, pointerType: 'mouse', isPrimary: true };
317
+ element.dispatchEvent(type.indexOf('pointer') === 0 ? new PointerEvent(type, init) : new MouseEvent(type, init));
318
+ };
319
+ var under = function (at) { return document.elementFromPoint(at.x, at.y) || node; };
320
+ var finish = function (result) { if (${JSON.stringify(webNote)}) result.note = ${JSON.stringify(webNote)}; return result; };
321
+ return new Promise(function (resolve) {
322
+ fire('pointerdown', from, node);
323
+ fire('mousedown', from, node);
324
+ var step = 0;
325
+ var move = function () {
326
+ step += 1;
327
+ var at = { x: from.x + ((to.x - from.x) * step) / ${SWIPE_STEPS}, y: from.y + ((to.y - from.y) * step) / ${SWIPE_STEPS} };
328
+ var element = under(at);
329
+ fire('pointermove', at, element);
330
+ fire('mousemove', at, element);
331
+ if (step < ${SWIPE_STEPS}) { setTimeout(move, ${SWIPE_INTERVAL_MS}); return; }
332
+ setTimeout(function () {
333
+ var end = under(to);
334
+ fire('pointerup', to, end);
335
+ fire('mouseup', to, end);
336
+ resolve(finish({ found: true, applied: true, requests: count }));
337
+ }, ${SWIPE_INTERVAL_MS});
338
+ };
339
+ setTimeout(move, ${SWIPE_INTERVAL_MS});
340
+ });
341
+ })()`;
342
+ }
276
343
  const target = String(action.target ?? '');
277
344
  const root = rootElementId(target);
278
345
  // G-14 — `applied` yalnız "DOM'a yazıldı" demekti: tarih seçici olayı dinlemiyordu, kutu dolu