@appaflytech/zeyrek-ai-mcp 0.0.1

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 (88) hide show
  1. package/.env.example +46 -0
  2. package/README.md +260 -0
  3. package/dist/config.js +128 -0
  4. package/dist/index.js +88 -0
  5. package/dist/knowledge/componentCatalog.js +438 -0
  6. package/dist/knowledge/componentEvents.js +164 -0
  7. package/dist/knowledge/coverage.js +60 -0
  8. package/dist/knowledge/normalize.js +1309 -0
  9. package/dist/knowledge/outlines.js +167 -0
  10. package/dist/knowledge/policy.js +44 -0
  11. package/dist/knowledge/reservedTables.js +75 -0
  12. package/dist/knowledge/rules.js +1725 -0
  13. package/dist/observability/trace.js +337 -0
  14. package/dist/platform/errors.js +132 -0
  15. package/dist/platform/http.js +169 -0
  16. package/dist/platform/responses.js +94 -0
  17. package/dist/platform/token.js +79 -0
  18. package/dist/resources/examples/entity-pair-m2m.json +105 -0
  19. package/dist/resources/examples/flow-list-to-form.json +65 -0
  20. package/dist/resources/examples/flow-load-table.json +178 -0
  21. package/dist/resources/examples/microservice-table-list.json +49 -0
  22. package/dist/resources/examples/middleware-authenticate.json +194 -0
  23. package/dist/resources/examples/page-detail-profile.json +95 -0
  24. package/dist/resources/examples/page-dynamic-form.json +221 -0
  25. package/dist/resources/examples/page-form-upsert.json +279 -0
  26. package/dist/resources/examples/page-html-embed.json +106 -0
  27. package/dist/resources/examples/page-sign-in.json +194 -0
  28. package/dist/resources/examples/workflow-basic.json +94 -0
  29. package/dist/resources/guides/component.md +108 -0
  30. package/dist/resources/guides/entity.md +348 -0
  31. package/dist/resources/guides/expressions.md +139 -0
  32. package/dist/resources/guides/getting-started.md +109 -0
  33. package/dist/resources/guides/html.md +106 -0
  34. package/dist/resources/guides/microservice.md +313 -0
  35. package/dist/resources/guides/middleware.md +271 -0
  36. package/dist/resources/guides/modify-existing.md +35 -0
  37. package/dist/resources/guides/page.md +1574 -0
  38. package/dist/resources/guides/view.md +89 -0
  39. package/dist/resources/guides/workflow.md +359 -0
  40. package/dist/resources/knowledge.js +44 -0
  41. package/dist/resources/schemas/CreateDatasourceCommand.json +54 -0
  42. package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +133 -0
  43. package/dist/resources/schemas/CreateEntityCommand.json +387 -0
  44. package/dist/resources/schemas/CreateMicroserviceCommand.json +528 -0
  45. package/dist/resources/schemas/CreateQueryCommand.json +43 -0
  46. package/dist/resources/schemas/CreateViewCommand.json +158 -0
  47. package/dist/resources/schemas/CreateWorkflowCommand.json +1719 -0
  48. package/dist/resources/schemas/Filter.json +124 -0
  49. package/dist/resources/schemas/GetRowsQuery.json +122 -0
  50. package/dist/resources/schemas/PreviewMicroserviceCommand.json +565 -0
  51. package/dist/resources/schemas/PreviewQueryCommand.json +74 -0
  52. package/dist/resources/schemas/PreviewWorkflowCommand.json +981 -0
  53. package/dist/resources/schemas/StepDto.json +900 -0
  54. package/dist/resources/schemas/UpdateDatasourceCommand.json +58 -0
  55. package/dist/resources/schemas/UpdateEntityCommand.json +679 -0
  56. package/dist/resources/schemas/UpdateMicroserviceCommand.json +512 -0
  57. package/dist/resources/schemas/UpdateQueryCommand.json +86 -0
  58. package/dist/resources/schemas/UpdateViewCommand.json +153 -0
  59. package/dist/resources/schemas/UpdateWorkflowCommand.json +1719 -0
  60. package/dist/resources/schemas.js +40 -0
  61. package/dist/server.js +63 -0
  62. package/dist/tools/access.js +27 -0
  63. package/dist/tools/applications.js +59 -0
  64. package/dist/tools/automations.js +103 -0
  65. package/dist/tools/common.js +18 -0
  66. package/dist/tools/components.js +280 -0
  67. package/dist/tools/datasources.js +82 -0
  68. package/dist/tools/dependencies.js +17 -0
  69. package/dist/tools/deployments.js +17 -0
  70. package/dist/tools/dtos.js +54 -0
  71. package/dist/tools/entities.js +166 -0
  72. package/dist/tools/files.js +138 -0
  73. package/dist/tools/flows.js +226 -0
  74. package/dist/tools/folders.js +51 -0
  75. package/dist/tools/layouts.js +105 -0
  76. package/dist/tools/microservices.js +198 -0
  77. package/dist/tools/organizations.js +65 -0
  78. package/dist/tools/pages.js +597 -0
  79. package/dist/tools/platformInfo.js +44 -0
  80. package/dist/tools/projects.js +152 -0
  81. package/dist/tools/queries.js +48 -0
  82. package/dist/tools/rows.js +115 -0
  83. package/dist/tools/screens.js +97 -0
  84. package/dist/tools/templates.js +65 -0
  85. package/dist/tools/variables.js +94 -0
  86. package/dist/tools/views.js +58 -0
  87. package/dist/tools/workflows.js +196 -0
  88. package/package.json +49 -0
