@ozsarman/clarityjs 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/layout.js ADDED
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Clarity.js — Layout System
3
+ *
4
+ * Next.js app/ layout.js / Nuxt layouts/ eşdeğeri.
5
+ * Dosya tabanlı nested layout konvansiyonu + programatik API.
6
+ *
7
+ * ── Dosya konvansiyonu ────────────────────────────────────────────────────────
8
+ *
9
+ * pages/
10
+ * ├── _layout.js ← root layout (tüm sayfaları sarar)
11
+ * ├── index.js
12
+ * ├── about.js
13
+ * └── blog/
14
+ * ├── _layout.js ← /blog/* için layout (root layout'u da devralır)
15
+ * └── [slug].js
16
+ *
17
+ * ── _layout.js örneği ────────────────────────────────────────────────────────
18
+ *
19
+ * // pages/_layout.js
20
+ * export default function RootLayout({ children }) {
21
+ * return (
22
+ * <html lang="tr">
23
+ * <body>
24
+ * <Nav />
25
+ * <main>{children}</main>
26
+ * <Footer />
27
+ * </body>
28
+ * </html>
29
+ * );
30
+ * }
31
+ *
32
+ * ── Programatik API ───────────────────────────────────────────────────────────
33
+ *
34
+ * import { defineLayout, applyLayouts, renderWithLayouts } from '@ozsarman/clarityjs/layout'
35
+ *
36
+ * defineLayout('root', RootLayout);
37
+ * defineLayout('blog', BlogLayout, { parent: 'root' });
38
+ *
39
+ * // Bir sayfayı layout'larıyla render et:
40
+ * const html = renderWithLayouts(BlogPost, { slug: 'hello' }, 'blog');
41
+ *
42
+ * ── definePageMeta ────────────────────────────────────────────────────────────
43
+ *
44
+ * // pages/blog/[slug].js
45
+ * export const pageMeta = definePageMeta({
46
+ * layout: 'blog', // kullanılacak layout
47
+ * title: 'Blog Yazısı',
48
+ * auth: true, // route guard
49
+ * });
50
+ *
51
+ * ── SSR entegrasyonu ──────────────────────────────────────────────────────────
52
+ *
53
+ * import { wrapWithLayouts } from '@ozsarman/clarityjs/layout'
54
+ *
55
+ * // SSR pipeline'da:
56
+ * const html = renderToString(wrapWithLayouts(PageComponent, layoutChain), { props });
57
+ *
58
+ * Author: Claude (Anthropic) + Özdemir Sarman
59
+ */
60
+
61
+ import { renderToString } from './ssr.js';
62
+
63
+ // ─── Layout Registry ──────────────────────────────────────────────────────────
64
+
65
+ const _layoutRegistry = new Map();
66
+
67
+ /**
68
+ * Programatik layout tanımı.
69
+ *
70
+ * @param {string} name – Layout adı (benzersiz)
71
+ * @param {Function} ComponentFn – Layout bileşeni ({ children, ...props }) => node
72
+ * @param {object} [opts]
73
+ * @param {string} [opts.parent] – Ebeveyn layout adı (nested layouts)
74
+ */
75
+ export function defineLayout(name, ComponentFn, opts = {}) {
76
+ const { parent = null } = opts;
77
+ _layoutRegistry.set(name, { name, ComponentFn, parent });
78
+ }
79
+
80
+ /**
81
+ * Kayıtlı layout'u al.
82
+ * @param {string} name
83
+ * @returns {{ name: string, ComponentFn: Function, parent: string|null } | null}
84
+ */
85
+ export function getLayout(name) {
86
+ return _layoutRegistry.get(name) ?? null;
87
+ }
88
+
89
+ /**
90
+ * Tüm layout kayıtlarını sıfırla (test / SSR yeniden başlatma için).
91
+ */
92
+ export function resetLayoutRegistry() {
93
+ _layoutRegistry.clear();
94
+ }
95
+
96
+ // ─── definePageMeta ───────────────────────────────────────────────────────────
97
+
98
+ /**
99
+ * Sayfa metadata tanımla — layout seçimi, auth guard, title vb.
100
+ * Sayfa dosyasında `export const pageMeta = definePageMeta({...})` şeklinde kullanılır.
101
+ *
102
+ * @param {object} opts
103
+ * @param {string} [opts.layout] – Kullanılacak layout adı (yoksa 'default')
104
+ * @param {string} [opts.title] – Sayfa başlığı
105
+ * @param {boolean} [opts.auth] – Kimlik doğrulama gerektiriyor mu?
106
+ * @param {string} [opts.middleware] – Middleware adı
107
+ * @param {object} [opts.meta] – Ek metadata
108
+ * @returns {PageMeta}
109
+ */
110
+ export function definePageMeta(opts = {}) {
111
+ return {
112
+ __clarity_page_meta__: true,
113
+ layout: opts.layout ?? 'default',
114
+ title: opts.title ?? null,
115
+ auth: opts.auth ?? false,
116
+ middleware: opts.middleware ?? null,
117
+ meta: opts.meta ?? {},
118
+ ...opts,
119
+ };
120
+ }
121
+
122
+ // ─── Layout zinciri oluştur ───────────────────────────────────────────────────
123
+
124
+ /**
125
+ * Bir layout adından başlayarak en üst layout'a kadar zincir oluşturur.
126
+ * ['root', 'blog'] gibi dıştan içe sıralı döner.
127
+ *
128
+ * @param {string} layoutName
129
+ * @returns {Array<{ name: string, ComponentFn: Function }>}
130
+ */
131
+ export function buildLayoutChain(layoutName) {
132
+ const chain = [];
133
+ let current = _layoutRegistry.get(layoutName) ?? null;
134
+
135
+ while (current) {
136
+ chain.unshift(current); // en dıştan başla
137
+ current = current.parent ? _layoutRegistry.get(current.parent) ?? null : null;
138
+ }
139
+
140
+ return chain;
141
+ }
142
+
143
+ // ─── Runtime: children injection ─────────────────────────────────────────────
144
+
145
+ /**
146
+ * Sayfa bileşenini layout zinciriyle sar.
147
+ * En dış layout en üstte, sayfa bileşeni en içte.
148
+ *
149
+ * @param {Function} PageComponent
150
+ * @param {Array} layoutChain – buildLayoutChain() çıktısı
151
+ * @returns {Function} Sarılmış bileşen (render için kullanılır)
152
+ */
153
+ export function wrapWithLayouts(PageComponent, layoutChain) {
154
+ if (!layoutChain || layoutChain.length === 0) return PageComponent;
155
+
156
+ // Layout zincirini içten dışa sarar: page → innerLayout → ... → rootLayout
157
+ let Wrapped = PageComponent;
158
+
159
+ for (let i = layoutChain.length - 1; i >= 0; i--) {
160
+ const { ComponentFn } = layoutChain[i];
161
+ const Inner = Wrapped;
162
+ Wrapped = function LayoutWrapper(props) {
163
+ // children olarak Inner bileşenini ilet
164
+ return ComponentFn({ ...props, children: Inner(props) });
165
+ };
166
+ Wrapped.displayName = `Layout(${ComponentFn.name || '?'})`;
167
+ }
168
+
169
+ return Wrapped;
170
+ }
171
+
172
+ // ─── SSR: layout'larla render ─────────────────────────────────────────────────
173
+
174
+ /**
175
+ * Bir sayfayı layout zinciriyle SSR render et.
176
+ *
177
+ * @param {Function} PageComponent
178
+ * @param {object} [props={}]
179
+ * @param {string} [layoutName] – layout adı; yoksa pageMeta.layout veya 'default'
180
+ * @returns {string} HTML string
181
+ */
182
+ export function renderWithLayouts(PageComponent, props = {}, layoutName = null) {
183
+ // Sayfa meta'sından layout adını belirle
184
+ const metaLayout = PageComponent.pageMeta?.layout
185
+ ?? PageComponent.__clarity_page_meta__?.layout
186
+ ?? null;
187
+
188
+ const finalLayout = layoutName ?? metaLayout ?? 'default';
189
+ const chain = buildLayoutChain(finalLayout);
190
+
191
+ const Wrapped = wrapWithLayouts(PageComponent, chain);
192
+ return renderToString(Wrapped, { props });
193
+ }
194
+
195
+ // ─── Dosya sistemi layout tarayıcı ───────────────────────────────────────────
196
+
197
+ /**
198
+ * `pages/` dizin yapısını tarayıp _layout.js dosyalarını bulur,
199
+ * otomatik olarak defineLayout() ile kaydeder.
200
+ *
201
+ * File-router ve SSG pipeline'da çağrılır.
202
+ *
203
+ * @param {string} pagesDir – Mutlak yol
204
+ * @param {string} [rootName='default']
205
+ * @returns {Promise<string[]>} Kaydedilen layout adları
206
+ */
207
+ export async function scanAndRegisterLayouts(pagesDir, rootName = 'default') {
208
+ const { readdir, stat } = await import('node:fs/promises');
209
+ const { existsSync } = await import('node:fs');
210
+ const { join, relative, dirname } = await import('node:path');
211
+ const { pathToFileURL } = await import('node:url');
212
+
213
+ const registered = [];
214
+
215
+ async function scan(dir, parentName) {
216
+ if (!existsSync(dir)) return;
217
+
218
+ const entries = await readdir(dir).catch(() => []);
219
+
220
+ // _layout.js bu dizinde var mı?
221
+ const layoutFile = entries.find(e => e === '_layout.js' || e === '_layout.mjs');
222
+ if (layoutFile) {
223
+ const fullPath = join(dir, layoutFile);
224
+ const relPath = relative(pagesDir, dir) || '.';
225
+ const layoutName = relPath === '.' ? rootName : relPath.replace(/\\/g, '/');
226
+
227
+ try {
228
+ const mod = await import(pathToFileURL(fullPath).href + `?t=${Date.now()}`);
229
+ const ComponentFn = mod.default;
230
+ if (typeof ComponentFn === 'function') {
231
+ defineLayout(layoutName, ComponentFn, { parent: parentName !== layoutName ? parentName : null });
232
+ registered.push(layoutName);
233
+ console.log(`[clarity/layout] Layout kaydedildi: '${layoutName}' (parent: ${parentName ?? 'none'})`);
234
+ }
235
+ } catch (err) {
236
+ console.warn(`[clarity/layout] Layout yüklenemedi (${fullPath}): ${err.message}`);
237
+ }
238
+
239
+ // Alt dizinleri bu layout'u parent olarak kullanarak tara
240
+ for (const entry of entries) {
241
+ const fullEntry = join(dir, entry);
242
+ const info = await stat(fullEntry).catch(() => null);
243
+ if (info?.isDirectory()) {
244
+ await scan(fullEntry, layoutName);
245
+ }
246
+ }
247
+ } else {
248
+ // Bu dizinde layout yok — alt dizinleri parent ile tara
249
+ for (const entry of entries) {
250
+ const fullEntry = join(dir, entry);
251
+ const info = await stat(fullEntry).catch(() => null);
252
+ if (info?.isDirectory()) {
253
+ await scan(fullEntry, parentName);
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ await scan(pagesDir, null);
260
+ return registered;
261
+ }
262
+
263
+ // ─── Client-side: layout route değişimlerinde uygula ─────────────────────────
264
+
265
+ /**
266
+ * Client router ile entegrasyon — route değiştiğinde layout'u güncelle.
267
+ * `hydrateRoot` ve router `navigate` ile birlikte çalışır.
268
+ *
269
+ * @param {object} opts
270
+ * @param {HTMLElement} opts.container – Layout wrapper DOM node
271
+ * @param {Function} opts.getPage – (path: string) => { ComponentFn, layoutName }
272
+ */
273
+ export function initLayoutRouter(opts = {}) {
274
+ const { container, getPage } = opts;
275
+ if (typeof window === 'undefined') return;
276
+
277
+ function handleRouteChange() {
278
+ const path = window.location.pathname;
279
+ const page = getPage(path);
280
+ if (!page) return;
281
+
282
+ const { ComponentFn, layoutName } = page;
283
+ const chain = buildLayoutChain(layoutName ?? 'default');
284
+ const Wrapped = wrapWithLayouts(ComponentFn, chain);
285
+
286
+ // Container'ı temizle ve yeniden render et
287
+ if (container) {
288
+ // Clarity runtime ile hydrate
289
+ import('./hydrate.js').then(({ hydrateRoot }) => {
290
+ hydrateRoot(Wrapped, container, {});
291
+ }).catch(() => {
292
+ // Fallback: innerHTML (SSR olmadan)
293
+ const html = renderWithLayouts(ComponentFn, {}, layoutName);
294
+ container.innerHTML = html;
295
+ });
296
+ }
297
+ }
298
+
299
+ window.addEventListener('popstate', handleRouteChange);
300
+ window.addEventListener('clarity:navigate', handleRouteChange);
301
+
302
+ return {
303
+ destroy: () => {
304
+ window.removeEventListener('popstate', handleRouteChange);
305
+ window.removeEventListener('clarity:navigate', handleRouteChange);
306
+ },
307
+ };
308
+ }
309
+
310
+ // ─── Built-in layout bileşenleri ─────────────────────────────────────────────
311
+
312
+ /**
313
+ * Boş layout — sadece children render eder, ek sarmalama yok.
314
+ * `layout: 'none'` veya doğrudan kullananlar için.
315
+ */
316
+ export function BlankLayout({ children }) {
317
+ return children;
318
+ }
319
+
320
+ /**
321
+ * Gerektiğinde kullanılabilecek basit HTML shell layout.
322
+ * SSR root layout olarak kullanılabilir.
323
+ *
324
+ * @param {object} props
325
+ * @param {*} props.children
326
+ * @param {string} [props.lang='tr']
327
+ * @param {string} [props.title]
328
+ * @param {string} [props.bodyClass]
329
+ */
330
+ export function HTMLShellLayout({ children, lang = 'tr', title = '', bodyClass = '' }) {
331
+ // Bu bileşen SSR ortamında kullanılır — string tabanlı çıktı için basit tag üretimi
332
+ if (typeof document === 'undefined') {
333
+ // SSR ortam
334
+ return {
335
+ __clarity_html_shell__: true,
336
+ lang, title, bodyClass,
337
+ content: children,
338
+ };
339
+ }
340
+ // Client: sadece children
341
+ return children;
342
+ }