@@ -0,0 +1,1574 @@
1
+ # Sayfa Tasarımı Rehberi (page, layout, component, flow)
2
+
3
+ Studio'nun tuval tarafı. Buradaki kurallar controller + komut kodundan doğrulandı
4
+ (`zeyrek-mcp/docs/13-faz2-envanter.md` §4); veri/mantık tarafı için
5
+ `platform_guide('entity')`, `('microservice')`, `('workflow')`.
6
+
7
+ ## Kuruluş sırası
8
+
9
+ ```
10
+ 1. layout_list / layout_create (layout isteğe bağlı; sayfalar layoutId ile bağlanır)
11
+ 2. role_list (roleIds'i buradan oku — rol id'si TAHMİN EDİLMEZ)
12
+ 3. component_list / component_get (konulacak tipin prop sözlüğü + boyut sınırları)
13
+ 4. page_create (şemasız aç; şema micro-op hattından kurulur)
14
+ 5. page_state (değişkenler, komponentler, layout slotları, ms imzaları)
15
+ 6. page_schema_patch (bütün add_component'ler — tek çağrıda)
16
+ 7. page_schema_patch (tema + stil: page_container.style'da --ng-* ve komponent
17
+ style'ları; ifadeye bağlı prop'lar için set_binding)
18
+ 8. flow_create × N (BÜTÜN akışlar — bağlı akış varken yenisi eklenemez, Kural 3)
19
+ 9. page_schema_patch (bütün set_event'ler tek çağrıda)
20
+ 10. page_get + page_runtime_get (doğrulama: bağlar doğru mu, son hâl nasıl çiziliyor)
21
+ ```
22
+
23
+ **Ekranın tipi hangi kuralı okuyacağını belirler:** alanları tasarım anında bilinen klasik form
24
+ → Kural 7a; **alan sayısı/tipi veriden geliyorsa** (anket, dinamik başvuru) → **Kural 1g**
25
+ (`repeater`, `html`'e gerek yok); görsel kalite ve marka teması → **Kural 1h** (hazır stil
26
+ blokları 1h-3'te).
27
+
28
+ ## Kural 1 — Komponent gövdesinin şekli (`DesignComponentManagementSchema`)
29
+
30
+ Sayfa şeması **micro-op hattından** kurulur; `page_create`'e satır içi `schema` vermek de
31
+ mümkündür ama doğrulama duvarı ve canlı canvas yalnız `page_schema_patch`'te çalışır.
32
+
33
+ ```jsonc
34
+ { "elementId": "page_header_1", "kind": "component", "type": "<component_list slug'ı>",
35
+ "name": "Page header",
36
+ "props": { "pageTitle": { "type": "static", "data": "Branşlar" } },
37
+ "states": { "isSticky": false, "isDroppable": false, "isScrollable": false, "isWrapper": false },
38
+ "blocks": [{ "breakpoint": 1024, "size": { "h": 4, "w": 12 }, "position": { "x": 0, "y": 0 } }],
39
+ "children": [], "events": [], "validations": [], "stateBindings": [] }
40
+ ```
41
+
42
+ - **`props` düz sözlük DEĞİL:** her değer bir `Prop` nesnesidir ve alanları `type` / `ref` /
43
+ `data`'dır — **`value` diye bir alan yoktur.** Static prop için `{ "type": "static", "data": … }`
44
+ yaz; `{ "value": … }` yazarsan platform sessizce boş prop kaydeder
45
+ (`DesignComponentProperty.SetRef` static'te `value.Data` okur).
46
+ - **Prop'un içi de Prop'tur (özyinelemeli).** Bir Prop'un `data`'sı nesne ya da dizi ise
47
+ platform içini **her seviyede** `Dictionary<string, Prop>` olarak çözer
48
+ (`CreateProp` object/array dalları) — düz bırakılan iç değer `JsonException … could not be
49
+ converted to Prop` verir. Skaler dizi (`[1,2,3]`) hiçbir biçimde desteklenmez. Doğru şekil
50
+ (header `navigation` örneği — iç `icon` nesnesinin İÇİ de sarılı):
51
+
52
+ ```jsonc
53
+ "navigation": { "type": "static", "data": [
54
+ { "title": { "type": "static", "data": "Kitaplar" },
55
+ "href": { "type": "static", "data": "/kitaplar" },
56
+ "icon": { "type": "static", "data": {
57
+ "name": { "type": "static", "data": "book" },
58
+ "color": { "type": "static", "data": "#0052CC" } } } }
59
+ ] }
60
+ ```
61
+
62
+ **Gateway eksik sarımı kendisi tamamlıyor** (`gatewayNormalized`: "iç değerler Prop'a
63
+ sarıldı") — düz `{name, color}` yazsan da düzeltilir; ama ham HTTP kullanıyorsan bu kural sana ait.
64
+ Sarım `add_component` / `replace_prop` op'larında, `page_create`/`page_update` ve `layout_*`
65
+ gövdelerinde ve artık `page_schema_validate` KURU KOŞUSUNDA da uygulanır (crm koşumu B-11'de
66
+ kuru koşu ile yazma farklı davranıyordu; ikisi eşitlendi). Platformun kendisi sarmıyor:
67
+ `columns` · `colorMap` · `actions` · `options` gibi **dizi prop'larının her yaprağı**
68
+ `{ type:'static', data: … }` olmak zorunda, aksi hâlde `to-domain: The JSON value could not be
69
+ converted to … Prop. Path: $.name` alırsın (hata komponenti/prop'u söylemez).
70
+ - **Enum'lar küçük harf** (`StringEnumConverter` + `[Description]`): `kind` → `component` |
71
+ `widget`, prop `type` → `static` | `file` | `flow` | `widget` | `element` | `page` | `code`.
72
+ - **Sayfaya bağlanan prop'larda (link/yönlendirme) URL yazma, `type: 'page'` + `ref: <pageId>`
73
+ kullan.** Platform slug'ı kendisi çözer (`Prop(PropValueType.Page)` → `Ref = RedirectPageId`,
74
+ `Data = RedirectPage.Slug`). Sabit `'/hastalar'` yazarsan sayfa slug'ı değişince bağlantı
75
+ sessizce kırılır; `pageId` referansı değişiklikten etkilenmez. Aynısı `flow` prop'u için
76
+ `type: 'flow'` + `ref: <flowId>`, dosya için `type: 'file'` + `ref: <fileId>`.
77
+ - **`blocks[].size`, komponentin `component_get`'teki `maxSize`'ını aşamaz.** Dikkat: katalog
78
+ varsayılanı aşabiliyor (`page-header` default `h:8`, `maxSize` `h:4`) — varsayılanı körü körüne
79
+ kopyalama, doğrulayıcı `"Height of 'page-header' must be between 1 and 4."` ile reddeder.
80
+ - **Aynı ızgara hücrelerini iki komponente veremezsin.** Studio'da komponent tuvale bırakıldığında
81
+ çakışan kardeşleri aşağı itme işini studio-api'nin sayfa kurma servisi yapar
82
+ (`SchemaHelper.reorderComponents` → `draggableElement.moveItem`); komple şema yazan `page_create`
83
+ / `page_update` o servisten geçmez, yani verdiğin koordinatlar olduğu gibi kayda girer ve tuvalde
84
+ komponentler üst üste biner. **Gateway aynı algoritmayı şemanın tamamına uyguluyor** (kök dizi +
85
+ her `children`, yalnız desktop `blocks[]` girdisi; `x`/`w`/`h` ve dizi sırası korunur, çakışanın
86
+ `position.y`'si aşağı kayar) ve yanıtta `gatewayNormalized: "çakışan komponentler ızgarada aşağı
87
+ kaydırıldı (…)"` ile bildiriyor. Kökteki `modal`/`drawer` katmanları sayfa akışında durmadığı için
88
+ hariç. Aynı hücreyi paylaşan komponentlerden hangisinin üstte kalacağı garanti değil — konumu sen
89
+ doğru ver, düzeltme yalnız emniyet kemeri.
90
+ - **Zorunlu alanlar:** `states` her yolda, `events` / `validations` / `stateBindings` ise
91
+ **micro-op yolunda** (`page_schema_patch`). `states` için sebep açık:
92
+ `DesignComponentManagementSchema.ToDomain:47` guard'sız okur, `Appearance`'ın varsayılanı var
93
+ `States`'in yok. Diğer üçünü `page_create` (POST /pages) tolere ediyor ama `PATCH …/schema`
94
+ etmiyor — canlı deneyde üçü **birlikte** verilince geçti, her biri **tek başına** verilince
95
+ `NullReferenceException` aldı. **Gateway hepsini senin yerine dolduruyor**
96
+ (`page_create`/`page_update`/`page_schema_patch`, `layout_create/update`,
97
+ `component_create/update`) ve yanıtta `gatewayNormalized` ile bildiriyor; yani elle yazman
98
+ gerekmiyor, ama ham HTTP kullanıyorsan bu farkı bilmen gerekir.
99
+
100
+ > **`page_schema_validate`'e tek başına güvenme.** `ValidatePageSchemaQuery` yalnız micro-op'ları
101
+ > uygulayıp şema kurallarını denetler, `ToDomain`'e hiç girmez — eksik `states`'li bir gövdeye
102
+ > `{"valid":true}` der, yazma yolu ise patlar. Gövde tamamlandığında ise doğrulayıcı işini
103
+ > düzgün yapar (boyut/hedef hatalarını açık mesajla verir).
104
+
105
+ ## Kural 1b — `page_build` YOK
106
+
107
+ Entity'den otomatik sayfa üreten `page_build` tool'u **kaldırıldı**. Platformun
108
+ `POST /pages/{app}/build/{page}` ucu üretimi `PageBuilderApiOptions:Url` adresindeki **ayrı bir
109
+ servise** delege ediyor (`PageBuilderApiClient`); dev'de `localhost:4000`, prod'da `studio-api…`.
110
+ O servis bu ürünün parçası değil ve ayakta olmayabilir. Sayfa/komponent şemasının tam karşılığı
111
+ platformda zaten var → doğru yol yukarıdaki micro-op hattıdır.
112
+
113
+ ## Kural 1c — Komponente VERİ bağlama: `mount` olayı + flow (setter adımı)
114
+
115
+ Tablonun/listenin satırları şemaya "dataSource" yazarak değil, **flow ile** doldurulur. Desen:
116
+
117
+ ```
118
+ 1. component_get → komponentin EVENT listesini oku (event'ler komponente özeldir, tahmin edilmez)
119
+ 2. flow_create → start → microservice → setter → end
120
+ 3. page_schema_patch set_event → akışı ilgili olaya bağla
121
+ ```
122
+
123
+ **Event'ler komponent tipine göre değişir; `component_get`'in `events` alanından okunur.**
124
+ Sayfa açılışında veri çekmek için `mount` kullanılır ama tek seçenek o değil — örneğin `table`
125
+ 8 olay yayar:
126
+
127
+ `mount · pagination · filter · sort · select · selectionChange · doubleClick · rowChange`
128
+
129
+ Yani "satırı seçince detay getir" (`select`), "kolondan süzünce sunucudan yeniden çek"
130
+ (`filter`), "sayfa değişince yeni sayfayı çek" (`pagination`), "satıra çift tıklayınca formu aç"
131
+ (`doubleClick`) akışları da aynı `set_event` mekanizmasıyla kurulur. Buna karşılık `page-header`
132
+ ve `datatable` yalnız `mount` yayar (`datatable`'ınki bilinçli — Kural 1e).
133
+
134
+ **Olay adları `on` ÖNEKİ ALMAZ** (`button` → `click`, `form` → `submit`), tek istisna katalogda
135
+ `texteditor`'dur (`onMount`/`onChange`). **Komponentin desteklemediği bir olay adı verirsen bağ
136
+ sessizce ölü kalır** — önce `component_get`. Gateway `set_event`'i katalogla karşılaştırır ve
137
+ uymayan adı **PAGE-008** ile istek gitmeden reddeder.
138
+
139
+ **Olay KALDIRMAK için `remove_event` kullan.** Platformun `set_event` micro-op'u olay silemez:
140
+ `eventValue: null` verildiğinde kayıt `{ name: 'click' }` olarak (değersiz) kalır, diziden
141
+ çıkmaz — şemada ölü bağ artığı bırakır. `{ "op": "remove_event", "target": "<elementId>",
142
+ "event": "click" }` gateway emülasyonudur: şemayı okur, olayı diziden çıkarır ve sayfayı tam
143
+ gövdeyle geri yazar (yanıtta `removedEvents`). Sayfa düzeyi olaylar için `target: "page"`.
144
+
145
+ **Olay YÜKÜ komponente özeldir ve `component_get` onu da veriyor** (`events[].payload`, gerekirse
146
+ `events[].reads`) — **ama yük akışa `event.data` ile GELMEZ.** Motor emit'in `data`'sını önce
147
+ komponentin KENDİ prop'una yazar (`FlowMachine.onEmit` → `setNode`), akışı ise emit'in
148
+ `params`'ıyla başlatır; ifade kapsamındaki `event.data` `params.data`'dır ve komponent
149
+ olaylarında **boştur** (ölçüm 2026-08-25, kaynak okuması: `flow-machine.ts:299-318` +
150
+ `flow-execution.functions.ts:236`; `Table.svelte:292` kendi yorumunda aynı şeyi söylüyor).
151
+
152
+ Pratik kural: **`payload` anahtarları aynı zamanda prop adlarıdır; akışta `self.<anahtar>` ile
153
+ oku** (ya da `value { type:'property', ref:{ source:'component', element, property } }` ile).
154
+ `event.data` yalnız `params` ile gelen olaylarda dolar — pratikte `html` komponentinin
155
+ `fireEvent` aksiyonları (`platform_guide('html')`). Sessiz hata örneği: sayfalama akışını
156
+ `event.data.limit`'e bağlarsan sorgu `LIMIT null OFFSET null` ile koşar ve **tüm satırları**
157
+ döndürür; hata yok, sayfalama yok.
158
+
159
+ | Komponent · olay | Yük (aynı zamanda prop adları) | Akışta nereden okunur |
160
+ |---|---|---|
161
+ | `table.sort` | `{ sortField, sortType, offset }` | `self.sortField` · `self.sortType` · `self.offset` |
162
+ | `table.filter` | `{ filters, offset }` | `self.filters` · `self.offset` |
163
+ | `table.pagination` | `{ offset, limit }` | `self.offset` · `self.limit` |
164
+ | `table.select` · `doubleClick` | — | `self.selectedRow` |
165
+ | `table.selectionChange` | — | `self.selectedRows` |
166
+ | `datatable.mount` | — | `self.filters` · `self.sortField` · `self.sortType` · `self.offset` |
167
+ | `pagination.change` | `{ offset, limit }` | `self.offset` · `self.limit` |
168
+ | `textinput/textarea/password.change` | `{ value }` | `self.value` |
169
+ | `dropdown.change` · `multi-select.change` | `{ value }` | `self.value` |
170
+ | `dropdown.search` · `multi-select.search` | `{ searchText }` | `self.searchText` |
171
+ | `tabs.tab` | `{ selected }` | `self.selected` |
172
+ | `chart.dataPointClick` | `{ name, seriesName, value, dataIndex, row }` | `self.row` (kaynak satırın tamamı) |
173
+ | `repeater.change` | `{ selectedIndex }` | `item` / `Row` satır bağlamı |
174
+ | `html` aksiyonu (`fireEvent`) | ikinci argüman | **`event.data`** (tek istisna) |
175
+
176
+ Adım gövdesi `StepManagementSchema`: `{ stepId, name, type, kind, args, branches }`.
177
+ **`branches[].nextStepId` bir GUID değil, hedef adımın `stepId` STRING'idir**
178
+ (`allSteps.TryGetValue(b.NextStepId)`).
179
+
180
+ Bu adımda platformun sessizce yuttuğu beş tuzak var — hepsi isteği 200 döndürür, akış yine de
181
+ ölü ya da yarım çalışır. Gateway hepsini yakalar (FLOW-004…012), ama şekli baştan doğru yazın:
182
+
183
+ **1. Dal adı motorun anahtarıdır, etiket değil.** Runtime dal kimliğini `branch.name`'den üretir
184
+ (`Runtime.svelte:329`) ve her adım `engine.next('always')` çağırır. Ad `always` değilse motor
185
+ dalı bulamaz ve akışı **sessizce durdurur** (`flow-engine.ts:79`); Studio da zinciri
186
+ `sourceBranch === 'always'` ile çizdiği için panelde **Start kartından sonrası boş görünür**.
187
+ İstisnalar: `condition` → dal adı rule id'sidir, `confirm` → `yes|no|always`, `microservice` →
188
+ `always` (eski akışlarda `success` de çalışır).
189
+
190
+ **2. `iteratees[].type` yalnız `change` | `fill`.** `setter.ts` başka değerde hiçbir dala
191
+ girmez — adım çalışır, hiçbir şey yapmaz. `change` tek prop'a değer yazar, `fill` diziyi
192
+ `mapping` ile eşleyerek yazar.
193
+
194
+ **3. Storage anahtarı adımın ADIdır, `stepId` değil.** Sonuç `setStorage(task.name, …)` ile
195
+ yazılır; `isRaw: false` iken sarmal `{ data, headers, error }` olduğu için ref
196
+ `"<adım adı>.data"` olur. Adım adında **nokta kullanmayın** (okuma dot-prop ile yapılır).
197
+ Kod ifadesinden okuma biçimi `Steps["<adım adı>"].data`'dır; `value.type: "code"` + ref olarak
198
+ düz `stepId` yazmak tanımsız değişken hatası verir. **Anahtarı iki yerden verebilirsin ve
199
+ öncelik `mapping.source`'tadır:** doluysa `value.ref`'e hiç bakılmaz (`setter.ts:161`) — satır
200
+ eşlemeli tablo yazımında Studio'nun ürettiği şekil de budur (`mapping.source: "<ad>.data"`,
201
+ `value.ref: ""`). İkisini birden doldurma.
202
+
203
+ **4. Hata yolunda storage sarmalında `headers` YOKTUR.** Başarıda `{ data, headers, error }`,
204
+ hatada `{ error, data }` yazılır (`microservice.ts:132`). Bu yüzden toplam kayıt sayısını
205
+ guard'sız okumak (`Number(Steps.get_service.headers["total-count"])`) istek patladığı anda
206
+ TypeError verir: prop'a `NaN` yazılır, ifade hatası toast'ı düşer ve akış `onError`'a düşebilir.
207
+ Doğrusu `Number(Steps.get_service?.headers?.["total-count"] ?? 0)`. Header adı backend'de
208
+ `Total-Count`'tur, axios anahtarları küçük harfe indirir → `"total-count"`.
209
+
210
+ **5. `mapping.mappings` anahtarı satıra DÜZ yazılır.** `object[key] = …` (`setter.ts:83-104`) —
211
+ `"parent.name"` anahtarı `row["parent.name"]` üretir, `row.parent.name` üretmez. Kolonun kendi
212
+ key'ini düz anahtar ver, iç içe yolu `ref`'e yaz: `"parent_name": { "type": "item", "ref":
213
+ "parent_title.name" }`. Anahtar kümesini tablonun gerçek kolonlarıyla sınırlı tut; her fazladan
214
+ anahtar her satırda taşınır. `type` sözlüğü: `item` (kaynak alanı), `code` (satır başına JS,
215
+ satır `Row` değişkenindedir), diğerleri sabit.
216
+
217
+ ```jsonc
218
+ // akışı başlatan adım — dal adı 'always' olmak zorunda
219
+ { "stepId": "start", "name": "Start", "type": "start", "kind": "normal", "args": {},
220
+ "branches": [{ "id": "<GUID>", "name": "always", "nextStepId": "load_specialties" }] }
221
+
222
+ // veriyi çeken adım
223
+ { "stepId": "load_specialties", "name": "Branşları getir", "type": "microservice", "kind": "normal",
224
+ "args": { "microserviceId": "<ms id>", "isRaw": false, "parameters": [] },
225
+ "branches": [{ "id": "<GUID>", "name": "always", "nextStepId": "set_rows" }] }
226
+
227
+ // prop'a yazan adım
228
+ { "stepId": "set_rows", "name": "Tabloya yaz", "type": "setter", "kind": "normal",
229
+ "args": { "iteratees": [{
230
+ "id": "<GUID>",
231
+ "type": "change", // yalnız change | fill
232
+ "element": "specialties_table", // hedef komponentin elementId'si
233
+ "property": "rows", // component_get'teki prop adı
234
+ "source": "component",
235
+ "mapping": { "source": "", "mappings": {} }, // storage okurken zorunlu, boş da olsa
236
+ "value": { "type": "storage", "ref": "Branşları getir.data" } }] }, // kaynak adımın ADI
237
+ "branches": [{ "id": "<GUID>", "name": "always", "nextStepId": null }] } // son adım: null
238
+ ```
239
+
240
+ > **`element` ve `property` İKİSİ DE zorunludur — biri eksikse adım sessizce ölür.** `setter.ts`
241
+ > `change()` ve `fill()` her dalda `machine.setNode(element, { [property]: value })` çağırır;
242
+ > `element` boşken çağrı hiçbir düğüme denk gelmez: **adım çalışır, hata vermez, hiçbir şey
243
+ > yazmaz.** Platform gövdeyi 200 ile kabul eder (alan serbest string) ve Studio adımı çizer —
244
+ > ekranda "değer girilmiş ama hedef seçilmemiş" bir adım görürsün. Ölçüm (anket-spike,
245
+ > 2026-08-19): `html` komponentine `context` yazan iki akış da yalnız bu yüzden hiç çalışmıyordu.
246
+ > Platform bu alanları doğrulamaz; tek savunma gateway'in **FLOW-017** kuralıdır — gövdeyi istek
247
+ > göndermeden reddeder. `element` değerini `page_schema_get` çıktısındaki `elementId` ile doğrula,
248
+ > uydurma.
249
+
250
+ Satır eşlemeli tablo yüklemesinin tam şekli (yükleniyor bayrağı → uç → satırlar + toplam kayıt)
251
+ `ngapps://examples/flow-load-table`'dadır; setter'ın kritik iki iteratee'si şöyle görünür:
252
+
253
+ ```jsonc
254
+ // toplam kayıt — guard'sız yazarsan hata yolunda patlar
255
+ { "type": "change", "element": "titles_table", "property": "totalRows", "source": "component",
256
+ "mapping": { "source": "", "mappings": {} },
257
+ "value": { "type": "code", "ref": "Number(Steps.load_titles?.headers?.[\"total-count\"] ?? 0)" } }
258
+
259
+ // satırlar — anahtar mapping.source'ta, mapping anahtarları DÜZ ve kolon key'leriyle birebir
260
+ { "type": "change", "element": "titles_table", "property": "rows", "source": "component",
261
+ "mapping": { "source": "load_titles.data", "mappings": {
262
+ "name": { "type": "item", "ref": "name", "propertyType": "text" },
263
+ "parent_name": { "type": "item", "ref": "parent_title.name", "propertyType": "text" },
264
+ "status": { "type": "code", "ref": "Row.status === 'Active' ? 'Aktif' : 'Pasif'", "propertyType": "text" } } },
265
+ "value": { "type": "storage", "ref": "" } }
266
+ ```
267
+
268
+ `errorSteps` verirsen (start dışında en az bir adım) hata anında `engine.onError` true döner ve
269
+ **ana akışın kalan adımları hiç çalışmaz** — yükleniyor bayrağını kapatmak, hata mesajını
270
+ göstermek error flow'un işidir (`alert` adımı: `args: { message, alertType: 'error' }`).
271
+
272
+ Zorunlu filtresi olan uçlarda (`patient-*`, `doctor-appointments` gibi `isRequired: true`
273
+ filtreli microservice'ler) parametreyi adımda ver, yoksa uç `MissingParameterException` atar:
274
+
275
+ ```jsonc
276
+ "parameters": [{ "name": "patient_id", "type": "guid", "source": "query-string",
277
+ "value": { "ref": "<hasta id>", "type": "static" } }]
278
+ ```
279
+
280
+ Gerçek uygulamada `static` yerine sayfa değişkeni / route parametresi kullanılır
281
+ (`variable_create` + `value.type: 'variable'`). **Kullanıcıya özel veri çeken uçlarda
282
+ (hasta/doktor/oturum sahibi) kalıcı çözüm parametre DEĞİL middleware'dir:** uca auth
283
+ middleware'i bağlanır, filtre `source:'expression'` + `value:'user.id'` olur — kimlik
284
+ istemciden gelmediği için sahtelenemez ve sayfada sabit id tutmak gerekmez. Sayfanın kendisi de
285
+ `page_create/update` `middlewareIds` ile korunur (girişsiz kullanıcıyı login'e yönlendirme).
286
+ Tarif: `platform_guide('middleware')`.
287
+
288
+ **`flow-trigger` adımının İKİ geçerli şekli var** (`flow-trigger.ts`; başka şekil sessizce hiçbir
289
+ şey yapmaz, ikisi de yoksa kayıt 404 döner):
290
+
291
+ ```jsonc
292
+ // (1) başka bir akışı çalıştır — "kaydettim, listeyi yenile"
293
+ { "stepId": "reload_list", "name": "reload_list", "type": "flow-trigger", "kind": "normal",
294
+ "args": { "flowId": "<hedef flow id>" },
295
+ "branches": [{ "id": "<GUID>", "name": "always", "nextStepId": null }] }
296
+
297
+ // (2) bir komponentin OLAY akışını çalıştır — Studio'nun ürettiği şekil budur
298
+ { "stepId": "refresh_table", "name": "refresh_table", "type": "flow-trigger", "kind": "normal",
299
+ "args": { "triggerElementId": "tickets_table", "triggerElementEvent": "mount" },
300
+ "branches": [{ "id": "<GUID>", "name": "always", "nextStepId": null }] }
301
+ ```
302
+
303
+ - İkisi birden verilirse runtime **yalnız `flowId`'ye** bakar, element dalına hiç girmez.
304
+ - (2)'de `triggerElementId` sayfadaki bir **elementId**'dir ve `triggerElementEvent` o komponentin
305
+ **gerçekten yaydığı** bir olay olmalı; ayrıca o olayın bir akışa bağlı olması gerekir
306
+ (`events[].value` dolu), yoksa çalıştıracak bir şey bulunamaz.
307
+ - Adım hedef akışı **bekler** (`await engine.run`), sonra `always` dalından devam eder.
308
+ - Gateway boş/yarım `args`'ı **FLOW-014** ile yakalar.
309
+
310
+ **Sözlükler** (`StepType.cs`, `StepParameterValueType.cs` — hepsi küçük harf):
311
+ `start · microservice · setter · setcookie · validation · condition · redirect · alert ·
312
+ confirm · flow-trigger · storage · navigate · code · download · end`
313
+ değer tipleri: `static · page · property · storage · variable · code · item · state`
314
+
315
+ > `code` adımının içinde ve `value.type:'code'` ifadelerinde kullanılabilen bağlam
316
+ > (`Page.*`, komponentlere adıyla erişim, `state.page/app`, `Steps`, `self`/`item`/`event`,
317
+ > `cookie.get/set`, `await`) ve `value.type` çözümleme tablosu için
318
+ > **`platform_guide('expressions')`** — uydurma erişim adı sessizce `undefined` üretir.
319
+
320
+ > Bir microservice adımı kaydedildiğinde platform `args.microserviceSchema`'yı
321
+ > (parameterScheme + returnScheme + `isReturnArray`) kendisi doldurur — `flow_get` ile geri
322
+ > okuyunca hangi alanların döndüğünü oradan görürsün.
323
+
324
+ **Sayfalama üç parçadır; biri eksikse tablo sessizce tek sayfa kalır.** Uç `limit`/`offset`'i
325
+ **query-string'den** okur (`sourceParameters.limit: { name, offset, source }`) ve
326
+ `TableMicroserviceExecutor.cs:178` sayfalamayı **yalnız limit doluysa** uygular — parametre
327
+ geçmezsen sunucu her çağrıda tüm kayıtları döndürür, `pagination` olayı yeni sayfa çekemez.
328
+ Gereken üçlü:
329
+
330
+ ```jsonc
331
+ // 1) akışta: uca tablonun kendi prop'larını geçir
332
+ "parameters": [
333
+ { "name": "limit", "type": "text", "source": "query-string",
334
+ "value": { "type": "property", "ref": { "source": "component", "element": "blog_table", "property": "limit" } } },
335
+ { "name": "offset", "type": "text", "source": "query-string",
336
+ "value": { "type": "property", "ref": { "source": "component", "element": "blog_table", "property": "offset" } } }
337
+ ]
338
+ // 2) şemada: { "op": "replace_prop", "target": "blog_table", "prop": "isPaginated", "value": true }
339
+ // 3) şemada: { "op": "set_event", "target": "blog_table", "event": "pagination", "eventValue": "<load flow id>" }
340
+ ```
341
+
342
+ Toplam kayıt sayısı `Total-Count` header'ından gelir (`TableMicroserviceExecutor.cs:201`, uç
343
+ `isSingle: false` olduğu sürece) — `totalRows`'u yukarıdaki guard'lı ifadeyle yaz. Gateway
344
+ parametreleri geçmeyi unutursan `FLOW-013` ile uyarır.
345
+
346
+ > **`replace_prop` Prop sarmalı DEĞİL ham değer alır** — `PageSchemaMicroOpApplier.cs:77`
347
+ > `new Prop(PropValueType.Static) { Data = op.Value }` kurar. `{ "type": "static", "data": true }`
348
+ > gönderirsen sarmalın kendisi `Data`'ya yazılır ve platform onu `Dictionary<string, Prop>` diye
349
+ > çözmeye çalışıp **500 JsonException** verir. Doğrusu `"value": true`; kod prop'u için `value`
350
+ > değil `expression` alanı kullanılır. (Gateway her ikisini de düzeltip `gatewayNormalized` ile
351
+ > bildirir, ama ham HTTP'de bu kural sana ait.) `add_component`'te ise sarmal ZORUNLUdur — iki
352
+ > op'un şekli birbirinin tersidir, karıştırma.
353
+
354
+ ## Kural 1c-2 — Komponentin `name`'i JS TANIMLAYICISI olmalı (yoksa sayfadaki tüm ifadeler ölür)
355
+
356
+ Runtime, ifade çalıştırmadan önce sayfadaki **her** komponent için bir değişken üretir:
357
+
358
+ ```js
359
+ const <komponentin name'i> = Components["<komponentin name'i>"];
360
+ ```
361
+
362
+ `name` geçerli bir JS tanımlayıcısı değilse (boşluk, nokta, birden çok tire…) üretilen satır
363
+ sözdizimi hatası olur ve **o sayfadaki BÜTÜN ifadeler** — condition kuralları, `value.type:'code'`
364
+ ref'leri, `code` adımları — tek seferde ölür:
365
+
366
+ ```
367
+ Expression error: Missing initializer in const declaration in <senin ifaden>
368
+ ```
369
+
370
+ Belirti kafa karıştırıcıdır: hata **ifadeyi** gösterir, oysa suçlu bambaşka bir komponentin adıdır.
371
+ Ölçüm (2026-08-18, `ik`): `name: "Giriş Kutusu"` yüzünden giriş sayfası hiç çalışmadı — servis 200
372
+ dönüyor, condition patlıyor, kullanıcı yönlenmiyordu.
373
+
374
+ **Kural:** `name` **her zaman `elementId` ile aynı** olsun (`btn_login`, `tbl_mng`). Okunabilir
375
+ Türkçe başlık `props.title` / `props.text` / `props.label` / `props.pageTitle` içine yazılır —
376
+ `name` bir etiket değil, **değişken adıdır**.
377
+
378
+ > **Layout adları her sayfaya sızar.** Layout'un komponentleri de aynı `const` listesine girer;
379
+ > layout'ta tek bir bozuk ad, o layout'a bağlı **her** sayfayı öldürür. `layout_update` yaparken de
380
+ > `name = elementId` kuralı geçerlidir.
381
+ >
382
+ > Aynı sebeple akış ifadelerinde komponente **`name` ile** erişilir (`tbl_mng.selectedRow`);
383
+ > `elementId` ile eşit tutmazsan `element:` alanı çalışır ama ifadedeki değişken `undefined` kalır.
384
+ >
385
+ > Gateway bunu **PAGE-009** ile `add_component`, `page_update(schema)` ve `layout_update(views)`
386
+ > yollarında istek gitmeden reddeder.
387
+
388
+ ## Kural 1d — Sayfa iskeleti: `page_container` ve boyutlandırma
389
+
390
+ **Her sayfada `page_container` adlı bir kapsayıcı vardır ve silinmez.** Komponentler köke değil
391
+ **onun `children`'ına** eklenir:
392
+
393
+ ```jsonc
394
+ { "op": "add_component", "parent": "page_container", "component": { … } } // ✔
395
+ { "op": "add_component", "parent": "root", "component": { … } } // ✘ kapsayıcı dışında kalır
396
+ ```
397
+
398
+ `page_container` bir `container`'dır (`isWrapper: true`, `isDroppable: true`,
399
+ `isScrollable: true`, `appearance.heightType: 'auto'`). `container`, `form` ve `repeater` de aynı
400
+ şekilde kapsayıcıdır — onların içine de `parent` vererek eklenir.
401
+
402
+ Sayfa düzeyi olaylar (page load vb.) **bu kapsayıcıya** bağlanır:
403
+ `set_event { target: 'page_container', event: 'mount', eventValue: <flow id> }`.
404
+ `page_container`'ın kendi `isLoading` prop'u vardır — sayfa açılış akışında yükleniyor bayrağı olarak
405
+ onu kullan. Bir `container` olduğu için **düzen prop'larını da alır**: çocuklarını ızgara yerine
406
+ akışla (ortalama, sağa yaslama, aralık) dizmek istiyorsan `layout: 'flex'` — Kural 1f-2.
407
+
408
+ **Tek istisna `modal` ve `drawer`:** onlar `page_container`'ın içinde değil, şemanın **kökünde**
409
+ kapsayıcının kardeşi olarak yaşar (qm `[SY] Profil bilgilerim`: `page_container`, `drawer_personel`,
410
+ `mdl_parolayi_guncelle` aynı seviyede). Modal içeriği (kendi formu ve `submit` akışı dahil) modalın
411
+ `children`'ına eklenir. Diğer her komponent kapsayıcıya girer.
412
+
413
+ > `page_create` sayfayı boş şemayla açar; **gateway `page_container`'ı senin için ekler** ve
414
+ > yanıtta `gatewayNormalized` ile bildirir. `page_schema_get` ile geri okuduğunda ilk düğüm odur.
415
+
416
+ **Boyut:** `w` 12'lik ızgaradadır. Yükseklikte belirleyici alan `size.h` değil
417
+ `appearance.heightType`'tır:
418
+
419
+ - **`auto`** — kutunun yüksekliği İÇERİKTEN gelir; Studio tuvalde ölçtüğü yüksekliği
420
+ `blocks[].size.h`'e geri yazar ve kardeşleri aşağı iter (`ResizeObserver` → `shrinkOrExpand`).
421
+ Verdiğin `h` yalnız bir başlangıç tahminidir, kırpma yapmaz.
422
+ - **`fixed`** — kutu tam `size.h * 10px` çizilir ve **taşan içerik KESİLİR**
423
+ (`.draggable__item-component { overflow: hidden }`). Yalnız bilerek sınırlamak istediğinde ver.
424
+
425
+ > **Gateway bunu senin için yazıyor (PAGE-014).** Platform `appearance` verilmemiş gövdede
426
+ > yüksekliği `fixed`e sabitler — Studio ise komponentin katalogdaki `isDefaultAutoHeight`
427
+ > değerine bakar. MCP hattında oluşan tüm sayfalar bu yüzden kırpılmış çiziliyordu; gateway artık
428
+ > yazma yollarında (`page_create` · `page_update(schema)` · `page_schema_patch(add_component)` ·
429
+ > `layout_*(views)` · `component_*(schema)`) her düğüme katalog varsayılanını damgalar ve
430
+ > `gatewayNormalized` ile bildirir. Senin verdiğin `appearance.heightType`'a dokunmaz.
431
+ >
432
+ > Katalogda `isDefaultAutoHeight: false` olan (yani `fixed` damgalanan) tipler: `auth-layout`,
433
+ > `html`, `image`, `layout`, `qrcode`, `table`, `text-editor`, `video`. Diğer her tip `auto`'dur.
434
+ > Bu tiplerde `size.h` gerçek yüksekliktir — göz kararı değil, içeriğe göre hesapla
435
+ > (bir satır ≈ 10px; Kural 1f).
436
+
437
+ ## Kural 1d-2 — Sayfanın ve layout'un KENDİ olayları: `mount` · `destroy`
438
+
439
+ Komponent olaylarından AYRI olarak sayfa ve layout kayıtları kendi olay listesini taşır
440
+ (`events: [{ name, value: <flow id> }]`) ve geçerli adlar SABİTTİR: **`mount`**
441
+ (yüklendiğinde) ve **`destroy`** (sayfadan/layouttan ayrılırken — runtime destroy akışlarını
442
+ navigasyondan ÖNCE koşar). Başka ad platformca kabul edilir ama bağ sessizce ölü kalır —
443
+ gateway **PAGE-011 / LAY-004** ile istek gitmeden reddeder.
444
+
445
+ - Bağlama yolu `set_event` DEĞİLDİR (o komponent hedefler): sayfada `page_create` /
446
+ `page_update`'in `events` parametresi, layout'ta `layout_create/update`'in `events`'i.
447
+ - Sayfa açılış akışı için yerleşik desen `page_container.mount`'tur (Kural 1d) — ikisi de
448
+ çalışır; **`destroy` için tek yol buradaki sayfa olayıdır** (interval temizliği, "ayrılmadan
449
+ kaydet" uyarısı gibi işler).
450
+ - Akış kilidi burada da geçerlidir (Kural 3): önce bütün `flow_create`'ler, sonra events bağı.
451
+ - Mevcut bağları `page_get` gösterir (`events[].value` = flow id).
452
+
453
+ ## Kural 1e — İKİ tablo komponenti var: `table` ve `datatable`
454
+
455
+ > **`data-table` KALDIRILDI (2026-08-17).** Katalogda böyle bir slug yok; eski bir örnekte ya da
456
+ > eski bir sayfada görürsen `datatable` ya da `table` ile değiştir. Katalogda tablo işi gören
457
+ > başka komponent yoktur — aşağıdaki iki satır listenin tamamıdır.
458
+
459
+ | slug | Olaylar | Ayırt edici prop'lar |
460
+ |---|---|---|
461
+ | `table` | 8: `mount · pagination · filter · sort · select · selectionChange · doubleClick · rowChange` | `rowSelection` (none/single/multi), `selectedRow`/`selectedRows`, `filterMode` (panel/hide/row), `filters`, `showSort`, `sortField`/`sortType`, `isEditable`, `showColumnMenu`, `actions`, `emptyIcon/emptyText/emptyActions`, `style` (root/thead/th/tr/td) |
462
+ | `datatable` | 1: `mount` | `title`, `locale` (tr/en), `tableStyle` (zebra/plain), kolonlarda `isFilterable`/`isSortable`/`colorMap`/`prefix`/`suffix`/`precision`/`thousands`/`dateFormat`/`imageShape`, `filters`/`sortField`/`sortType` (hidden), `actions` |
463
+
464
+ **`datatable`'ın tek olay yayması eksiklik değil, tasarım.** Filtre paneli, sıralama ve sayfalama
465
+ komponentin İÇİNDE: kullanıcı süzdüğünde/sıraladığında/sayfa değiştirdiğinde `datatable` önce
466
+ kendi `filters`/`sortField`/`sortType`/`offset` prop'larını node'a yazar, sonra **`mount`'u
467
+ yeniden tetikler** (`updateQuery` → `emitMount`). Yani tek bir `mount` akışı yazarsın; o akış
468
+ tabloya bağlı uca bu prop'ları parametre olarak geçirir ve dönen satırları geri yazar.
469
+
470
+ **`table`'da her etkileşim AYRI bir olaydır** ve her birini ayrı akışa bağlarsın (`filter`,
471
+ `sort`, `pagination` olayları yükte yeni değerleri de taşır — yukarıdaki olay yükü tablosu).
472
+
473
+ Her ikisinin de süzme/sıralaması sunucu taraflıdır: seçilen filtreler uca `filters` parametresi
474
+ olarak gider ve ancak `enableDynamicFilters: true` olan bir uçta karşılık bulur
475
+ (`platform_guide('microservice')`).
476
+
477
+ **Karar senin, dayatma yok.** Sorular:
478
+
479
+ - *Kullanıcı bu listeyi ekrandan süzecek/sıralayacak mı?* Hayır ise ikisi de olur; en az kavramla
480
+ kurulanı seç ve `mount` akışı yeterlidir.
481
+ - *Süzme/sıralama var mı?* → `datatable` tek akışla kurar (daha az bağlama), `table` her olayı
482
+ ayrı akışa bağlamanı ister ama karşılığında satır seçimi (`rowSelection`), çift tık, hücre içi
483
+ düzenleme (`isEditable`) ve boş-durum aksiyonları verir.
484
+ - *Satır seçimi / çift tıkla forma geçiş / hücre düzenleme gerekiyor mu?* → **`table`**; bunlar
485
+ `datatable`'da yok.
486
+ - *Zengin kolon biçimlendirme (renk haritası, para birimi öneki, tarih formatı, avatar) gerekiyor
487
+ mu?* → **`datatable`**; kolon şeması bunun için tasarlandı.
488
+
489
+ **Bu, katalogdaki her komponent için geçerlidir:** hangi komponenti kullanacağını
490
+ `component_list`/`component_get` ile keşfedip kendin seçersin, rehberdeki örnekler tercih değil
491
+ şablondur.
492
+
493
+ Kaçınılacak tek şey **yarım kurulum**: süzme arayüzünü açıp (`table` + `filterMode:'row'`, ya da
494
+ `datatable` + `isFilterable` kolonlar) arkasına statik filtreli uç bağlamak. Sayfa çalışır
495
+ görünür, kullanıcı süzer, hiçbir şey olmaz. Ya süzme arayüzünü kapat, ya ucu
496
+ `enableDynamicFilters: true` yap ve akışta `filters` parametresini geçir.
497
+
498
+ > Komponenti sonradan değiştirmek pahalıdır: flow'un `setter` adımı hedefi kayıt anında
499
+ > **DesignComponent id**'sine çözer (`StepIterate.component?.Id`), komponenti silip yeniden
500
+ > eklersen elementId aynı olsa bile o bağ kopar ve flow'u yeniden kaydetmen gerekir. Tipi
501
+ > baştan doğru seç.
502
+
503
+ ## Kural 1f — Koordinat modeli ve layout-slot yerleşimi
504
+
505
+ Izgara kapsayıcıya görelidir ve **bir satır ≈ 10px**'tir; `w` 12 kolon. Her komponentin
506
+ `blocks[].position` (x,y) değeri **kendi parent'ının** ızgarasında anlamlıdır — sayfanın
507
+ tamamında değil.
508
+
509
+ **Sayfa şeması layout'un `slot`'unun İÇİNE enjekte edilir** (Studio ve runtime aynı mapper'ı
510
+ kullanır: `page.mappers.ts` `mapPageLayoutToPreview` — layout varsa çizilen şema layout'un
511
+ `views`'üdür, sayfanın şeması slot'un `children`'ına konur). Bundan üç kural çıkar:
512
+
513
+ 1. **Layout header'ı/sidebar'ı için sayfada boşluk BIRAKMA.** Header slot'un dışındadır;
514
+ "header'ın altından başlayayım" diye ilk komponente y ofseti verirsen boşluk İKİ KEZ
515
+ oluşur — semptom: üstte kocaman boş bant, içerik ancak scroll'la görünür. İçerik
516
+ `page_container` içinde **y=0'dan** başlar.
517
+ 2. **`page_container` her zaman (0,0)'dadır.** Studio her kayıtta bunu kendisi sıfırlar
518
+ (`mapPageSchemaRaw`); gateway de `page_create`/`page_update(schema)` yolunda sıfırlar ve
519
+ `gatewayNormalized` ile bildirir. Kayıtlı bir sayfada y≠0 kapsayıcı görürsen düzeltme
520
+ yolu `page_update(schema)`'dır — `move_component` kapsayıcıya uygulanamaz (PAGE-006).
521
+ 3. **Kardeş y'lerini bitişik ver.** Çakışma itmesi yalnız AŞAĞI iter, boşluk kapatmaz.
522
+ Gateway şüpheli boşlukları **PAGE-010 (uyarı)** ile yanıtın `layoutWarnings` alanında
523
+ söyler: kapsayıcı içinde ilk komponent y>4'te başlıyorsa ya da ardışık kardeşler arasında
524
+ >12 satır (~120px) dikey bant varsa. Layout `views`'ünde aynı analiz **LAY-003**'tür.
525
+ Bilinçli boşluksa uyarıyı yok say; değilse y'leri toparla.
526
+
527
+ > **Layout `views`'ünde satırlar YIĞILMAZ — yalnız kolonlar çalışır.** Ölçüm (anket projesi,
528
+ > 2026-08-19, 1280×720): layout'un doğrudan çocuklarına `position.y` verildiğinde hepsi
529
+ > **top:0**'da çizilir ve üst üste biner; `x`/`w` ise doğru uygulanır. Ölçülen değerler —
530
+ > `public_topbar` (y:0,h:2) → top 0, **576px**; `public_slot` (y:2,h:78) → **top 0**, 576px:
531
+ > ikisi çakıştı, sayfa içeriği header'ın altında kaldı. `heightType: 'auto'` yapmak yüksekliği
532
+ > düzeltmedi (kap yine 576px kaldı).
533
+ >
534
+ > Çalışan desen **kolon ayrımıdır**: sidebar `{x:0,w:2}` + slot `{x:2,w:10}` → sidebar solda,
535
+ > içerik sağda, çakışma yok (aynı ölçümde doğrulandı). Üstte şerit istiyorsan iki seçenek var:
536
+ > (a) şeridi **sayfanın kendi şemasına** koy (ya da tek `html` ekranıysa şablonun içine), ya da
537
+ > (b) katalogdaki `header`/`sidebar` **layout komponent tipleri**ni kullan — generic `container`
538
+ > yerine onlar için ayrı yerleşim yolu var (bu ikinci yol ölçülmedi).
539
+ >
540
+ > Semptom sözlüğü: layout bağlıyken sayfada **devasa üst boşluk + içeriğin başlıkla çakışması**
541
+ > görüyorsan sebep budur; layout bağını kaldırdığında sayfa düzgün çiziliyorsa teşhis kesinleşir.
542
+
543
+ > **Neden 576px? `states.isWrapper: true` düğüme `min-height: 80vh` bastırıyor.** Renderer
544
+ > `getStyle` içinde wrapper düğümlere koşulsuz `height:100%` + **`min-height:80vh`** +
545
+ > `overflow-y:inherit` yazıyor (`RenderComponent.svelte:134`). 720px'lik pencerede bu tam
546
+ > **576px**'tir — yukarıdaki layout ölçümündeki sayı da, sayfa içinde boş görünen dev
547
+ > kutuların sayısı da budur. Ölçüm (anket-native sayfası, 2026-08-19): `isWrapper:true` bir
548
+ > `container`'ın içinde 36px'lik tek bir `multi-select` varken kutu **576px** çizildi; aynı
549
+ > düğüm `isWrapper:false` yapılınca **36px**'e indi (kart yüksekliği 666px → 126px).
550
+ >
551
+ > Kural: **`isWrapper` yalnız gerçekten sayfa boyu kaplaması gereken kap içindir**
552
+ > (`page_container` böyledir ve öyle kalmalı). Sadece "başlık/çerçeve çizilmesin" diye wrapper
553
+ > yapma — o iş `container` için `style` ile (`background:'transparent'`, `border:'none'`),
554
+ > `form` için `hideBorders`/`hidePadding` prop'larıyla yapılır. **`states` için micro-op
555
+ > YOKTUR:** yanlış `isWrapper`'ı düzeltmenin tek yolu düğümü `remove_component` +
556
+ > `add_component` ile yeniden kurmaktır (sonrasında Kural 2'deki flow yeniden kaydetme adımı
557
+ > ZORUNLU olur).
558
+
559
+ Mobil not: <768px'te ızgara tek kolona düşer (`RenderComponent` media query'si) — y sırası
560
+ dikey sıraya dönüşür, x/w yok sayılır. Yerleşimi masaüstü (breakpoint 1024) bloğuyla kur.
561
+
562
+ ## Kural 1f-2 — Kapsayıcı düzeni: `flex` (varsayılan) ↔ `grid`
563
+
564
+ `container` çocuklarını iki şekilde dizebilir ve **`page_container` de bir `container`'dır** —
565
+ aynı prop grubunu o da alır. Grup katalogda tanımlıdır, yani şemadan kurulabilir; ölçüm
566
+ (2026-08-25, `page_schema_validate` kuru koşusu): yedi prop da `valid: true` döndü,
567
+ `layout: 'flexbox'` → `invalid-enum-value`, `flexDirection` → `unknown-prop`
568
+ (doğru adlar `component_get(slug:'container')` çıktısındadır).
569
+
570
+ | Prop | Değerler (varsayılan **kalın**) | Ne yapar |
571
+ |---|---|---|
572
+ | `layout` | **`flex`** · `grid` | `flex`: akış düzeni. `grid`: 12 kolonluk ızgara (Kural 1f). |
573
+ | `direction` | **`row`** · `column` | `flex-direction`; hangi hizalamanın ANA eksen olduğunu belirler. |
574
+ | `horizontalAlign` | **`left`** · `center` · `right` · `stretch` | `row`'da ana eksen (`justify-content`), `column`'da çapraz eksen (`align-items`). |
575
+ | `verticalAlign` | **`top`** · `middle` · `bottom` · `stretch` | `row`'da çapraz eksen, `column`'da ana eksen (rolleri `horizontalAlign` ile takas). |
576
+ | `size` | **`shrink`** · `grow` | Çocuklara `flex: 0 1 auto` / `1 1 auto`. |
577
+ | `gap` | `none` · `small` · **`medium`** · `large` | 0 / 8 / 16 / 32px. |
578
+ | `wrap` | **kapalı** · açık | `flex-wrap`; kapalıyken taşan çocuklar sıkışır, alta inmez. |
579
+
580
+ `stretch` iki eksende farklı anlama gelir: **ana** eksende `space-between` (çocukları uçlara
581
+ dağıtır), **çapraz** eksende gerçekten doldurur.
582
+
583
+ **Varsayılan ≠ renderer davranışı — ikisini karıştırma.** `layout`'un katalog varsayılanı
584
+ 2026-08-25'te `grid` → `flex` oldu, ama varsayılan düğüm **kurulurken** props'a yazılır (Studio
585
+ bırakma yolu `definition.defaults`'u kopyalar); renderer varsayılanı bilmez, yalnız düğümün kendi
586
+ prop'una bakar (`isFlexLayout`). İki sonucu var:
587
+
588
+ - **Eski kayıtlar değişmedi:** `layout` prop'u olmayan her kapsayıcı ızgara çizilmeye devam eder.
589
+ - **Sen şema kuruyorsan varsayılana güvenme:** istediğin düzeni `layout`'a AÇIKÇA yaz. Yazmazsan
590
+ ızgara alırsın — Studio'dan sürüklenen kardeş kapsayıcı flex gelirken seninki grid kalır.
591
+
592
+ Gateway'in kendi eklediği kök kapsayıcı (`page_create`'te şemada `page_container` yoksa) bu yüzden
593
+ seti açıkça taşır: `flex` · `row` · `left` · `top` · `shrink` · `medium`. **`row` + `wrap` kapalı
594
+ demek, alt alta düşünülmüş `w: 12` çocukların TEK SATIRA sıkışması demek** — dikey akan bir sayfa
595
+ istiyorsan kapsayıcıya `direction: 'column'` yaz (ya da `layout: 'grid'`e dön).
596
+
597
+ **Ne zaman flex?** Yan yana buton/aksiyon grubu, toolbar, rozet dizisi, ortalanmış giriş kartı,
598
+ "kalanı doldursun" yerleşimleri. Alt alta akan form alanları için ızgara da doğrudur — orada flex
599
+ istiyorsan `direction: 'column'` şart, `row` ile alanlar tek satıra sıkışır.
600
+
601
+ Dört şeyi bilmeden flex kurma:
602
+
603
+ 1. **Genişlik yine 12 kolondur — içerik genişliği DEĞİL.** Çocuğun `blocks[].size.w`'si
604
+ `row`'da `flex-basis: w/12`, `column`'da `width: w/12` olur (`gap` payı formülden düşülür,
605
+ böylece `w` toplamı 12 olan satır tam kapanır). Bu bilinçli bir tercih: ızgara ↔ flex geçişi
606
+ kayıpsızdır ve genişlik şemadan ifade edilebilir kalır. `column` + `horizontalAlign: 'stretch'`
607
+ tek istisnadır — orada çocuk tam genişlik olur.
608
+ 2. **`position.y` artık yalnız SIRADIR.** Flex çocuklar `y`, sonra `x` sırasına göre dizilir;
609
+ aradaki boşluklar çizilmez. Yine de bitişik ver (0,1,2…): kapsayıcı ileride `grid`'e
610
+ döndürülürse şema geçerli kalsın. Gateway bu yüzden **flex kapsayıcının İÇİNDE PAGE-010
611
+ boşluk uyarısı üretmez** (ızgarada üretmeye devam eder).
612
+ 3. **Flex kapsayıcının yüksekliği içeriğine oturur.** `verticalAlign`'ın dağıtacak boşluğu
613
+ olması için `appearance.heightType: 'fixed'` + `size.h` vermelisin. **İstisna
614
+ `page_container`**: `states.isWrapper: true` olduğu için zaten `min-height: 80vh` alır
615
+ (Kural 1f), dolayısıyla dikey hizalama onda kutu boyu vermeden de çalışır.
616
+ 4. **`layout: 'grid'` iken diğer altı prop ÖLÜDÜR.** Katalogda tanımlı oldukları için
617
+ doğrulama duvarı susar, renderer da okumaz: ızgara kapsayıcıya `direction: 'column'` yazmak
618
+ sessizce hiçbir şey yapmaz. Gateway bunu **PAGE-015 (uyarı)** ile söyler.
619
+
620
+ ```jsonc
621
+ // Ortalanmış giriş kartı — style ile CSS hack'i YOK, kabın kendi düzeni
622
+ { "op": "replace_prop", "target": "page_container", "prop": "layout", "value": "flex" }
623
+ { "op": "replace_prop", "target": "page_container", "prop": "direction", "value": "column" }
624
+ { "op": "replace_prop", "target": "page_container", "prop": "horizontalAlign", "value": "center" } // çapraz eksen → yatay ortalama
625
+ { "op": "replace_prop", "target": "page_container", "prop": "verticalAlign", "value": "middle" } // ana eksen → dikey ortalama
626
+ { "op": "replace_prop", "target": "page_container", "prop": "gap", "value": "large" }
627
+ // kart w:4 verilirse %33 genişlikte ve ortada durur (column'da genişlik = w/12)
628
+
629
+ // Sağa yaslı aksiyon şeridi — kabın kendisi w:12, çocuklar sıkışmadan sağa toplanır
630
+ { "op": "replace_prop", "target": "row_actions", "prop": "layout", "value": "flex" }
631
+ { "op": "replace_prop", "target": "row_actions", "prop": "horizontalAlign", "value": "right" }
632
+ { "op": "replace_prop", "target": "row_actions", "prop": "verticalAlign", "value": "middle" }
633
+ { "op": "replace_prop", "target": "row_actions", "prop": "gap", "value": "small" }
634
+ ```
635
+
636
+ > Düzen işi `style` prop'una AİT DEĞİL. Kural 1h'deki `style` reçeteleri tema/görsellik içindir;
637
+ > `display:flex` gibi düzen kurallarını oraya yazmak kabın kendi düzeniyle çakışır (PAGE-014'teki
638
+ > ölçüm: string style ile kart 66px'e çöktü). Yerleşim bu prop grubuyla, tema `style` ile kurulur.
639
+
640
+ ## Kural 1g — Dinamik form (satır sayısı ve alan tipi veriden gelen ekran): `repeater`
641
+
642
+ "N tane soru, her birinin cevap tipi farklı" gibi ekranlar için `html` kaçış kapısına gerek
643
+ YOK — `repeater` (katalog adı *List View*) bunun hazır cevabıdır. **Tam gövde (iki akış, satır
644
+ şekli, bağ listesi, doğrulama adımları): `ngapps://examples/page-dynamic-form`.** Ölçülmüş çalışan desen
645
+ (anket-native sayfası, 2026-08-19; 4 soru: tekil seçim / çoklu seçim / evet-hayır / serbest
646
+ metin → tek gönderimde kaydedildi):
647
+
648
+ ```
649
+ q_form (form)
650
+ └─ q_list (repeater) rows = [{id, no, text, help, required, kind,
651
+ └─ q_card (container) is_single, is_multi, is_text, is_yesno,
652
+ ├─ q_head (text) options:[{label,value}], answer}]
653
+ ├─ q_help (text)
654
+ ├─ a_choice (radio) isHidden: {{ !(item.is_single || item.is_yesno) }}
655
+ ├─ a_text (textarea) isHidden: {{ !item.is_text }}
656
+ └─ box_multi (container) isHidden: {{ !item.is_multi }}
657
+ └─ a_multi (multi-select)
658
+ ```
659
+
660
+ - **Şablon `children`'dır.** Repeater `rows` dizisindeki her satır için `children` ağacını
661
+ yineler; satır bağlamı çocuklara `item` (eşanlamlısı `Row`, sırası `RowIndex`) olarak iner
662
+ ve **iç içe her seviyeye** taşınır (`box_multi` içindeki `a_multi` de `item`'ı görür).
663
+ - **İki yönlü bağ vardır ve flow gerektirmez.** Bir çocuk komponentin prop'u kod ifadesiyse ve
664
+ ref'i **tam olarak** `{{ item.<alan> }}` kalıbına uyuyorsa, kullanıcı o alanı değiştirdiğinde
665
+ runtime değeri `rows.data[<index>].<alan>`'a geri yazar
666
+ (`repeater.handler.ts` → `handleRepeaterItemPropertySet`, regex `^\{\{\s*item\.(.+?)\s*\}\}$`).
667
+ Yani `value: {type:'code', ref:'{{ item.answer }}'}` yazmak "cevabı satıra kaydet" demektir.
668
+ **Kalıp bozulursa geri yazma sessizce çalışmaz:** `{{ item.answer || '' }}` ya da
669
+ `{{ item["answer"] }}` regex'e uymaz — okur ama yazmaz.
670
+ - **Toplu okuma flow'da `<repeaterAdı>.rows`'tur.** `microservice` adımının `value.type:'code'`
671
+ parametresinde ve `condition` adımının `expression`'ında komponentlere adıyla erişilir, yani
672
+ `JSON.stringify((q_list.rows || []).map(…))` doğrudan çalışır. Kod bağlamında satırlar
673
+ **ham** gelir (Prop sarımı `mapSchemaPropToRawProp` ile her seviyede açılır); `machine.getNode`
674
+ ile bakarsan `{data,type}` sarılı görünür — ikisini karıştırma.
675
+ - **Satır kimliği:** `getRepeaterRowId` sırayla `row._id` / `row.id` / index'e bakar. Satırlara
676
+ **kalıcı bir `id` ver**; yoksa index'e düşer ve liste değişince girilen değerler kayar.
677
+
678
+ **Satır tipine göre FARKLI alan göstermek — aynı ızgara hücresi + `isHidden`.** Dört varyantı
679
+ alt alta dizersen gizli olanların satırları boş kalır (gizli düğüm hiç çizilmez ama grid
680
+ koordinatı sabittir → dev boşluk). Doğrusu **hepsini aynı `position`'a koymaktır**; zaten aynı
681
+ anda yalnız biri görünür. Bu güvenlidir çünkü **`page_schema_patch` çakışma normalizasyonu
682
+ UYGULAMAZ** — koordinatları aynen kaydeder (aşağı itme yalnız `page_create` / `page_update`
683
+ / `layout_update` yolundadır, Kural 1). Aynı sebeple bu deseni komple şema yazarak kuramazsın.
684
+
685
+ > **`isHidden` her komponentte YOK.** Renderer `{#if !props.isHidden}` ile *her* düğümü
686
+ > geçiriyor, ama prop sözlüğünde tanımlı değilse doğrulama duvarı `unknown-prop` ile reddeder.
687
+ > Ölçüm: `radio`, `textarea`, `container`, `form`, `button` **var**; `text` ve `multi-select`
688
+ > **yok**. Çözüm: gizlenecek komponenti `isHidden`'ı olan bir `container`'a sar ve koşulu
689
+ > sarmalayıcıya bağla (yukarıdaki `box_multi`). Sarmalayıcıyı **`isWrapper:false`** kur
690
+ > (Kural 1f: wrapper = `min-height:80vh`) ve çerçevesini `style` ile kapat:
691
+ > `{ background:'transparent', border:'none', '&__body': { padding:'0px' } }`.
692
+ > `appearance.isHidden` diye bir kaçış yolu YOKTUR — runtime onu hiç okumaz.
693
+
694
+ > **Satır BAŞINA doğrulama kurulamaz.** `validations` komponentin kendisindedir, tüm satırlar
695
+ > onu paylaşır — "3. soru zorunlu, 4. soru değil" şemayla ifade edilemez. Zorunluluk satır
696
+ > verisinde taşınır (`required`) ve gönderim akışının ilk `condition` adımında denetlenir:
697
+ > `(q_list.rows||[]).filter(r => r.required && (r.answer==='' || (Array.isArray(r.answer) && !r.answer.length))).length > 0`.
698
+
699
+
700
+ > **⚠ İÇ İÇE REPEATER YAZMA İÇİN KULLANILAMAZ (ölçüldü, 2026-08-19).** Repeater içinde ikinci bir
701
+ > repeater (`rows: {{ item.options }}`) **okumada çalışır** — dış satır başına doğru sayıda iç satır
702
+ > çizilir, değerler doğru gelir. Ama:
703
+ > - **Geri yazma ölü:** iç girdiyi değiştirdiğinde dış satırın alanı DEĞİŞMEZ
704
+ > (`p_outer.rows.data[0].options[0].label` eski kaldı) ve iç repeater'ın `_repeaterValues`'ü
705
+ > `null`. Sebep yapısal: şemada iç repeater **tek bir düğümdür**, dış satır başına ayrı bir düğüm
706
+ > yoktur; `_repeaterValues` anahtarı iç satır id'sine göredir ve dış satırlar arasında çakışır.
707
+ > - **DOM bozuluyor:** tek bir düzenlemeden sonra girdi sayısı 3 → **19**'a çıktı, değerler karıştı.
708
+ > - Ekranda sürekli `Expression error: Cannot read properties of null (reading '0')` toast'ı çıkıyor
709
+ > (iç repeater bir kez `item` olmadan da değerlendiriliyor).
710
+ >
711
+ > **Gateway bunu `PAGE-013` ile istek gitmeden reddediyor** (`page_create` / `page_update(schema)` ve
712
+ > `page_schema_patch`'te op'lar UYGULANDIKTAN SONRAKİ ağaca bakarak).
713
+ >
714
+ > **Yapılacak:** satır içi listeyi tek bir `textarea`'da **satır başına bir öğe** olarak topla
715
+ > (`options_text`), kaydederken `split('\n')`'le böl. Ölçülmüş çalışan sürüm: anket editörü
716
+ > (`anket-admin/anket-duzenle`). Gerçekten satır satır düzenleme gerekiyorsa iç listeyi AYRI bir
717
+ > sayfada/modalda yönet — iç içe repeater bunu karşılamıyor.
718
+
719
+ > **Repeater satırını akıştan silmek/eklemek:** repeater ÇOCUĞUNDAKİ bir butonun click akışında
720
+ > `RowIndex` doğru satırı verir, ve hedef repeater'ın KENDİSİ olduğu için `rows`'a yazmak serbesttir
721
+ > (`isRepeaterSelf` dalı satır-değeri yoluna girmez). Ölçülmüş desen:
722
+ > `q_list.rows = (q_list.rows || []).filter(function(r, i){ return i !== RowIndex; })`.
723
+ > Ekleme aynı şekilde `.slice()` + `push` ile yapılır; satır numarasını ekledikten sonra yeniden ata.
724
+
725
+ **Gönderimi saklamak:** satır başına normalize kayıt yerine tek `TEXT` kolonda anlık görüntü
726
+ (`answers_json`) tutmak bu desende en pratik yoldur — yazarken `JSON.stringify`, okurken platform
727
+ kendisi nesneye çevirir; Studio tablosunda `[object Object]` görünmesi normaldir ve veri sağlamdır
728
+ (`platform_guide('entity')` → "TEXT kolonunda JSON"). Ekranın görsel tarafı için **Kural 1h**:
729
+ tema `--ng-*` değişkenleriyle tek yerden çevrilir, `html` kaçış kapısına gerek kalmaz.
730
+
731
+ ## Kural 1h — Tasarım: `style` prop'u ve tema değişkenleri
732
+
733
+ Her komponentin `style` prop'u vardır ve emotion `css()` bloğudur: iç seçiciler, pseudo-class'lar
734
+ ve **CSS değişkeni tanımı** çalışır, üretilen sınıf komponente scope'ludur. Ekranı "modern"
735
+ yapmanın yolu budur — ölçülmüş kurallar (anket-doldur, 2026-08-19):
736
+
737
+ > Bu kural TEMA/GÖRSELLİK içindir. Çocukların dizilimi (ortalama, yaslama, aralık, "kalanı
738
+ > doldur") `style` ile değil kapsayıcının kendi `layout` prop grubuyla kurulur — Kural 1f-2.
739
+
740
+ **1. Temayı tek yerden çevir: `page_container.style` içinde `--ng-*` değişkenlerini ez.**
741
+ Runtime bütün renk/tipografi/form tokenlarını CSS değişkeni olarak basar
742
+ (`Theme.svelte`: `--ng-accent-primary`, `--ng-accent-hover`, `--ng-background-surface`,
743
+ `--ng-border-primary`, `--ng-rounded`, `--ng-font`, `--ng-form-border-focus`,
744
+ `--ng-form-radius-md`, `--ng-form-height-md`…). Bunları kök kapsayıcının `style`'ında yeniden
745
+ tanımlarsan **sayfadaki her komponente miras kalır**:
746
+
747
+ ```jsonc
748
+ { "op": "replace_prop", "target": "page_container", "prop": "style", "value": {
749
+ "--ng-accent-primary": "#4F46E5", "--ng-accent-hover": "#4338CA",
750
+ "--ng-form-border-focus": "#4F46E5",
751
+ "background": "#F1F5F9",
752
+ "& > .ng-container__body > .ng-container__slot": {
753
+ "maxWidth": "720px", "margin": "0 auto", "padding": "48px 20px 80px", "height": "auto" } } }
754
+ ```
755
+ Ölçüldü: butonun dolgusu tek satırda `rgb(79,70,229)`'a döndü (buton stilini hiç yazmadan) ve
756
+ içerik sütunu 720px'te ortalandı. **İçeriği ortalamak için `.ng-container__slot`'u hedefle** —
757
+ `.ng-app__node-children`'ın `padding`'i renderer tarafından SATIR İÇİ yazılır, sınıfla ezilmez.
758
+
759
+ **2. Alt parça seçicileri `&__parça` şeklindedir ve komponent markup'ıyla eşleşir.** Komponent
760
+ kendi elemanlarına `class="{emotionSınıfı}__parça"` basar; `style` içinde `"&__item"` yazınca
761
+ emotion `.css-xxx__item` üretir ve tutar. Hangi parçaların olduğu `component_get`'in
762
+ `style` prop'unun alt listesindedir (`radio` → `&__items · &__item · &__item-input ·
763
+ &__item-text · &__label · &__description · &__error`). Pseudo-class, `::before/::after`,
764
+ kardeş ve iç seçiciler de yazılabilir: `"&__item:hover &__item-text"`,
765
+ `"&__item-input:checked + &__item-text"`, `"&__item-text::before"`.
766
+
767
+ **3. ⚠ Platform `style` anahtarlarının SIRASINI korumaz.** Prop sözlüğü `Dictionary` olarak
768
+ saklanıp yeniden serileştiriliyor; `appliedPatch` çıktısında `ref` ile `data` anahtar sırasının
769
+ farklı olduğunu görebilirsin. Sonuç: **aynı özgüllükteki iki kendi kuralın arasında "sonra yazan
770
+ kazanır" GARANTİ DEĞİLDİR.** Ölçüm: `&__item` ile `&__item:has(...)` arasında yalnız bazı
771
+ bildirimler geçti. Çözüm — durum kurallarını **özgüllükle** ayır
772
+ (`&__item-input:checked + &__item-text` = 0,2,0 > `&__item-text` = 0,1,0) ya da kritik
773
+ bildirimlere `!important` koy.
774
+
775
+ **4. Komponentin kendi boyut kuralları seninkinden ÖZGÜLDÜR.** `radio`/`textinput` vb.
776
+ `--sm/--md/--lg` modifier'ları `.css-x--md .css-x__item-text { padding-left: … }` gibi (0,2,0)
777
+ kurallar yazar; senin `&__item-text` kuralın (0,1,0) bunu ezemez. Ölçüm: `padding` verdim,
778
+ `padding-left` 22.4px kaldı (dot ile metin çakıştı); `!important` ile 42px'e oturdu. Kural:
779
+ komponentin varsayılanının üstüne yazarken ya `&--md &__parça` yaz ya da `!important` kullan.
780
+
781
+ **5. Gizlenecek/koşullu görsel durumlarda `:has()`'e bel bağlama.** `:has(input:checked)`
782
+ seçicisi doğru eşleşiyor (`element.matches()` true) ama işaretleme çalışma anında değiştiğinde
783
+ tarayıcı üst elemanı yeniden stillendirmeyebiliyor — ölçümde yalnız kalıtılan `color` güncellendi,
784
+ `background`/`border`/`box-shadow` eski kaldı (aynı bayatlama platformun KENDİ radyo noktasında da
785
+ görüldü: `input:checked + span::after { opacity: 1 }` uygulanmadı). Sağlam yol kardeş seçicidir:
786
+ işareti taşıyan input'un **sonraki kardeşini** boya (`&__item-input:checked + &__item-text`).
787
+
788
+
789
+ ### 1h-2 — Tema değişkeni envanteri (`Theme.svelte`, tamamı)
790
+
791
+ Bunlar **runtime'da her sayfada tanımlıdır**; `style` içinde yeniden tanımlayarak eziyorsun.
792
+ Varsayılan palet `white.ts` (accent `#0052CC`, border `#dfe1e6`, text `#262626`) — yani hiçbir şey
793
+ yapmazsan ekran "platform mavisi" görünür; marka rengine çevirmek tek satırdır.
794
+
795
+ | Küme | Değişkenler |
796
+ |---|---|
797
+ | Vurgu | `--ng-accent-primary` · `--ng-accent-hover` · `--ng-accent-muted` · `--ng-accent-subtle` |
798
+ | Zemin | `--ng-background-canvas` · `--ng-background-surface` · `--ng-background-surface-alt` · `--ng-background-readonly` · `--ng-background-disabled` · `--ng-background-overlay` |
799
+ | Metin | `--ng-text-primary` · `--ng-text-secondary` · `--ng-text-disabled` |
800
+ | Çerçeve / köşe / gölge | `--ng-border-primary` · `--ng-border-secondary` · `--ng-rounded` · `--ng-shadow-none/sm/md/lg` |
801
+ | Tipografi | `--ng-font` · `--ng-font-size` · `--ng-font-weight` |
802
+ | Boşluk | `--ng-spacing` · `--ng-spacing-small` · `--ng-spacing-large` |
803
+ | Durum | `--ng-status-success` · `--ng-status-warning` · `--ng-status-error` · `--ng-status-info` |
804
+ | Form (girdilerin tamamı) | `--ng-form-color` / `-disabled` / `-placeholder` / `-invalid` · `--ng-form-background` / `-disabled` / `-readonly` · `--ng-form-border` / `-focus` / `-disabled` / `-invalid` · `--ng-form-radius-sm/md/lg` · `--ng-form-height-sm/md/lg` · `--ng-form-padding-x-sm/md/lg` · `--ng-form-font-family` · `--ng-form-font-size-sm/md/lg` · `--ng-form-label-color` / `-font-weight` / `-font-size-*` / `-margin-bottom-*` · `--ng-form-default-size` |
805
+
806
+ **Önce token'ı ez, sonra komponent stiline in.** `--ng-form-radius-md`'yi 10px yapmak sayfadaki
807
+ BÜTÜN girdileri yuvarlar; aynı şeyi komponent komponent `&__input { borderRadius }` yazarak
808
+ yapmak hem uzun sürer hem de yeni komponent eklendiğinde tutarsızlık üretir.
809
+
810
+ ### 1h-3 — Ölçülmüş hazır bloklar (anket ekranından, olduğu gibi kullanılabilir)
811
+
812
+ ```jsonc
813
+ // 1) Sayfa kabuğu — tema + ortalanmış içerik sütunu [page_container.style]
814
+ { "--ng-accent-primary": "#4F46E5", "--ng-accent-hover": "#4338CA",
815
+ "--ng-form-border-focus": "#4F46E5",
816
+ "background": "#F1F5F9",
817
+ "fontFamily": "'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
818
+ "& > .ng-container__body": { "overflowY": "visible" },
819
+ "& > .ng-container__body > .ng-container__slot": {
820
+ "maxWidth": "720px", "margin": "0 auto", "padding": "48px 20px 80px", "height": "auto" } }
821
+
822
+ // 2) Kart — beyaz yüzey, ince çerçeve, hover'da yükselen gölge [container.style, isWrapper:false]
823
+ { "background": "#FFFFFF", "border": "1px solid #E2E8F0", "borderRadius": "16px",
824
+ "boxShadow": "0 1px 2px rgba(15, 23, 42, 0.04)",
825
+ "transition": "border-color 140ms ease, box-shadow 140ms ease",
826
+ "&:hover": { "borderColor": "#CBD5E1", "boxShadow": "0 6px 16px rgba(15, 23, 42, 0.07)" },
827
+ "&__body": { "padding": "22px 24px 24px", "overflowY": "visible", "height": "auto" } }
828
+
829
+ // 3) Radyo → tıklanabilir seçenek satırları (46px hedef, seçilince indigo) [radio.style]
830
+ // padding/lineHeight'ta !important ŞART: komponentin --md kuralı daha özgül (madde 4).
831
+ { "marginTop": "16px",
832
+ "&__items": { "gap": "8px" },
833
+ "&__item": { "display": "block", "cursor": "pointer" },
834
+ "&__item-text": { "display": "block", "border": "1px solid #E2E8F0", "borderRadius": "10px",
835
+ "padding": "12px 14px 12px 42px !important", "lineHeight": "1.45 !important",
836
+ "background": "#FFFFFF", "fontSize": "15px", "color": "#334155",
837
+ "transition": "border-color 120ms ease, background-color 120ms ease, box-shadow 120ms ease" },
838
+ "&__item-text::before": { "left": "15px" },
839
+ "&__item-text::after": { "left": "calc(15px + (var(--ng-form-font-size-md) * 1.1) / 2 + 1px)" },
840
+ "&__item:hover &__item-text": { "borderColor": "#C7D2FE", "background": "#F8FAFF" },
841
+ "&__item-input:checked + &__item-text": { "borderColor": "#4F46E5", "background": "#EEF2FF",
842
+ "boxShadow": "inset 0 0 0 1px #4F46E5", "color": "#1E1B4B", "fontWeight": "500" } }
843
+
844
+ // 4) Metin alanı — odak halkası [textarea.style]
845
+ { "marginTop": "16px",
846
+ "&__input": { "borderRadius": "10px", "border": "1px solid #E2E8F0", "padding": "12px 14px",
847
+ "fontSize": "15px", "lineHeight": "1.6", "color": "#0F172A", "background": "#FFFFFF",
848
+ "transition": "border-color 120ms ease, box-shadow 120ms ease" },
849
+ "&__input:focus": { "borderColor": "#4F46E5", "boxShadow": "0 0 0 3px rgba(79, 70, 229, 0.12)",
850
+ "outline": "none" } }
851
+
852
+ // 5) Çoklu seçim — dış sarmalayıcıya yazılır, iç kutu iner seçiciyle bulunur [multi-select.style]
853
+ { "& .ng-multiselect": { "borderRadius": "10px", "border": "1px solid #E2E8F0",
854
+ "minHeight": "44px", "background": "#FFFFFF" },
855
+ "& .ng-multiselect:focus-within": { "borderColor": "#4F46E5",
856
+ "boxShadow": "0 0 0 3px rgba(79, 70, 229, 0.12)" } }
857
+
858
+ // 6) Görünmez-kalabilen hata şeridi [container.style + isHidden akıştan çevrilir]
859
+ { "background": "#FEF2F2", "border": "1px solid #FECACA", "borderRadius": "10px",
860
+ "marginTop": "20px",
861
+ "&__body": { "padding": "12px 14px", "overflowY": "visible", "height": "auto" } }
862
+
863
+ // 7) Birincil buton — dolgu rengi token'dan gelir, burada sadece ölçü [button.style]
864
+ { "marginTop": "24px", "height": "48px", "borderRadius": "10px", "fontSize": "15px",
865
+ "fontWeight": "600", "paddingLeft": "30px", "paddingRight": "30px",
866
+ "boxShadow": "0 1px 2px rgba(15, 23, 42, 0.08)" }
867
+ ```
868
+
869
+ Ölçülen sonuç (1280×720): gövde 1292px, kartlar 680px genişlik / 14px ara, seçenek satırları
870
+ 46px, buton 48px; 375px'te yatay taşma yok, seçenekler 285px. Boş `text` komponenti ~24px yer
871
+ kaplar — yardım metni opsiyonelse bunu hesaba kat.
872
+
873
+ > **Erişilebilirlik notu:** seçenek satırını `label`'ın İÇİNDEKİ `span`'e boyamak tıklama
874
+ > alanını daraltmaz (`label` zaten sarmalayıcıdır) ama **odak halkasını da taşımanız gerekir** —
875
+ > `input` `display:none` olduğu için klavye odağı görünmez. Klavyeyle gezilecek ekranlarda
876
+ > `&__item-input:focus-visible + &__item-text` için de bir dış çizgi tanımlayın.
877
+
878
+ ## Kural 2 — Şema değişikliği `page_update` ile YAPILMAZ
879
+
880
+ İki ayrı yazma yolu vardır ve karıştırılmaz:
881
+
882
+ | Değişiklik | Tool | Neden |
883
+ |---|---|---|
884
+ | Komponent ekle/sil/taşı, prop, binding, event | `page_schema_patch` | Doğrulama duvarı + açık Studio canvas'ına SignalR canlı yayını + eşzamanlı yazar kilidi yalnız bu yolda çalışır |
885
+ | Ad, slug, browserTitle, layout, roller, middleware | `page_update` | Üstbilgi alanları micro-op sözlüğünde yok |
886
+
887
+ Platformun `PUT /pages/...` ucu **replace**'tir: gönderilmeyen `schema`/`events`/`roleIds`
888
+ alanları silinir. Gateway bu yüzden `page_update`'te sayfayı önce okur, yalnız verdiğin
889
+ alanları değiştirip tam gövdeyi geri yazar (`layout_update` ve `component_update` de aynı).
890
+ Yine de `schema` parametresini elle doldurursan okuduğun hâli değil, **verdiğin liste** yazılır.
891
+
892
+ ### Atomiklik: 500 KB'lık şemada tek komponente dokunmak
893
+
894
+ `page_schema_patch` şemanın tamamını göndermez — yalnız op'ları gönderir. Tam gövde ne LLM
895
+ bağlamına girer ne de tel üzerinden geçer; şemayı platform kendi belleğinde okur, op'ları uygular
896
+ ve geri yazar. Sözleşme (`PatchPageSchemaCommandHandler`):
897
+
898
+ | Aşama | Ne yapar |
899
+ |---|---|
900
+ | Kilit | Sayfa başına 60 sn'lik dağıtık kilit; başka bir aktör yazıyorsa istek `PageSchemaLockedException` ile döner (409) |
901
+ | Sürüm | `baseVersion` verdiysen `UpdatedAt.Ticks` ile karşılaştırılır; tutmazsa `PageSchemaVersionConflictException` (409) ve **hiçbir op uygulanmaz** |
902
+ | Uygulama | Her op hedefini ağaçta arar (`Find` → JSON Pointer) ve **yalnız o düğüme** dokunur; kardeşler, üst düğümler ve diğer alt ağaçlar bit düzeyinde korunur |
903
+ | Hep-ya-hiç | Op'lar önce bellekte uygulanır; **tek bir hata** (bilinmeyen op, bulunamayan hedef, tekrar eden elementId, doğrulama) tüm komutu düşürür — kısmi yazma yoktur |
904
+ | Doğrulama | Tam şema `PageSchemaValidator`'dan geçer (422 + alan bazlı `errors`) |
905
+ | Yazma | Tek transaction (`ITenantContextTransactionalCommand`) |
906
+ | Yayın | Açık Studio canvas'larına SignalR ile `page-schema-patched` |
907
+
908
+ Yanıt üç alan döner: `applied` (uygulanan op sayısı), **`appliedPatch`** (her op'un şemada tam
909
+ olarak hangi JSON Pointer'a ne yaptığı — RFC 6902 biçiminde; "gerçekten yalnız o komponente mi
910
+ dokunuldu" sorusunun kanıtı) ve **`newVersion`** (`UpdatedAt.Ticks`).
911
+
912
+ **Sürüm kontrolü platformda var ama gateway bugün KULLANMIYOR.** Platform gövdede `baseVersion`
913
+ (long tick) kabul eder ve tutmazsa 409 verir; `page_state.baseVersion` bu değeri döner.
914
+ `page_schema_patch` bu alanı geçirmez — bayat plan koruması gateway tarafında
915
+ `expectedUpdatedAt` ile yapılır: sayfa okunur, `audit.updatedAt` karşılaştırılır, tutmazsa istek
916
+ hiç gönderilmez. Bu kontrol **iki çağrı arasına başka bir yazar girerse yakalamaz**; gerçek
917
+ korumayı isteyen ham HTTP ile `baseVersion` göndermelidir.
918
+
919
+ > **Büyük şemada çalışma sırası:** `mode:'summary'` ile ağacı çıkar (props yok, 500 KB'lık sayfada
920
+ > bile küçük) → hedefin `elementId`'sini al → gerekiyorsa `mode:'subtree'` ile yalnız o dalı oku →
921
+ > op'ları `page_schema_validate` ile kuru koştur → `page_schema_patch`. `mode:'full'` yalnız
922
+ > gerçekten tüm şema gerektiğinde çağrılır.
923
+
924
+ > **`remove_component`'in yan etkisi — tek istisna.** Sayfa kaydedilirken artık hiçbir komponent
925
+ > olayının, prop'unun ya da sayfa olayının referans vermediği akışlar platformca **kalıcı olarak
926
+ > silinir** (`PageService.UpdateAsync` → `flowRepository.DeleteRangeAsync`). Yani akış bağlı bir
927
+ > komponenti silmek, o akış başka yerde kullanılmıyorsa akış kaydını da götürür. Gateway bunu
928
+ > yanıtta `flowWarnings` ile bildirir; akışı korumak istiyorsan silmeden önce başka bir komponente
929
+ > `set_event` ile bağla ya da `flow_get` ile gövdesini yedekle. Ayrıntılı analiz:
930
+ > `PatchPageSchemaCommandHandler` (backend) — kilit, sürüm ve hep-ya-hiç davranışının kaynağı.
931
+
932
+ ## Kural 3 — Flow yazımı barındırıcıyı da günceller
933
+
934
+ Platform `flow` uçlarında gövdede hem akışı hem barındırıcının tam güncelleme komutunu ister
935
+ (`{ flow, page | layout | component }`) — şemadaki `events[].value` = flow id bağı tek işlemde
936
+ tutarlı kalsın diye. **Silme bile gövdelidir.** Gateway barındırıcıyı okuyup gövdeye kendisi
937
+ koyar; sen yalnız akışı verirsin.
938
+
939
+ Akışı bir komponente bağlamak ayrı adımdır:
940
+
941
+ ```
942
+ flow_create(host:'page', hostId:<pageId>, name:'Save order', slug:'save-order', steps:[…])
943
+ page_schema_patch(ops:[{ op:'set_event', target:'button_1', event:'click', eventValue:<flow id> }])
944
+ ```
945
+
946
+ > Olay adı `component_get`'in `events` listesinden alınır ve **`on` öneki almaz**: buton `click`,
947
+ > form `submit`, tablo `mount`/`select`/`sort`. `onClick` yazarsan platform 200 döner ama bağ
948
+ > ölüdür — gateway bunu PAGE-008 ile istek gitmeden reddeder.
949
+
950
+ `flow_create`'e `flowId` verirsen id'yi önceden bilir, iki adımı tek planda kurabilirsin.
951
+
952
+ ### Sayfada bağlı akış varken YENİ akış eklenemez — ve çözümü
953
+
954
+ Bir sayfanın herhangi bir olayı zaten bir akışa bağlıysa, o sayfaya ikinci bir `flow_create`
955
+ FK ihlaliyle reddedilir:
956
+
957
+ ```
958
+ Geçersiz istek: This record is in use. Please remove its relations first.
959
+ ```
960
+
961
+ Bu yüzden **kuruluş sırası bağlayıcıdır** — önce şema, sonra **bütün** akışlar, en son **tek**
962
+ patch'te bütün `set_event`'ler:
963
+
964
+ ```
965
+ page_create → page_schema_patch (tüm add_component)
966
+ → TÜM flow_create'ler
967
+ → TEK page_schema_patch (bütün set_event'ler)
968
+ ```
969
+
970
+ Sonradan akış eklemen gerekirse sayfayı baştan kurmana gerek yok — **bağı geçici olarak çöz**:
971
+
972
+ ```jsonc
973
+ 1) page_schema_patch [{ "op":"set_event", "target":"page_container", "event":"mount", "eventValue": null }]
974
+ 2) flow_create × N
975
+ 3) page_schema_patch [ bütün set_event'ler tek seferde ]
976
+ ```
977
+
978
+ `eventValue: null` olay kaydını bırakıp değerini boşaltır ve kilidi açar.
979
+
980
+ > **Aynı kilit LAYOUT'ta da vardır** (ölçüm 2026-08-18: menüye yeni bağlantı eklerken
981
+ > `POST /layouts/<id>/flows` aynı hatayı verdi). Layout'un olayları komponentin kendi `events`
982
+ > dizisinde satır içidir (`{ "name": "click", "value": "<flow id>" }`) ve `page_schema_patch`
983
+ > layout'a uygulanmaz; çözüm de bu yüzden farklı yazılır:
984
+ >
985
+ > ```
986
+ > 1) layout_update → tüm komponentlerde "events": [] (bağları çöz)
987
+ > 2) flow_create × N
988
+ > 3) layout_update → tam ağaç + bütün events'ler
989
+ > ```
990
+ >
991
+ > Sıfırdan kurarken kilide takılmazsın (akışları bağlamadan önce yaratırsın); kilit yalnız
992
+ > **mevcut ve bağlı** bir layout'a yeni akış eklerken çıkar.
993
+
994
+ ## Kural 4 — Komponent güncellemesi slug ile, silme id ile
995
+
996
+ `component_update` platformdaki **tek** slug'la çalışan update ucudur
997
+ (`PUT components/{applicationId}/{slug}`); `component_delete` id alır. Widget kaydında
998
+ (`kind:'widget'`) `widgetType` (`container` | `modal` | `drawer`) zorunludur.
999
+
1000
+ `component_list`/`component_get` iki işi görür: (1) sayfaya konabilecek tipleri ve prop
1001
+ sözlüğünü okumak — şemaya **uydurma prop yazma**, (2) kendi yazdığın komponentleri yönetmek.
1002
+ `scope:'all'` platformun varsayılan kataloğunu getirir.
1003
+
1004
+ ## Kural 5 — Layout: şema alanı `views` ve MUTLAKA bir `slot` içerir
1005
+
1006
+ `layout_create/update` komponent ağacını `views` alanında taşır (`schema` değil) ve layout'un
1007
+ micro-op hattı **yoktur** — değişiklik tam gövde PUT'tur (gateway merge eder).
1008
+
1009
+ **Slot şart:** sayfa bir layout'a bağlandığında render şeması **layout'un views'ü** olur ve
1010
+ sayfanın kendi içeriği YALNIZ layout'taki `type: 'slot'` komponentinin içine yerleştirilir
1011
+ (Studio `page.mappers.ts`: `mapPageLayoutToPreview` sayfa şemasını slot'un `children`'ına koyar).
1012
+ Slot'suz bir layout'a bağlanan her sayfa — kaydı kusursuz olsa da — **Studio'da ve runtime'da
1013
+ boş görünür**; kayıt tarafında hiçbir hata çıkmaz (kütüphane turu R1). Gateway slot'suz
1014
+ `views`'te LAY-002 uyarısı verir. İskelet:
1015
+
1016
+ ```jsonc
1017
+ "views": [
1018
+ { "elementId": "app_header", "kind": "component", "type": "header", "props": { … },
1019
+ "blocks": [{ "breakpoint": 1024, "size": { "h": 8, "w": 12 }, "position": { "x": 0, "y": 0 } }] },
1020
+ { "elementId": "content_slot", "kind": "component", "type": "slot", "name": "İçerik",
1021
+ "props": {}, "blocks": [{ "breakpoint": 1024, "size": { "h": 80, "w": 12 }, "position": { "x": 0, "y": 8 } }] }
1022
+ ]
1023
+ ```
1024
+
1025
+ Doğrulama: `page_state` çıktısındaki `layoutSlots` — **boşsa sayfa içeriği görünmeyecek** demektir.
1026
+
1027
+ > Layout gövdesindeki `parameters` alanını **boş bırak**: domain'de saklanır ama Studio'da
1028
+ > editörü yoktur ve runtime'da okunan bir tüketicisi tespit edilmedi (22 E-8a).
1029
+
1030
+ ## Kural 6 — Değişkenler: tek kayıt, `state.page.<key>` ile okunur
1031
+
1032
+ `set_binding` ifadeleri yalnız `page_state`'in döndürdüğü değişken/komponent/microservice
1033
+ isimlerine bağlanır.
1034
+
1035
+ **Platform değişkenleri tek tek değil SET olarak tutar:** uygulama başına bir kayıt, sayfa başına
1036
+ bir kayıt (`variable_create` `pageId` verirsen sayfa, vermezsen uygulama düzeyi). Kayıt zaten varsa
1037
+ create update'e döner. Şekil `[{ key, value }]`; `value` **herhangi bir JSON** olabilir:
1038
+
1039
+ ```jsonc
1040
+ variable_create({ applicationId, pageId, variables: [
1041
+ { "key": "MyTest", "value": { "isActive": true } },
1042
+ { "key": "sayfaBaslik", "value": "Etkinlikler" }
1043
+ ]})
1044
+ ```
1045
+
1046
+ - Uç seti **komple değiştirir**; gateway mevcut seti okuyup anahtar bazında birleştirir. Kasten
1047
+ silmek için `replace: true`. `variable_delete` **setin tamamını** siler.
1048
+ - Platform `data`'yı JSON **string** olarak saklar (`"[{\"key\":…}]"`); gateway çeviriyi iki yönde
1049
+ de yapar — `variable_list` sana `[{key, value}]` döndürür. Ham HTTP kullanıyorsan string
1050
+ zorunludur: gövdede yalnız `data` alanı okunur, `name`/`type`/`defaultValue` gibi alanlar
1051
+ **sessizce yok sayılır** ve kayıt boş yazılır (istek yine 200 döner).
1052
+
1053
+ **Okuma yolu — sayfada `state.page.<key>`, uygulama düzeyinde `state.app.<key>`:**
1054
+
1055
+ ```jsonc
1056
+ // prop'u doğrudan ifadeye bağlama (flow gerekmez):
1057
+ "text": { "type": "code",
1058
+ "data": "Button text",
1059
+ "ref": "{{ state.page.MyTest.isActive == true ? 'Test' : 'FalseTest' }}" }
1060
+ ```
1061
+
1062
+ `type: 'code'` prop'unda **ifade `ref` alanındadır ve `{{ }}` ile sarılır**; `data` tasarım
1063
+ zamanı/yedek değeri taşır. Aynı erişim flow `code` adımlarında ve `value.type:'state'`
1064
+ ref'lerinde de geçerlidir (`platform_guide('expressions')` §2-3). `value` bir nesneyse alanlarına
1065
+ `state.page.<key>.<alan>` ile inilir.
1066
+
1067
+ **Kod prop'ları CANLI bağdır (reaktif).** Runtime, `type:'code'` + `ref` taşıyan HER prop'u
1068
+ otomatik olarak canlı bağa çevirir: ifadenin okuduğu state/komponent değeri her değiştiğinde
1069
+ prop KENDİLİĞİNDEN yeniden hesaplanır — güncellemek için flow kurmak gerekmez (kaynak:
1070
+ zeyrek-frontend `initialization.handler.ts` → `executeStateBindings`, her prop yazımından sonra).
1071
+ Tercih kuralı: *"bu değer başka bir değerden TÜRETİLİYORSA"* `set_binding` / kod prop'u kullan;
1072
+ *"bir OLAY sonucunda bir kez yazılacaksa"* (uçtan veri çekme gibi) flow + setter kullan.
1073
+ İkisini aynı prop'ta karıştırma — setter'ın yazdığı değeri canlı bağ bir sonraki tetikte ezer.
1074
+
1075
+ **`stateBindings[]` (şema düğümündeki dizi)** Studio'nun state-bağımlılık özelliğidir:
1076
+ `[{ name, expression, targets: [{ elementId, elementProperty }] }]` — bir ifade değişince
1077
+ listedeki hedef prop'lara yazılır. Micro-op sözlüğünde karşılığı YOKTUR; yalnız
1078
+ `add_component` gövdesinde ya da `page_update(schema)` ile yazılabilir. Aynı işi kod prop'u
1079
+ zaten gördüğü için yeni kurulumda tercih etme; mevcut sayfada görürsen silme.
1080
+
1081
+ ## Kural 7 — Sayfa arketipleri: liste tek başına uygulama değildir
1082
+
1083
+ Bir uygulama, tabloların listelendiği ekranlar toplamı değildir. **CRUD ucu yazdıysan o kaydın
1084
+ yönetim ekranını da kur.** Kullanıcı kavramı varsa giriş ekranı olmadan sistem kullanılamaz.
1085
+ Aşağıdaki dört arketip `qm` projesinin canlı kayıtlarından çıkarıldı (140 sayfa aynı desende);
1086
+ ayrıntılı kanıt ve kayıt id'leri: `zeyrek-mcp/docs/18-sayfa-arketipleri.md`. **Tam gövdeler
1087
+ altın örneklerde** — şemayı ve akışları oradan kopyalayıp uyarla:
1088
+
1089
+ | Arketip | Örnek |
1090
+ |---|---|
1091
+ | Liste | `ngapps://examples/flow-load-table` |
1092
+ | Form (create+edit) | `ngapps://examples/page-form-upsert` |
1093
+ | Liste → form geçişi | `ngapps://examples/flow-list-to-form` |
1094
+ | Giriş | `ngapps://examples/page-sign-in` |
1095
+ | Detay / profil | `ngapps://examples/page-detail-profile` |
1096
+ | Dinamik form (alanlar veriden) | `ngapps://examples/page-dynamic-form` (Kural 1g + 1h) |
1097
+
1098
+ > Gateway iki yarım kurulumu yakalar: **FORM-001** (`form` eklendi, `submit` bağlanmadı) ve
1099
+ > **FORM-002** (akışta POST/PUT adımı var ama hiç `source:'body'` parametresi yok → gövde boş
1100
+ > gider). `project_overview` ayrıca proje düzeyi boşlukları döndürür (`coverageGaps`): kimlik
1101
+ > middleware'i var ama giriş sayfası yok, yazma ucu var ama sayfa yok.
1102
+
1103
+ **Adlandırma:** çoğul = liste sayfası, tekil = form sayfası (`Departmanlar` / `Departman`).
1104
+ Sayfa çiftini bu şekilde kur.
1105
+
1106
+ ### 7a — Form sayfası (create + edit AYNI sayfa)
1107
+
1108
+ ```
1109
+ page_container
1110
+ ├── breadcrumb · notification
1111
+ ├── form[ text · form-validation-errors · textinput · textarea · dropdown … ]
1112
+ └── container[ container[ btn_save · btn_cancel ] · container[ audit-basic ] ]
1113
+ ```
1114
+
1115
+ `form` komponenti `submit` olayı yayar; `errorElementId` prop'unu sayfadaki
1116
+ `form-validation-errors` komponentine bağla, input'ların `validations`'ı (required/minLength/
1117
+ maxLength/pattern — sözlük `component_get`'te) orada listelenir.
1118
+
1119
+ **Validasyon yazma şekli — DÖRT alan da zorunludur:** `{ type, message, value, isActive }`.
1120
+
1121
+ ```jsonc
1122
+ { "op": "set_validations", "target": "in_email", "validations": [
1123
+ { "type": "required", "message": "E-posta zorunludur.", "value": true, "isActive": true },
1124
+ { "type": "email", "message": "Geçerli bir e-posta girin.", "value": true, "isActive": true },
1125
+ { "type": "max-length", "message": "En fazla 120 karakter.", "value": 120, "isActive": true }
1126
+ ] }
1127
+ ```
1128
+
1129
+ Geçerli `type` değerleri: `required` · `email` · `regex` · `max-length` · `min-length` ·
1130
+ `max-value` · `min-value` · `value-options`. Üç tuzak ölçüldü (crm koşumu B-9):
1131
+
1132
+ | Yanlış gövde | Ne olur |
1133
+ |---|---|
1134
+ | `{ type, isActive }` (mesaj/değer yok) | Kullanıcı neyin yanlış olduğunu göremez; eksik alanlı gövde sayfayı okunamaz bırakabilir (doküman 23 §B-2). Gateway **PAGE-016** ile reddeder. |
1135
+ | `{ name, value, message }` (`type` yok) | `ValidationModel.Name` **JsonIgnore**'dur: tip enum varsayılanına düşer ve alan SESSİZCE `regex` doğrulamasına döner. |
1136
+ | `{ type: "maxLength" }` (deve harfli) | Backend eski enum üyesiyle kabul edip geri okur ama **runtime doğrulayıcı yalnız tireli adları tanır** (`validation.ts` switch → `default: return true`): doğrulama hiç koşmaz. |
1137
+
1138
+ Gateway ilk iki ve üçüncü durumdan düzeltebildiklerini düzeltir (`name` → `type`, `maxLength` →
1139
+ `max-length`, eksik `isActive` → `true`) ve `gatewayNormalized` ile bildirir; düzeltilemeyen
1140
+ (mesajsız / tanınmayan tip / değersiz eşik) gövdeyi **PAGE-016** ile yazmadan reddeder.
1141
+ Validasyon ya `add_component` gövdesinde baştan verilir ya da sonradan `set_validations` ile
1142
+ değiştirilir.
1143
+
1144
+ Op **listeyi tamamen değiştirir** (boş dizi hepsini siler) ve yalnız o düğümün `validations`
1145
+ alanına dokunur. **Komponenti silip yeniden eklemeyin:** `remove_component` + `add_component`
1146
+ turu o komponente bağlı setter hedeflerini koparır (Kural 1e sonundaki not) ve komponentin tek
1147
+ referans verdiği akışları kalıcı olarak sildirir (Kural 2'deki yan etki notu).
1148
+
1149
+ **Kip ayrımı query parametresidir:** aynı sayfa `?id=<kayıt>` ile edit, parametresiz create.
1150
+
1151
+ `mount` akışı (`page_container` ya da `form` üzerine bağlanır):
1152
+
1153
+ ```
1154
+ start → [setter] page_container.isLoading = true
1155
+ → [condition] Page.queryParams.id !== undefined
1156
+ ├─ always → [setter] page_container.isLoading = false
1157
+ ├─ if → [setter] EDIT etiketleri (buton metni, breadcrumb.currentTitle, audit görünür)
1158
+ │ → [microservice] get (id query-string'den) → [setter] alanları doldur
1159
+ └─ else → [setter] CREATE etiketleri
1160
+ ```
1161
+
1162
+ > **Condition adımı `always` dalını kural dallarıyla BİRLİKTE taşır.** `always` her hâlükârda koşar
1163
+ > (yükleniyor bayrağını kapatmanın yolu budur), kural dalları (`branch.name` = **rule id**) eşleşene
1164
+ > göre. `rules`: `{ id, expression, type:'if' }` + `{ id, expression:'', type:'else' }`.
1165
+ >
1166
+ > **`rules[].id` GERÇEK bir GUID olmalı** — okunabilir bir ad (`"rule_ok"`) verirsen istek
1167
+ > `The JSON value could not be converted to System.Guid. Path: $.flow.steps[N].args.rules[0].id`
1168
+ > ile reddedilir. Dal adı da o GUID'in kendisidir (`branch.name === rule.id`), yani iki yerde de
1169
+ > okunabilir ad kullanamazsın.
1170
+ >
1171
+ > **Kural ifadesi patlarsa akış hata vermez, kuralı EŞLEŞMEMİŞ sayar** ve sessizce `else`'e
1172
+ > gider. Bu yüzden condition ifadeleri guard'lı yazılır — özellikle `Page.user` okuyanlar
1173
+ > (Kural 8).
1174
+
1175
+ **İki ayrı ucun sonucunu tek tabloya yazmak:** `mapping` tek kaynak alır, o yüzden birleştirmeyi
1176
+ `value.type: 'code'` ile yap — ilgisiz kayıtları da orada süz:
1177
+
1178
+ ```jsonc
1179
+ { "type": "change", "element": "tbl", "property": "rows", "source": "component",
1180
+ "mapping": { "source": "", "mappings": {} },
1181
+ "value": { "type": "code", "ref": "[].concat(Steps.load_a?.data || [], Steps.load_b?.data || []).filter(function (r) { return !!r.request_id; }).map(function (r) { return { … }; })" } }
1182
+ ```
1183
+
1184
+ > Bir tablo birden çok kayıt tipini besliyorsa (aynı `approval_steps` hem izin hem avans
1185
+ > kademesi tutuyor gibi) **ilgisiz tipi süzmek şart** — süzülmezse karşı tipin satırları
1186
+ > arayüzde **boş satır** olarak belirir ve hiçbir hata çıkmaz.
1187
+
1188
+ Alanları doldurma — kaynak adımın **ADI**, ilişki alanında **`.id`**:
1189
+
1190
+ ```jsonc
1191
+ { "type": "change", "element": "name_f7bc", "property": "value", "source": "component",
1192
+ "mapping": { "source": "", "mappings": {} },
1193
+ "value": { "type": "storage", "ref": "get_service.data.name" } }
1194
+ // ilişki (dropdown) → etiket değil id
1195
+ { "type": "change", "element": "manager_bb97", "property": "value", "source": "component",
1196
+ "mapping": { "source": "", "mappings": {} },
1197
+ "value": { "type": "storage", "ref": "get_service.data.manager.id" } }
1198
+ // audit kutusu tek prop'ta beslenir
1199
+ { "type": "change", "element": "auditbasic7f72", "property": "meta", "source": "component",
1200
+ "mapping": { "source": "", "mappings": {} },
1201
+ "value": { "type": "storage", "ref": "get_service.data.meta" } }
1202
+ ```
1203
+
1204
+ `submit` akışı:
1205
+
1206
+ ```
1207
+ start → [setter] btn_save.isLoading = true
1208
+ → [microservice] upsert (POST)
1209
+ → [condition] Steps.upsert_service.error
1210
+ ├─ always → [setter] btn_save.isLoading = false
1211
+ ├─ if → [setter] notification: kind='error', header, content, isHidden=false
1212
+ └─ else → [condition] Page.queryParams.id
1213
+ ├─ if (edit) → [setter] notification: kind='success', timeout=7000
1214
+ └─ else (create) → [redirect] { pageId:<liste>, path:'departmanlar' }
1215
+ ```
1216
+
1217
+ **Komponentten uca değer geçirme** — form kablolamasının tamamı bu şekildedir:
1218
+
1219
+ ```jsonc
1220
+ "parameters": [
1221
+ { "name": "id", "type": "number", "source": "query-string",
1222
+ "value": { "type": "code", "ref": "Page.queryParams.id" } },
1223
+ { "name": "name", "type": "string", "source": "body",
1224
+ "value": { "type": "property",
1225
+ "ref": { "source": "component", "element": "name_f7bc", "property": "value" } } }
1226
+ ]
1227
+ ```
1228
+
1229
+ - **Upsert deseni:** tek POST ucu hem create hem update yapar — `id` query-string'den, alanlar
1230
+ `source:'body'`'den. Entity başına iki uç yazmaya gerek yok.
1231
+ - **Hata yolunu `errorSteps` ile kurma**, `condition Steps.<adımAdı>.error` ile kur: `errorSteps`
1232
+ ana akışın kalanını hiç çalıştırmaz, yükleniyor bayrağı açık kalır.
1233
+ - Geri bildirim `notification` komponentiyle verilir (`kind` · `header` · `content` · `timeout` ·
1234
+ `showCloseButton` · `style`), `alert` adımıyla değil.
1235
+
1236
+ > **`isHidden`: ŞEMAYA yazmak ile SETTER ile yazmak AYRI yollardır.** Runtime tarafı ortak:
1237
+ > `RenderComponent` her düğümü `{#if !props.isHidden}` ile çizer, yani akıştaki setter
1238
+ > `isHidden` yazınca komponent katalogda o prop'u tanımlamasa bile gizlenir. **Şema yolu ise
1239
+ > katalogla sınırlıdır:** `props.isHidden`'ı düğüme yazmak, komponent onu tanımlamıyorsa
1240
+ > `unknown-prop` ile REDDEDİLİR (81 kaydın 36'sı tanımlar; ölçüm 2026-08-25). Tanımlamayanlar
1241
+ > arasında sık kullanılanlar: **`text` · `table` · `datatable` · `dropdown` · `multi-select` ·
1242
+ > `tag-input` · `tabs` · `checkbox` · `switch` · `switcher` · `card` · `modal` · `drawer` ·
1243
+ > `repeater` · `notification` · `toast` · `datepicker` ailesi · `breadcrumb-title`.**
1244
+ > Tek doğru kaynak `component_get` + `page_schema_validate` kuru koşusudur.
1245
+ >
1246
+ > **`appearance.isHidden` bir kaçış yolu DEĞİLDİR:** kaydedilir ama runtime hiçbir yerde okumaz
1247
+ > (renderer yalnız `props.isHidden`'a bakar) — komponent görünmeye devam eder. Başlangıçta gizli
1248
+ > göstermek için ya komponenti `isHidden`'ı OLAN bir `container`'a sar (Kural 1g) ya da
1249
+ > görünürlüğü sayfa açılış akışının setter'ıyla kur.
1250
+
1251
+ ### 7b — Liste → form geçişi
1252
+
1253
+ **Önce hangi tabloyu kullandığını bil — ikisi FARKLI kurulur:**
1254
+
1255
+ | | `table` | `datatable` |
1256
+ |---|---|---|
1257
+ | Yaydığı olaylar | `mount` · `select` · `doubleClick` · `filter` · `sort` · `pagination` … | **yalnız `mount`** |
1258
+ | Süzme/sıralama/sayfalama | her biri AYRI akışa bağlanır | komponent kendi prop'larını (`filters`/`sortField`/`sortType`/`offset`) yazıp `mount`'u yeniden tetikler → **tek akış yeter** |
1259
+ | Satır seçimi / çift tık | var (`self.selectedRow`) | **YOK** — `doubleClick` bağlamak 200 döner ama bağ ölüdür (gateway PAGE-008 keser) |
1260
+ | Kayıt açma | `doubleClick` akışı | satır **aksiyonu**: `actions: [{ text, kind, icon, flow: { type: 'flow', ref: <flowId> } }]` |
1261
+
1262
+ `datatable` satır aksiyonunda tıklanan satır komponentin `selectedRow` prop'una **komponent
1263
+ tarafından** yazılır, sonra akış çalışır (`Table.svelte:72` → `setNode` + `onEmit`): akışta
1264
+ `<elementId>.selectedRow.<alan>` ile okunur. `actions[].kind` sözlüğü **butonunkinden farklıdır**
1265
+ (`error · information · solid · transparent · dark · danger · warning`; butonda
1266
+ `success · error · info · primary · warning`) — yanlış sözlükten değer `invalid-enum-value` verir.
1267
+
1268
+ Aşağıdaki `doubleClick` tarifi **`table`** içindir. Geçişi `redirect` yapar ve
1269
+ **`query` bir İFADEDİR**:
1270
+
1271
+ ```jsonc
1272
+ // satıra çift tıkla → edit ; self = tetikleyen komponent
1273
+ { "stepId": "go_edit", "type": "redirect", "kind": "normal",
1274
+ "args": { "pageId": "<form sayfası id>", "path": "departman",
1275
+ "query": "`id=${self.selectedRow.id}`" },
1276
+ "branches": [{ "id": "<GUID>", "name": "always" }] }
1277
+
1278
+ // "Yeni" butonu → create (bağlamı taşıyarak; komponente ADIYLA erişim)
1279
+ { "args": { "pageId": "<form sayfası id>", "path": "departman",
1280
+ "query": "table?.selectedRow?.id ? `parent=${table.selectedRow.id}` : ''" } }
1281
+ ```
1282
+
1283
+ Sayfalar arası veri geçmenin yolu budur: gönderen `query`'ye yazar, alan `Page.queryParams.<ad>`
1284
+ ile okur. `variable_create`'e ihtiyaç yoktur.
1285
+
1286
+ ### 7c — Giriş sayfası (kimlik varsa ZORUNLU)
1287
+
1288
+ ```
1289
+ page_container
1290
+ └── auth-layout[ text · notification · form[ textinput · password · checkbox · button ] ]
1291
+ ```
1292
+
1293
+ ```
1294
+ start → [setter] btn.isLoading = true, btn.isDisabled = true, notification gizle
1295
+ → [microservice] sign-in (POST; email + password body'den)
1296
+ → [condition] !!Steps.signIn.data.token
1297
+ ├─ always → [setter] buton durumunu sıfırla
1298
+ ├─ if → [setcookie] { name:'APP_Token', value:'Steps.signIn.data.token',
1299
+ │ expiresInHours: 240 }
1300
+ │ → [redirect] hedef sayfaya
1301
+ └─ else → [setter] notification.content =
1302
+ { type:'code', ref:'Steps.signIn.data.message || "Giriş başarısız"' }
1303
+ ```
1304
+
1305
+ - **Cookie adı middleware'in okuduğu adla birebir aynı olmalı** (`cookie.APP_Token` —
1306
+ `platform_guide('middleware')` Tarif 1). Uyuşmazlık sessizce "hep 401" demektir.
1307
+ - `redirect` adımı `isRaw: true` iken `path` **çok satırlı JS bloğudur** (son ifade değeri döner) —
1308
+ "korunan sayfaya gitmek isterken login'e düşen kullanıcıyı geri gönderme" böyle kurulur.
1309
+ - Prop değeri nesne olabilir: `icon` → `{ "type": "static", "ref": { "name": "progress_activity" } }`.
1310
+ - Giriş sayfası kimlik işinin görünen yarısıdır. Sign-in ucu + auth middleware'i kurup bu sayfayı
1311
+ kurmamak, sistemi kullanılamaz bırakır.
1312
+
1313
+ ### 7d — Detay / profil sayfası
1314
+
1315
+ Form arketipinin salt-okuma akrabası: input'lar `isViewmode: true`, altında ilişkili kayıt tablosu,
1316
+ `modal`/`drawer` ile ikincil işlemler (parola güncelleme gibi — modalın içinde kendi formu ve kendi
1317
+ `submit` akışı). Kimliği query parametresinden değil **middleware'den** alır: uca auth middleware'i
1318
+ bağlanır, filtre `source:'expression'` + `value:'user.id'` olur.
1319
+
1320
+ ### 7e — Hangi iş için hangi komponent
1321
+
1322
+ `component_get` prop sözlüğünü verir ama **hangi işe hangisi** kararını sen verirsin:
1323
+
1324
+ | İş | Komponent |
1325
+ |---|---|
1326
+ | Metin / uzun metin / zengin metin | `textinput` · `textarea` · `texteditor` |
1327
+ | Öneri listeli serbest metin | `autocomplete` (value YAZILAN metindir; id gerekiyorsa `select` olayı) |
1328
+ | Sayı / artan kod / kaydırıcı | `numberinput` · `incrementalinput` · `slider` |
1329
+ | Seçim (tek / çok / etiket) | `dropdown` · `multi-select` · `tag-input` · `radio` · `checkboxgroup` (değeri dizi) |
1330
+ | İki durumlu | `checkbox` · `switch` · `switcher` (iki durum yan yana) |
1331
+ | Puan / kod girişi | `rating` · `otpinput` |
1332
+ | Tarih / saat / aralık | `datepicker` · `datetimepicker` · `daterangepicker` · `datetimerangepicker` · `timepicker` (yalnız HH:mm) |
1333
+ | Kimlik alanları | `email` · `password` |
1334
+ | Dosya | `fileUpload` (dropzone) · `fileInput` · `filebutton` · `file-viewer` |
1335
+ | Liste | `table` · `datatable` (Kural 1e) · `repeater` (kart listesi VE dinamik form — Kural 1g) · `tree-view` (hiyerarşi; `isSearchable` ile istemci içi arama) · `timeline` (tarihli olay akışı) |
1336
+ | Sayfalama (tablo dışı listeler için) | `pagination` — offset/limit yazar, `self.offset`/`self.limit` ile sorguya bağlanır |
1337
+ | Grafik / KPI | `chart` (ECharts: bar·line·area·pie·donut + waterfall/histogram/pareto/bullet/tornado/radial) · `statcard` (tek KPI + trend + sparkline) · `progress` |
1338
+ | Kapsayıcı / düzen | `container` (ızgara ya da flex — Kural 1f-2) · `form` (düzen prop'u YOK, çocuklar ızgara koordinatıyla dizilir) · `tabs` · `collapsible-container` · `accordion` |
1339
+ | Kart görünümü | `card` — **kapsayıcı DEĞİLDİR**, çocuk kabul etmez (`not-droppable`) ve yüksekliği 10-50 aralığındadır. Kutu içinde serbest içerik için `container` kullan. |
1340
+ | Katman | `modal` · `drawer` (şema kökünde — Kural 1d) |
1341
+ | Geri bildirim | `notification` (sayfa içi kutu) · `toast` (geçici bildirim; isOpen yükselen kenarı) · `form-validation-errors` · `confirm-bar` · `result` (işlem sonucu ekranı) · `emptystate` (boş liste; ikon YA DA görsel + aksiyon düğmeleri) |
1342
+ | Gezinme / başlık | `breadcrumb` · `breadcrumb-title` (başlık + daralan iz) · `page-header` · `header` · `sidebar` · `auth-layout` · `language-selector` (dil değiştirici) |
1343
+ | Sunum | `text` · `html` · `image` · `video` · `icon` · `qrcode` · `tags` · `avatar` · `badge` (sayaç “99+” / nokta / durum metni) · `carousel` · `iframe` (sandbox’lı gömme) · `jsonviewer` (katlanabilir JSON; JSON METNİNİ de parse eder) · `audit-basic` · `audit-history` |
1344
+
1345
+ > **`chart` komponenti VAR (2026-08-25 ölçümü) — bu satır düzeltildi.** Rehberin önceki hâli
1346
+ > "grafik komponenti yoktur, `html` + `loadScript` kullanın" diyordu; `component_list` çıktısında
1347
+ > `chart` kayıtlıdır ve `component_get(slug:'chart')` prop sözlüğünü verir. Veri `rows` prop'una
1348
+ > akışla yazılır (komponent kendi verisini ÇEKMEZ), kategori `categoryField`, değerler
1349
+ > `series[].valueField` ile çözülür; `aggregate`/`sort`/`topN` dönüşümü komponent yapar.
1350
+ > `html` + `loadScript` yalnız `chart`'ın kapsamadığı görselleştirmeler için gerekir.
1351
+ >
1352
+ > **Veriye göre değişen form (çalışma anında N alan) `html` GEREKTİRMEZ — `repeater` bunu
1353
+ > yapar.** Girdiler şemaya tasarım anında yazılır ama `repeater` o şablonu `rows`'un her satırı
1354
+ > için yineler, `{{ item.<alan> }}` kod prop'u iki yönlü bağlanır ve alan tipi satır başına
1355
+ > `isHidden` ile seçilir. Anket / dinamik başvuru formu bu desenle kurulur — tam tarif ve
1356
+ > ölçülmüş sınırlar **Kural 1g**'de. (Bu satır 2026-08-19'da düzeltildi: önceki hâli "hazır
1357
+ > komponent yoktur, `html` kullanın" diyordu ve YANLIŞTI.)
1358
+
1359
+ **Hiçbiri işi karşılamıyorsa `html` kaçış kapısıdır** — Mustache + `context` ile veri basar,
1360
+ `actions` + `fireEvent` ile flow tetikler, satır içi script çalıştırır. "Platform bunu yapamaz"
1361
+ demeden önce `platform_guide('html')` oku; genel komponent modeli için `platform_guide('component')`.
1362
+
1363
+ Komponentlerin **`style` prop'u** vardır ve iç parçalara iner (`textinput.style`:
1364
+ `&__label` · `&__input` · `&__error` · `&__description`; `form.style`: `&__title` · `&__header` ·
1365
+ `&__body`). Tasarım kararlarını buradan uygula — varsayılan görünümle bırakmak zorunda değilsin.
1366
+
1367
+ > **Boyut tuzağı:** katalog varsayılan `size`'ı `maxSize`'ı aşan komponentler var
1368
+ > (`textinput` default `h:8`, `maxSize.h:5`). Varsayılanı körü körüne kopyalarsan doğrulayıcı
1369
+ > reddeder; gateway aşan değeri `maxSize`'a kırpar ve `gatewayNormalized` ile bildirir.
1370
+
1371
+ ## Kural 8 — Korunan sayfa: `middlewareIds` vermeden `Page.user` YOKTUR
1372
+
1373
+ Uçları middleware'e bağlamak sayfayı korumaz ve **`Page.user`'ı doldurmaz**. `Page.user`,
1374
+ sayfaya `page_update` ile bağlanan middleware'in `set-user` çıktısıdır; bağ yoksa `undefined`
1375
+ gelir ve `Page.user.x` okuyan her ifade *"Cannot read properties of undefined"* ile patlar.
1376
+ Bir `condition` adımında bu **sessizce else dalına düşmek** demektir.
1377
+
1378
+ ```
1379
+ page_update({ pageId, middlewareIds: ['<authenticate workflow id>'] })
1380
+ ```
1381
+
1382
+ Ölçüm (klinik/kutuphane/obs, 2026-08-18): 42 ucun 41'i middleware'e bağlıyken **sayfaların
1383
+ hiçbiri** bağlı değildi; üç portalın da rol menüsü boş çıkıyor, panel ifadeleri patlıyordu.
1384
+
1385
+ İki şeyi karıştırma:
1386
+
1387
+ | İstenen | Yol |
1388
+ |---|---|
1389
+ | Veri güvenliği | **ucun** `middlewareIds` + filtre `source:'expression'`, `value:'user.id'` |
1390
+ | `Page.user` + sayfa yönlendirmesi | **sayfanın** `middlewareIds`'i |
1391
+
1392
+ > **`page.roleIds` yetki KAPATMAZ — güvenlik olarak ona güvenme.** Ölçüm (2026-08-18): `roleIds`
1393
+ > yalnız yönetici rollerine ayarlanmış bir sayfayı hasta rolündeki kullanıcı **açabildi** (sayfa
1394
+ > çizildi, tablo boş kaldı çünkü **uçlar** 401/403 verdi). `roleIds` menü/görünürlük kararı için
1395
+ > bir sinyaldir; erişimi kapatan tek şey **ucun** rol kapısıdır. **Ama `microservice.roles` de
1396
+ > tek başına kapı değildir** (ölçüm 2026-08-19: `roleIds` dolu + `middlewareIds` boş uç,
1397
+ > tokensız POST'u 200 ile kabul etti). `roles` yalnız middleware'e `triggerSource.roles` olarak
1398
+ > taşınan veridir; kapıyı kuran şey middleware içindeki açık `Authorized(user.roles)` adımıdır
1399
+ > (`platform_guide('middleware')` Tarif 3b). Bir ekranı gerçekten kapatmak istiyorsan verisini
1400
+ > besleyen her ucu `middlewareIds` + `Authorized` kapısına al; sayfayı gizlemek kozmetiktir.
1401
+ >
1402
+ > **Sayfa kapısı VARDIR ama onu `redirect` DEĞİL, `redirect`'i izleyen `response` tutar.**
1403
+ > *(Ölçüm 2026-08-19 — bu bölümün önceki sürümü yanlıştı.)* `RedirectStep.cs` yalnız bir
1404
+ > `UiAction` kaydedip success dalından devam eder; durum kodunu değiştirmez. Akış `response`
1405
+ > adımı olmadan biterse sonuç 2xx kalır, `ExecutePageBehavior` kesmez ve **sayfanın tamamı
1406
+ > anonim isteğe döner** — üstelik `PagesController.HandleResult` `uiActions`'ı yalnız non-2xx
1407
+ > dalında yayınladığı için yönlendirme de kaybolur. Ölçüm: yalnız `redirect` ile biten guard'a
1408
+ > bağlı sayfa tokensız istekte **HTTP 200 + şemanın tamamı** döndürdü.
1409
+ >
1410
+ > Doğru desen (ölçülerek doğrulandı — 401, şema sızmadı, `uiActions` yönlendirmeyi taşıdı):
1411
+ >
1412
+ > ```
1413
+ > guard → [condition] triggerSource.type === 'page'
1414
+ > ├─ if → [redirect] expression: "'giris'" → [response] 401 ← kapı BURADA
1415
+ > └─ else → [response] 401 ← uç çağrısı 401 alır
1416
+ > ```
1417
+ >
1418
+ > `redirect` adımının `nextStepId`'si `null` değil, **`response` adımını** göstermeli. Tam gövde
1419
+ > ve tırnak tuzağı (`"'giris'"` — tırnaksız yazarsan runtime'da `giris is not defined`):
1420
+ > `platform_guide('middleware')` Tarif 1b. **Korumalı her sayfa, tokensız bir istekle 401
1421
+ > verdiği görülerek kabul edilir;** 200 + şema dönüyorsa kapı kurulmamıştır.
1422
+ >
1423
+ > **Tokensız istek nasıl atılır:** `page_runtime_get` + **`anonymous: true`** — gateway o çağrıya
1424
+ > hiçbir `Authorization` header'ı koymaz; korumalı sayfada `middlewareBlocked` (401) dönmeli ve
1425
+ > şema gelmemelidir. Sayfanın ham serve yolu **`GET /{project}/{application}/p/{sayfa slug'ı}`**
1426
+ > (`PagesController:127` — `{project}` öneki ZORUNLUDUR; `/{app}/p/{slug}` tek başına 404 verir),
1427
+ > `Host: <org>.<suffix>` + `X-Environment` başlıklarıyla. Ham `curl` yerine tool'u kullanmak
1428
+ > yeterlidir.
1429
+ >
1430
+ > `page.roleIds` bu dalla birlikte de kapı değildir: rol ayrımını sayfa değil **uç** yapar.
1431
+
1432
+ > **Rol okurken dizi bekleme.** `set-user` DTO'su `roles` dizisini çoğu kurulumda düşürür
1433
+ > (`dto_get` ile bak: genelde `role`, `role_name`, `roles_csv` vardır). Sayfa tarafında CSV
1434
+ > kullan — ayrıntı ve şablon `platform_guide('expressions')` §2.
1435
+ >
1436
+ > **Uç hata dönerse `.data` dizi değildir.** 401/404 sonrası `Steps.x.data` bir nesnedir ve
1437
+ > `rows || []` bunu yakalamaz (nesne truthy) → `rows.filter is not a function` /
1438
+ > `(items ?? []).map is not a function`. `Array.isArray(Steps.x?.data) ? Steps.x.data : []` yaz.
1439
+
1440
+ ## ⚠️ `page_schema_patch` `html` komponentini reddediyor — katalogda mükerrer kayıt (2026-08-19)
1441
+
1442
+ `page_schema_patch` ile `html` eklemeye çalışırsan:
1443
+
1444
+ ```
1445
+ Component type 'html' has no prop named 'context' → kabul edilenler: content
1446
+ Component type 'html' has no prop named 'style'
1447
+ Height of 'html' must be between 1 and 1
1448
+ ```
1449
+
1450
+ Oysa `component_get('html')` `content · context · style · actions · isHidden` prop'larını ve
1451
+ `maxSize {w:12,h:50}` döndürür. Çelişkinin sebebi **katalogdaki mükerrer satırlardır**:
1452
+ `development.components` tablosunda 11 slug'ın ikişer kaydı var —
1453
+ `button · checkbox · container · html · image · notification · numberinput · password · text ·
1454
+ textarea · textinput`. `ComponentRepository.FindBySlugsWithDefinitionsAsync` yalnız slug'a göre
1455
+ süzer (tekillik filtresi yok), `PageSchemaValidator` da `GroupBy(Slug).First()` yapar: **ilk
1456
+ satır kazanır ve sıralama garanti değildir.** `html`'de eski/fakir kayıt (tek prop `content`,
1457
+ 0 event) gerçek kaydı (8 prop, `mount` olayı) gölgeliyor.
1458
+
1459
+ Diğer 10 slug bugün çalışıyor çünkü zengin kayıt daha eski — bu tasarım değil sıralama şansıdır.
1460
+
1461
+ **Neden yalnız micro-op hattı etkileniyor:** prop/boyut denetimi yalnız
1462
+ `PageSchemaValidationMode.Full`'de yapılır. `page_create`/`page_update` `Structural` çağırır,
1463
+ `layout_*` doğrulayıcıya hiç uğramaz. `Full` kullanan tek iki yer `page_schema_patch` ve
1464
+ `page_schema_validate`'tir.
1465
+
1466
+ **Geçici çözüm (doğrulandı):** `html` içeren sayfayı `page_update(schema)` ile **tam gövde**
1467
+ yaz — `content`/`context`/`style`/`actions` prop'ları ve `events: [{ name:'mount', value:<flowId> }]`
1468
+ bağı kalıcı olur ve `page_runtime_get` paketinde görünür. Bedeli: o sayfada doğrulama duvarı,
1469
+ canlı canvas güncellemesi ve `baseVersion` iyimser kilidi devre dışı kalır. Kusur düzeltilince
1470
+ micro-op hattına dönün.
1471
+
1472
+ > **Sıra kritik — `page_update(schema)` mevcut flow'ların setter hedeflerini KOPARIR.**
1473
+ > Setter iteratee'sindeki `element` bir string olarak saklanmaz: platform onu **kayıt anında**
1474
+ > sayfanın komponentlerine karşı çözüp `DesignComponentId`'ye çevirir. `page_update(schema)` ise
1475
+ > komponent satırlarını yeniden üretir (yeni id'ler) — daha önce kurulmuş her flow'un setter
1476
+ > hedefi böylece dangling kalır ve `flow_get` çıktısında **`element` alanı kaybolur**. Semptom:
1477
+ > akış koşar, microservice 200 döner, ama komponent hiç güncellenmez (ölçüm: anket projesi,
1478
+ > 2026-08-19 — `html` sonsuza kadar "yükleniyor" durumunda kaldı).
1479
+ >
1480
+ > Doğru sıra: komponentleri yaz → `page_update(schema)` ile `actions`/`events` bağlarını kur →
1481
+ > **en son** flow'ları oluştur. Şemayı sonradan yeniden yazmak zorunda kaldıysan **etkilenen her
1482
+ > flow'u `flow_update` ile yeniden kaydet** (aynı gövde yeter; kayıt sırasında hedefler tekrar
1483
+ > çözülür) ve `flow_get` ile `element` alanının geri geldiğini doğrula.
1484
+ >
1485
+ > **Aynı tuzak micro-op yolunda da var:** `page_schema_patch` ile bir düğümü
1486
+ > `remove_component` + `add_component` yapıp **aynı `elementId`'yi** geri koyduğunda satırın
1487
+ > `DesignComponentId`'si yenidir — o düğümü hedefleyen setter'lar yine dangling kalır. Ölçüm
1488
+ > (anket-native, 2026-08-19): `q_list` repeater'ı `isWrapper`'ı düzeltmek için yeniden kurulunca
1489
+ > `t_title`/`t_desc` setter'ları çalışmaya devam etti (dokunulmamışlardı) ama `q_list.rows`
1490
+ > setter'ı sessizce hiçbir şey yazmadı — sayfa boş liste ile açıldı, konsolda hata yok.
1491
+ > `flow_update` ile aynı gövdeyi yeniden kaydetmek düzeltti. Kural: **şemadan bir düğüm sildiysen,
1492
+ > o düğümü hedefleyen her flow'u yeniden kaydet.**
1493
+
1494
+ ## Bilinen sınırlar
1495
+
1496
+ - **Ekranı tarayıcıdan doğrularken: `dropdown` ve `multi-select` seçeneklerine tıklamak için iç
1497
+ `<button>`'ı hedefle.** Bu iki komponentin açılır listesinde satır sarmalayıcıları (`div`) tıklama
1498
+ dinlemiyor; asıl dinleyici `button.ng-select-item__content` (dropdown) ve
1499
+ `button.ng-multiselect-item__content` (multi-select). Ölçüm (2026-08-19): satır `div`'ine tam
1500
+ pointer dizisi (`pointerdown/mousedown/pointerup/mouseup/click`) göndermek değeri DEĞİŞTİRMEDİ
1501
+ (`in_status.value` `draft` kaldı); iç `button`'a düz `.click()` göndermek değiştirdi
1502
+ (`published`). Listeyi açmak için ise `.ng-select__value-container` / `.ng-multiselect`'e tam
1503
+ pointer dizisi gerekir — düz `.click()` açmıyor. Değeri doğrulamanın kesin yolu DOM değil
1504
+ `machine.getNode('<elementId>').props.value`'dur.
1505
+ - **`style` prop'una CSS STRING yazma — nesne olmak zorunda.** String de kabul edilir ve emotion onu
1506
+ CSS metni sayar, ama (a) `--ng-*` token ezemezsin, (b) iç seçici / pseudo-class yazamazsın,
1507
+ (c) yanlışlıkla kabın kendi düzenini bozarsın. Ölçüm (anket-admin giriş sayfası, 2026-08-19):
1508
+ `page_container.style` string olarak `display:flex; align-items:center; justify-content:center`
1509
+ taşıyordu; `.ng-container` zaten `display:flex; flex-direction:column` olduğu için kabın iç
1510
+ düzeni çöktü — kart **66px genişlik × 733px yükseklik**, buton 44px çizildi. Aynı ekran nesne
1511
+ style + Kural 1h deseniyle yeniden kurulunca kart 400px'e ve ortaya oturdu. Gateway bunu **PAGE-014 (uyarı)** ile
1512
+ yanıtın `layoutWarnings` alanında bildirir. **Düzeni `style` ile kurmaya hiç kalkışma:**
1513
+ ortalama/yaslama/aralık işi kapsayıcının kendi `layout: 'flex'` prop grubuna aittir (Kural 1f-2).
1514
+ - **`isHidden` olmayan komponent listesine `dropdown` da dahildir** (`text`, `multi-select`,
1515
+ `dropdown`). Koşullu göstermek için `isHidden`'ı olan bir `container`'a sar (Kural 1g).
1516
+ - **Ana sayfayı TAŞIYAMAZSIN — `application_update.homepageId` kalıcı DEĞİL ve yanıtı yanıltıcı.**
1517
+ `ApplicationService.UpdateAsync` sayfaları `AsNoTrackingWithIdentityResolution()` ile okunmuş
1518
+ **takipsiz** bir graf üzerinde `SetHomepage(...)` ile işaretliyor; yanıttaki `homepage` alanı bu
1519
+ bellek içi değişiklikten üretildiği için **yeni sayfayı gösterir ama `pages.is_homepage` diskte
1520
+ değişmez** (ölçüm 2026-08-19: çağrıdan sonra `page_get` eski sayfada hâlâ `isHomepage: true`,
1521
+ yeni sayfada `false`). Sonucu: **ana sayfa silinemez** (`CantDeleteHomepage`) ve gateway'de bunu
1522
+ düzeltmenin yolu yoktur — `UpdatePageCommand`'da `IsHomepage` alanı yok.
1523
+ Daha kötüsü **iki ayrı ana sayfa kapısı var ve birbirine düşebilir:** çağrıdan sonra eski sayfa
1524
+ `pages.is_homepage` yüzünden ("… ana sayfadır ve silinemez"), YENİ sayfa da application'ın
1525
+ referansı yüzünden ("The home page can't be deleted") silinemez hâle geldi; kilidi açmak için
1526
+ `application_update`'i eski sayfaya geri çevirmek gerekti.
1527
+ **Pratik kural:** ana sayfayı değiştirmen gerekiyorsa sayfayı silip yeniden kurma —
1528
+ **mevcut ana sayfanın İÇİNİ** değiştir (`remove_component` + `add_component`), böylece slug,
1529
+ ana sayfa bayrağı ve gelen bağlantılar korunur. Gerçek ana sayfa taşıma Studio'dan yapılmalı.
1530
+ - **Akış adımlarının `branches[].id` ve `iteratees[].id` GUID'leri TÜM projede tekildir.** Başka
1531
+ bir sayfadaki akıştan kopyaladığın gövdeyi aynı GUID'lerle `flow_create`'e verirsen platform
1532
+ `RowAlreadyExists` döner — hata mesajı slug çakışması gibi okunur ve seni yanlış yere götürür
1533
+ (ölçüm 2026-08-19: slug'ı değiştirmek çözmedi, GUID'leri değiştirmek çözdü). Şablon olarak
1534
+ `flow_get` çıktısı kullanırken **bütün id alanlarını yenile**.
1535
+ - **Katalog kaydı frontend'in gerisinde kalabiliyor — doğrulama duvarı frontend'i değil KATALOĞU
1536
+ bilir.** Bir prop'un renderer'da çalışıyor olması onu yazabileceğin anlamına gelmez; **tek
1537
+ referans `component_get`'tir** (platform o satırı `unknown-prop` duvarında kullanır). Yaşanmış
1538
+ örnek ve **kapanışı**: `container`'ın flex düzen grubu (`layout`, `direction`,
1539
+ `horizontalAlign`, `verticalAlign`, `size`, `gap`, `wrap`) 2026-08-19 ölçümünde `ng-apps-ui`
1540
+ şemasında ve renderer'da vardı ama katalog satırında YOKTU — `page_schema_patch` hepsini
1541
+ `unknown-prop` ile reddediyordu. **2026-08-25 ölçümü: katalog güncellenmiş**, yedi prop da
1542
+ `component_get(slug:'container')` çıktısında (varsayılanlarıyla birlikte) ve
1543
+ `page_schema_validate` hepsine `valid: true` diyor — flex kapsayıcı artık şemadan kurulur
1544
+ (Kural 1f-2). Aynı sınıf sorunun HÂLÂ açık örneği: `text` ve `multi-select` komponentlerinde
1545
+ `isHidden` prop'u yok (Kural 1g'deki sarmalayıcı çözümü). Bir prop'u kullanmadan önce
1546
+ `component_get`'e bak; eksikse ürün sahibine katalog güncellemesi olarak bildir.
1547
+
1548
+ - **`container.title` `states.isWrapper: true` iken başlık ÇİZMEZ.** Wrapper kaplar salt düzen
1549
+ elemanıdır; başlığı da `__header` bloğunu da render etmez (klinik ölçümü: `__header` sayısı 0).
1550
+ `isWrapper: false` olan kapta başlık normal çizilir (İK ölçümü: başlık verilmeyen kapta
1551
+ varsayılan "Container title" göründü). Kart başlığını garantiye almak istiyorsan kabın
1552
+ `title`'ına güvenme, **içine bir `text` komponenti** koy.
1553
+ - **Kod prop'unun ifadesi `ref`'te ve `{{ }}` sarılı olmalı.** `{type:'code', ref:"'' + Row.value"}`
1554
+ (süssüz) ekrana ifadenin **kendisini** basar; ifade yalnız `data`'da olursa prop **boş**
1555
+ render edilir. `set_binding` micro-op'u eski sürümlerde `data`'ya yazıyordu — bu davranış
1556
+ düzeltildi, ama daha önce kurulmuş sayfalarda kalıntı olabilir. Prop nesnesini birebir
1557
+ kontrol etmen gerekirse `remove_component` + `add_component` ile
1558
+ `{"type":"code","ref":"{{ … }}","data":"<yedek>"}` yaz; `replace_prop` her zaman `static` kurar.
1559
+
1560
+ - **Ana sayfa sayfadan değil application'dan değişir:** `page_create/update` gövdesindeki
1561
+ `isHomepage` platformca yok sayılır (tasarım). Doğru yol `application_update` →
1562
+ `homepageId`; platform o çağrıda uygulamanın tek sayfasını ana sayfa yapar. `page_delete`
1563
+ ana sayfayı reddeder — gateway bunu istek göndermeden söyler.
1564
+ - **Runtime çıktısı `page_runtime_get` ile okunur:** Studio preview'u ve çıkan uygulama aynı
1565
+ ucu (`GET {application}/p/{slug}`) tüketir; bu tool birleşik son hâli (şema + layout +
1566
+ olaylar + flow'lar) döner ve sayfaya bağlı middleware'ler çağrıda GERÇEKTEN koşar —
1567
+ korumalı sayfada `middlewareBlocked` dönmesi beklenen davranıştır ve kendisi bir
1568
+ doğrulamadır. Tam doğrulama seti: `page_schema_validate` (kuru koşu) + `page_get` (bağlar)
1569
+ + `page_runtime_get` (son hâl).
1570
+ - **Sayfada dosya toplanabilir:** `fileUpload` (dropzone) · `fileInput` · `filebutton`
1571
+ komponentleri vardır, göstermek için `image` · `file-viewer`. Yani "dosya yükleyerek cevapla"
1572
+ tipi bir alan kurulabilir. Gateway tarafında `file_upload`/`file_download` tool'ları eklendi
1573
+ (multipart / `outPath`) ama **canlı platforma karşı henüz doğrulanmadı** — ilk kullanımda
1574
+ sonucu teyit et.