@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/README.md +178 -0
- package/package.json +168 -0
- package/src/analyze.js +534 -0
- package/src/async-state.js +555 -0
- package/src/bundle-runtime.js +35 -0
- package/src/clarity-bundle.js +332 -0
- package/src/clarity-test.js +622 -0
- package/src/cli.js +453 -0
- package/src/codegen.js +1934 -0
- package/src/dev-server.js +362 -0
- package/src/devtools.js +765 -0
- package/src/edge.js +606 -0
- package/src/error-overlay.js +535 -0
- package/src/file-conventions.js +472 -0
- package/src/font.js +513 -0
- package/src/game-loop.js +106 -0
- package/src/head.js +393 -0
- package/src/hydrate.js +292 -0
- package/src/i18n.js +403 -0
- package/src/image.js +352 -0
- package/src/index.js +193 -0
- package/src/islands.js +284 -0
- package/src/isr.js +306 -0
- package/src/layout.js +342 -0
- package/src/lexer.js +572 -0
- package/src/linter.js +547 -0
- package/src/pages-router.js +229 -0
- package/src/parser.js +1108 -0
- package/src/router.js +732 -0
- package/src/runtime.js +1465 -0
- package/src/scoped-css.js +641 -0
- package/src/server-actions.js +439 -0
- package/src/server-data.js +225 -0
- package/src/sourcemap.js +130 -0
- package/src/ssg.js +310 -0
- package/src/ssr.js +621 -0
- package/src/store.js +276 -0
- package/src/transitions.js +438 -0
- package/src/ts-plugin.js +613 -0
- package/src/typegen.js +240 -0
- package/src/vite-plugin.js +447 -0
- package/types/index.d.ts +366 -0
package/src/font.js
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clarity.js — Font Optimization
|
|
3
|
+
*
|
|
4
|
+
* next/font eşdeğeri.
|
|
5
|
+
* Google Fonts otomatik self-hosting, lokal font preload, sıfır layout shift,
|
|
6
|
+
* font-display:swap otomasyonu, subset & unicode-range desteği.
|
|
7
|
+
*
|
|
8
|
+
* ── Google Fonts kullanımı (.clarity şablonunda) ─────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* import { GoogleFont } from '@ozsarman/clarityjs/font'
|
|
11
|
+
*
|
|
12
|
+
* const inter = GoogleFont('Inter', {
|
|
13
|
+
* weights: [400, 500, 700],
|
|
14
|
+
* subsets: ['latin', 'latin-ext'],
|
|
15
|
+
* display: 'swap', // varsayılan
|
|
16
|
+
* variable: '--font-inter',
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* component App() {
|
|
20
|
+
* render {
|
|
21
|
+
* <div class={inter.className}>
|
|
22
|
+
* <h1 style={`font-family: ${inter.style.fontFamily}`}>Başlık</h1>
|
|
23
|
+
* </div>
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* ── Lokal font kullanımı ──────────────────────────────────────────────────────
|
|
28
|
+
*
|
|
29
|
+
* import { LocalFont } from '@ozsarman/clarityjs/font'
|
|
30
|
+
*
|
|
31
|
+
* const myFont = LocalFont({
|
|
32
|
+
* src: [
|
|
33
|
+
* { path: '/fonts/MyFont-Regular.woff2', weight: '400', style: 'normal' },
|
|
34
|
+
* { path: '/fonts/MyFont-Bold.woff2', weight: '700', style: 'normal' },
|
|
35
|
+
* ],
|
|
36
|
+
* variable: '--font-my',
|
|
37
|
+
* display: 'swap',
|
|
38
|
+
* preload: true,
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* ── CSS Variable kullanımı ────────────────────────────────────────────────────
|
|
42
|
+
*
|
|
43
|
+
* // CSS'te:
|
|
44
|
+
* body { font-family: var(--font-inter); }
|
|
45
|
+
*
|
|
46
|
+
* // Component'te:
|
|
47
|
+
* <html class={`${inter.variable} ${mono.variable}`}>
|
|
48
|
+
*
|
|
49
|
+
* ── SSR / Head entegrasyonu ───────────────────────────────────────────────────
|
|
50
|
+
*
|
|
51
|
+
* import { renderFontHead } from '@ozsarman/clarityjs/font'
|
|
52
|
+
*
|
|
53
|
+
* // SSR layout'unda:
|
|
54
|
+
* const headHTML = renderFontHead(); // <link rel="preload"> + <style> etiketleri
|
|
55
|
+
*
|
|
56
|
+
* ── Vite plugin entegrasyonu (otomatik) ──────────────────────────────────────
|
|
57
|
+
*
|
|
58
|
+
* clarityPlugin({ fontOptimization: true })
|
|
59
|
+
* // Build sırasında Google Fonts'u local'e indirir, CSS'i inline eder
|
|
60
|
+
*
|
|
61
|
+
* Author: Claude (Anthropic) + Özdemir Sarman
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
// ─── Kayıtlı fontlar (SSR head injection için) ────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const _registeredFonts = [];
|
|
67
|
+
|
|
68
|
+
// ─── Unique class name üretici ────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
let _fontCounter = 0;
|
|
71
|
+
function _uid() { return `clarity-font-${++_fontCounter}`; }
|
|
72
|
+
|
|
73
|
+
// ─── Google Fonts ─────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Google Fonts entegrasyonu — next/font/google eşdeğeri.
|
|
77
|
+
* Runtime'da preconnect + stylesheet URL üretir.
|
|
78
|
+
* Build sırasında (Vite plugin ile) fontları self-host eder.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} family – Font ailesi adı (örn: 'Inter', 'Roboto Mono')
|
|
81
|
+
* @param {object} opts
|
|
82
|
+
* @param {number[]} [opts.weights=[400]] – Yüklenecek font ağırlıkları
|
|
83
|
+
* @param {string[]} [opts.subsets=['latin']] – unicode subset'leri
|
|
84
|
+
* @param {string} [opts.display='swap'] – CSS font-display değeri
|
|
85
|
+
* @param {boolean} [opts.preload=true] – <link rel="preload"> ekle
|
|
86
|
+
* @param {string[]} [opts.axes] – Variable font axis'leri (örn: ['wght'])
|
|
87
|
+
* @param {string} [opts.variable] – CSS custom property adı (örn: '--font-inter')
|
|
88
|
+
* @param {boolean} [opts.italic=false] – italic varyantları da yükle
|
|
89
|
+
* @returns {FontObject}
|
|
90
|
+
*/
|
|
91
|
+
export function GoogleFont(family, opts = {}) {
|
|
92
|
+
const {
|
|
93
|
+
weights = [400],
|
|
94
|
+
subsets = ['latin'],
|
|
95
|
+
display = 'swap',
|
|
96
|
+
preload = true,
|
|
97
|
+
axes = null,
|
|
98
|
+
variable = null,
|
|
99
|
+
italic = false,
|
|
100
|
+
} = opts;
|
|
101
|
+
|
|
102
|
+
const className = _uid();
|
|
103
|
+
const fontFamily = `'${family}', sans-serif`;
|
|
104
|
+
const cssVariable = variable ?? `--clarity-font-${family.toLowerCase().replace(/\s+/g, '-')}`;
|
|
105
|
+
|
|
106
|
+
// Google Fonts URL oluştur (v2 API)
|
|
107
|
+
const gFamilyStr = _buildGoogleFamilyStr(family, weights, italic, axes);
|
|
108
|
+
const gSubsets = subsets.join(',');
|
|
109
|
+
const googleUrl = `https://fonts.googleapis.com/css2?family=${gFamilyStr}&subset=${gSubsets}&display=${display}`;
|
|
110
|
+
|
|
111
|
+
const fontObj = {
|
|
112
|
+
className,
|
|
113
|
+
variable: cssVariable,
|
|
114
|
+
style: { fontFamily },
|
|
115
|
+
// SSR için gerekli bilgiler
|
|
116
|
+
__clarity_font__: true,
|
|
117
|
+
__type__: 'google',
|
|
118
|
+
family,
|
|
119
|
+
googleUrl,
|
|
120
|
+
preload,
|
|
121
|
+
display,
|
|
122
|
+
weights,
|
|
123
|
+
subsets,
|
|
124
|
+
cssVariable,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
_registeredFonts.push(fontObj);
|
|
128
|
+
|
|
129
|
+
// Client-side: DOM'a inject et (SSR değilse)
|
|
130
|
+
if (typeof document !== 'undefined') {
|
|
131
|
+
_injectGoogleFont(fontObj);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return fontObj;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function _buildGoogleFamilyStr(family, weights, italic, axes) {
|
|
138
|
+
const name = family.replace(/ /g, '+');
|
|
139
|
+
if (axes) {
|
|
140
|
+
// Variable font — axis değerleri
|
|
141
|
+
const axisStr = axes.join(',');
|
|
142
|
+
const wghtRange = `${Math.min(...weights)}..${Math.max(...weights)}`;
|
|
143
|
+
return `${name}:${axisStr}@${wghtRange}`;
|
|
144
|
+
}
|
|
145
|
+
if (italic) {
|
|
146
|
+
const parts = weights.flatMap(w => [`0,${w}`, `1,${w}`]).join(';');
|
|
147
|
+
return `${name}:ital,wght@${parts}`;
|
|
148
|
+
}
|
|
149
|
+
const wStr = weights.join(';');
|
|
150
|
+
return `${name}:wght@${wStr}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _injectGoogleFont(fontObj) {
|
|
154
|
+
// Preconnect
|
|
155
|
+
_injectLinkOnce('preconnect', 'https://fonts.googleapis.com');
|
|
156
|
+
_injectLinkOnce('preconnect', 'https://fonts.gstatic.com', { crossorigin: true });
|
|
157
|
+
|
|
158
|
+
// Stylesheet
|
|
159
|
+
_injectLinkOnce('stylesheet', fontObj.googleUrl, { id: `gf-${fontObj.className}` });
|
|
160
|
+
|
|
161
|
+
// CSS variable → className rule
|
|
162
|
+
_injectFontClassStyle(fontObj.className, fontObj.cssVariable, fontObj.style.fontFamily);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Local Fonts ──────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Lokal font tanımı — next/font/local eşdeğeri.
|
|
169
|
+
* Sistemde kayıtlı veya /public/fonts/ altındaki fontlar için @font-face üretir.
|
|
170
|
+
*
|
|
171
|
+
* @param {object} opts
|
|
172
|
+
* @param {Array<{path:string, weight?:string, style?:string, format?:string}>} opts.src
|
|
173
|
+
* @param {string} [opts.variable] – CSS custom property adı
|
|
174
|
+
* @param {string} [opts.display='swap']
|
|
175
|
+
* @param {boolean} [opts.preload=true]
|
|
176
|
+
* @param {string} [opts.family] – Belirtilmezse dosya adından çıkarılır
|
|
177
|
+
* @param {string} [opts.unicodeRange]
|
|
178
|
+
* @returns {FontObject}
|
|
179
|
+
*/
|
|
180
|
+
export function LocalFont(opts = {}) {
|
|
181
|
+
const {
|
|
182
|
+
src,
|
|
183
|
+
variable = null,
|
|
184
|
+
display = 'swap',
|
|
185
|
+
preload = true,
|
|
186
|
+
family = null,
|
|
187
|
+
unicodeRange = null,
|
|
188
|
+
} = opts;
|
|
189
|
+
|
|
190
|
+
if (!src || !Array.isArray(src) || src.length === 0) {
|
|
191
|
+
throw new Error('[clarity/font] LocalFont: src dizisi zorunludur');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const className = _uid();
|
|
195
|
+
const fontFamily = family ?? _inferFamilyFromPath(src[0].path);
|
|
196
|
+
const cssVariable = variable ?? `--clarity-font-${fontFamily.toLowerCase().replace(/\s+/g, '-')}`;
|
|
197
|
+
|
|
198
|
+
const fontFaceCSS = src.map(({ path, weight = '400', style: fStyle = 'normal', format }) =>
|
|
199
|
+
_buildFontFaceRule(fontFamily, path, weight, fStyle, display, unicodeRange, format)
|
|
200
|
+
).join('\n');
|
|
201
|
+
|
|
202
|
+
const fontObj = {
|
|
203
|
+
className,
|
|
204
|
+
variable: cssVariable,
|
|
205
|
+
style: { fontFamily: `'${fontFamily}', sans-serif` },
|
|
206
|
+
__clarity_font__: true,
|
|
207
|
+
__type__: 'local',
|
|
208
|
+
family: fontFamily,
|
|
209
|
+
src,
|
|
210
|
+
preload,
|
|
211
|
+
display,
|
|
212
|
+
fontFaceCSS,
|
|
213
|
+
cssVariable,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
_registeredFonts.push(fontObj);
|
|
217
|
+
|
|
218
|
+
if (typeof document !== 'undefined') {
|
|
219
|
+
_injectLocalFont(fontObj);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return fontObj;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function _inferFamilyFromPath(path) {
|
|
226
|
+
const name = path.split('/').pop().replace(/\.(woff2?|ttf|otf|eot)$/i, '');
|
|
227
|
+
return name.replace(/[-_](Regular|Bold|Light|Medium|SemiBold|Italic|Black)/i, '').trim() || 'CustomFont';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function _buildFontFaceRule(family, path, weight, style, display, unicodeRange, format) {
|
|
231
|
+
const formatHint = format
|
|
232
|
+
? `format('${format}')`
|
|
233
|
+
: path.endsWith('.woff2') ? "format('woff2')"
|
|
234
|
+
: path.endsWith('.woff') ? "format('woff')"
|
|
235
|
+
: path.endsWith('.ttf') ? "format('truetype')"
|
|
236
|
+
: path.endsWith('.otf') ? "format('opentype')"
|
|
237
|
+
: '';
|
|
238
|
+
|
|
239
|
+
return `@font-face {
|
|
240
|
+
font-family: '${family}';
|
|
241
|
+
src: url('${path}') ${formatHint};
|
|
242
|
+
font-weight: ${weight};
|
|
243
|
+
font-style: ${style};
|
|
244
|
+
font-display: ${display};${unicodeRange ? `\n unicode-range: ${unicodeRange};` : ''}
|
|
245
|
+
}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function _injectLocalFont(fontObj) {
|
|
249
|
+
// @font-face kurallarını <style> olarak göm
|
|
250
|
+
const styleId = `lf-${fontObj.className}`;
|
|
251
|
+
if (!document.getElementById(styleId)) {
|
|
252
|
+
const style = document.createElement('style');
|
|
253
|
+
style.id = styleId;
|
|
254
|
+
style.textContent = fontObj.fontFaceCSS;
|
|
255
|
+
document.head.appendChild(style);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Preload: sadece woff2 dosyalarını preload et
|
|
259
|
+
if (fontObj.preload) {
|
|
260
|
+
for (const { path, format } of fontObj.src) {
|
|
261
|
+
const isWoff2 = (format === 'woff2') || path.endsWith('.woff2');
|
|
262
|
+
if (isWoff2) {
|
|
263
|
+
const link = document.createElement('link');
|
|
264
|
+
link.rel = 'preload';
|
|
265
|
+
link.as = 'font';
|
|
266
|
+
link.href = path;
|
|
267
|
+
link.type = 'font/woff2';
|
|
268
|
+
link.setAttribute('crossorigin', 'anonymous');
|
|
269
|
+
document.head.appendChild(link);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// className CSS rule
|
|
275
|
+
_injectFontClassStyle(fontObj.className, fontObj.cssVariable, fontObj.style.fontFamily);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ─── SSR: head HTML üret ──────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Kayıtlı tüm fontlar için SSR <head> HTML'i üret.
|
|
282
|
+
* `renderToDocument()` içinde veya manuel layout'larda çağrılır.
|
|
283
|
+
*
|
|
284
|
+
* @returns {string} HTML string — <link> + <style> etiketleri
|
|
285
|
+
*/
|
|
286
|
+
export function renderFontHead() {
|
|
287
|
+
const parts = [];
|
|
288
|
+
|
|
289
|
+
for (const font of _registeredFonts) {
|
|
290
|
+
if (font.__type__ === 'google') {
|
|
291
|
+
// Preconnect
|
|
292
|
+
parts.push(`<link rel="preconnect" href="https://fonts.googleapis.com">`);
|
|
293
|
+
parts.push(`<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>`);
|
|
294
|
+
// Stylesheet — preload ilk, sonra stylesheet
|
|
295
|
+
if (font.preload) {
|
|
296
|
+
parts.push(`<link rel="preload" as="style" href="${_escAttr(font.googleUrl)}">`);
|
|
297
|
+
}
|
|
298
|
+
parts.push(`<link rel="stylesheet" href="${_escAttr(font.googleUrl)}">`);
|
|
299
|
+
} else if (font.__type__ === 'local') {
|
|
300
|
+
// @font-face CSS
|
|
301
|
+
parts.push(`<style id="lf-${font.className}">${font.fontFaceCSS}</style>`);
|
|
302
|
+
// woff2 preload
|
|
303
|
+
if (font.preload) {
|
|
304
|
+
for (const { path, format } of font.src) {
|
|
305
|
+
const isWoff2 = (format === 'woff2') || path.endsWith('.woff2');
|
|
306
|
+
if (isWoff2) {
|
|
307
|
+
parts.push(`<link rel="preload" as="font" type="font/woff2" href="${_escAttr(path)}" crossorigin="anonymous">`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// CSS variable + className rule
|
|
314
|
+
parts.push(`<style>.${font.className}{font-family:${font.style.fontFamily};${font.cssVariable}:${font.style.fontFamily}}</style>`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return parts.join('\n');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Kayıtlı fontları temizle (SSR arasında state sıfırlama için).
|
|
322
|
+
*/
|
|
323
|
+
export function resetFontRegistry() {
|
|
324
|
+
_registeredFonts.length = 0;
|
|
325
|
+
_fontCounter = 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Kayıtlı font listesini döndür (Vite plugin build entegrasyonu için).
|
|
330
|
+
*/
|
|
331
|
+
export function getRegisteredFonts() {
|
|
332
|
+
return [..._registeredFonts];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ─── Build-time: Google Fonts self-hosting ────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Google Fonts CSS'ini indir ve @font-face kurallarını woff2 dosyaları
|
|
339
|
+
* ile birlikte outDir'e kaydet. Vite plugin `generateBundle` hook'undan çağrılır.
|
|
340
|
+
*
|
|
341
|
+
* @param {Array} fonts – getRegisteredFonts() çıktısı (sadece google tipi)
|
|
342
|
+
* @param {string} outDir – dist/ dizini
|
|
343
|
+
* @param {string} fontsDir – font dosyaları alt dizini (default: 'fonts')
|
|
344
|
+
* @returns {Promise<{ css: string, downloaded: number }>}
|
|
345
|
+
*/
|
|
346
|
+
export async function downloadGoogleFonts(fonts, outDir, fontsDir = 'fonts') {
|
|
347
|
+
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
348
|
+
const { join, resolve } = await import('node:path');
|
|
349
|
+
const { fetch: nodeFetch } = await _getFetch();
|
|
350
|
+
|
|
351
|
+
const outFontsDir = join(resolve(outDir), fontsDir);
|
|
352
|
+
await mkdir(outFontsDir, { recursive: true });
|
|
353
|
+
|
|
354
|
+
let allCSS = '';
|
|
355
|
+
let downloaded = 0;
|
|
356
|
+
|
|
357
|
+
for (const font of fonts) {
|
|
358
|
+
if (font.__type__ !== 'google') continue;
|
|
359
|
+
|
|
360
|
+
// Google Fonts CSS'ini çek (woff2 URL'lerini bulmak için)
|
|
361
|
+
let css;
|
|
362
|
+
try {
|
|
363
|
+
const resp = await nodeFetch(font.googleUrl, {
|
|
364
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; clarity-js build)' },
|
|
365
|
+
});
|
|
366
|
+
css = await resp.text();
|
|
367
|
+
} catch (err) {
|
|
368
|
+
console.warn(`[clarity/font] Google Fonts indirilemedi (${font.family}): ${err.message}`);
|
|
369
|
+
// Fallback: orijinal URL'yi kullan
|
|
370
|
+
allCSS += `/* ${font.family} — bağlantı hatası, CDN kullanılıyor */\n`;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// @font-face içindeki woff2 URL'lerini bul ve indir
|
|
375
|
+
const urlRegex = /url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.woff2)\)/g;
|
|
376
|
+
let match;
|
|
377
|
+
let localCSS = css;
|
|
378
|
+
|
|
379
|
+
while ((match = urlRegex.exec(css)) !== null) {
|
|
380
|
+
const remoteUrl = match[1];
|
|
381
|
+
const fileName = remoteUrl.split('/').pop().split('?')[0];
|
|
382
|
+
const localPath = join(outFontsDir, fileName);
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const fontResp = await nodeFetch(remoteUrl);
|
|
386
|
+
const buffer = Buffer.from(await fontResp.arrayBuffer());
|
|
387
|
+
await writeFile(localPath, buffer);
|
|
388
|
+
localCSS = localCSS.replace(remoteUrl, `/${fontsDir}/${fileName}`);
|
|
389
|
+
downloaded++;
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.warn(`[clarity/font] Font dosyası indirilemedi (${fileName}): ${err.message}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
allCSS += localCSS + '\n';
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Self-hosted CSS dosyasını yaz
|
|
399
|
+
if (allCSS) {
|
|
400
|
+
await writeFile(join(resolve(outDir), 'fonts.css'), allCSS, 'utf8');
|
|
401
|
+
console.log(`[clarity/font] ✅ ${downloaded} font dosyası indirildi → ${fontsDir}/`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return { css: allCSS, downloaded };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ─── Vite plugin entegrasyon hook'u ──────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Vite plugin'i için font optimization hook'u.
|
|
411
|
+
* clarityPlugin({ fontOptimization: true }) ile otomatik aktif olur.
|
|
412
|
+
*
|
|
413
|
+
* @param {object} opts
|
|
414
|
+
* @param {string} opts.outDir
|
|
415
|
+
* @param {string} [opts.fontsDir='fonts']
|
|
416
|
+
*/
|
|
417
|
+
export async function optimizeFonts(opts = {}) {
|
|
418
|
+
const { outDir = 'dist', fontsDir = 'fonts' } = opts;
|
|
419
|
+
const googleFonts = _registeredFonts.filter(f => f.__type__ === 'google');
|
|
420
|
+
if (googleFonts.length === 0) return;
|
|
421
|
+
return downloadGoogleFonts(googleFonts, outDir, fontsDir);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ─── Font display utility ─────────────────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Tüm @font-face kurallarına font-display değerini uygula.
|
|
428
|
+
* Mevcut bir CSS string'ine uygulanır.
|
|
429
|
+
*
|
|
430
|
+
* @param {string} css – Mevcut @font-face CSS
|
|
431
|
+
* @param {string} [display='swap']
|
|
432
|
+
* @returns {string} Güncellenmiş CSS
|
|
433
|
+
*/
|
|
434
|
+
export function applyFontDisplay(css, display = 'swap') {
|
|
435
|
+
// Zaten font-display varsa güncelle, yoksa ekle
|
|
436
|
+
return css.replace(
|
|
437
|
+
/(@font-face\s*\{[^}]*?)(font-display\s*:[^;]*;)?([^}]*?\})/gs,
|
|
438
|
+
(match, before, existing, after) => {
|
|
439
|
+
if (existing) {
|
|
440
|
+
return `${before}font-display: ${display};${after}`;
|
|
441
|
+
}
|
|
442
|
+
return `${before}font-display: ${display};\n ${after}`;
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ─── Sistem fontları (zero-layout-shift, zero-download) ──────────────────────
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Hazır sistem font stack'leri — indirme yok, layout shift yok.
|
|
451
|
+
* next/font/local + system-ui stack eşdeğeri.
|
|
452
|
+
*/
|
|
453
|
+
export const SystemFonts = {
|
|
454
|
+
/** Modern cross-platform sans-serif */
|
|
455
|
+
sans: {
|
|
456
|
+
className: 'clarity-font-system-sans',
|
|
457
|
+
variable: '--font-system-sans',
|
|
458
|
+
style: { fontFamily: 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' },
|
|
459
|
+
},
|
|
460
|
+
/** Sistem mono */
|
|
461
|
+
mono: {
|
|
462
|
+
className: 'clarity-font-system-mono',
|
|
463
|
+
variable: '--font-system-mono',
|
|
464
|
+
style: { fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace' },
|
|
465
|
+
},
|
|
466
|
+
/** Serif */
|
|
467
|
+
serif: {
|
|
468
|
+
className: 'clarity-font-system-serif',
|
|
469
|
+
variable: '--font-system-serif',
|
|
470
|
+
style: { fontFamily: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif' },
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
475
|
+
|
|
476
|
+
function _injectLinkOnce(rel, href, attrs = {}) {
|
|
477
|
+
if (typeof document === 'undefined') return;
|
|
478
|
+
const existing = document.querySelector(`link[rel="${rel}"][href="${href}"]`);
|
|
479
|
+
if (existing) return;
|
|
480
|
+
const link = document.createElement('link');
|
|
481
|
+
link.rel = rel;
|
|
482
|
+
link.href = href;
|
|
483
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
484
|
+
if (v === true) link.setAttribute(k, '');
|
|
485
|
+
else if (k !== 'id' || !v) link.setAttribute(k, v);
|
|
486
|
+
}
|
|
487
|
+
if (attrs.id) link.id = attrs.id;
|
|
488
|
+
document.head.appendChild(link);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function _injectFontClassStyle(className, cssVar, fontFamily) {
|
|
492
|
+
if (typeof document === 'undefined') return;
|
|
493
|
+
const styleId = `cls-${className}`;
|
|
494
|
+
if (document.getElementById(styleId)) return;
|
|
495
|
+
const style = document.createElement('style');
|
|
496
|
+
style.id = styleId;
|
|
497
|
+
style.textContent = `.${className}{font-family:${fontFamily};${cssVar}:${fontFamily}}`;
|
|
498
|
+
document.head.appendChild(style);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function _escAttr(str) {
|
|
502
|
+
return String(str ?? '').replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function _getFetch() {
|
|
506
|
+
// Node 18+ global fetch destekler; daha eski sürümlerde node-fetch
|
|
507
|
+
if (typeof globalThis.fetch === 'function') return { fetch: globalThis.fetch };
|
|
508
|
+
try {
|
|
509
|
+
return await import('node-fetch');
|
|
510
|
+
} catch {
|
|
511
|
+
throw new Error('[clarity/font] fetch API gerekli (Node 18+ veya node-fetch paketi)');
|
|
512
|
+
}
|
|
513
|
+
}
|
package/src/game-loop.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clarity.js — game / animation loop helper
|
|
3
|
+
*
|
|
4
|
+
* A small, honest requestAnimationFrame loop. Clarity is a DOM/UI framework,
|
|
5
|
+
* not a WebGL engine — but DOM games and animations are perfectly doable, and
|
|
6
|
+
* this gives them a clean fixed/variable timestep loop that integrates with the
|
|
7
|
+
* component lifecycle.
|
|
8
|
+
*
|
|
9
|
+
* import { createGameLoop, useRaf } from '@ozsarman/clarityjs/game-loop';
|
|
10
|
+
*
|
|
11
|
+
* const loop = createGameLoop({
|
|
12
|
+
* fps: 60, // fixed timestep (0 = uncapped, native rAF)
|
|
13
|
+
* update: (dt) => { ... }, // dt in seconds
|
|
14
|
+
* render: () => { ... }, // optional, after each update batch
|
|
15
|
+
* });
|
|
16
|
+
* loop.start(); loop.stop(); loop.toggle();
|
|
17
|
+
*
|
|
18
|
+
* // Inside a component — auto-stops on unmount:
|
|
19
|
+
* useRaf((dt) => { x += speed * dt; });
|
|
20
|
+
*
|
|
21
|
+
* Author: Claude (Anthropic) + Özdemir Şarman
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { onCleanup } from './runtime.js';
|
|
25
|
+
|
|
26
|
+
const _raf = (cb) =>
|
|
27
|
+
(typeof globalThis.requestAnimationFrame === 'function')
|
|
28
|
+
? globalThis.requestAnimationFrame(cb)
|
|
29
|
+
: setTimeout(() => cb(Date.now()), 16);
|
|
30
|
+
|
|
31
|
+
const _caf = (id) =>
|
|
32
|
+
(typeof globalThis.cancelAnimationFrame === 'function')
|
|
33
|
+
? globalThis.cancelAnimationFrame(id)
|
|
34
|
+
: clearTimeout(id);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a game loop.
|
|
38
|
+
* @param {object} opts
|
|
39
|
+
* @param {(dt:number)=>void} [opts.update] per-step callback, dt in seconds
|
|
40
|
+
* @param {()=>void} [opts.render] called once after the update(s) each frame
|
|
41
|
+
* @param {number} [opts.fps=0] fixed timestep target; 0 = uncapped
|
|
42
|
+
* @param {number} [opts.maxStepsPerFrame=5] clamp to avoid spiral-of-death
|
|
43
|
+
* @returns {{ start, stop, toggle, isRunning }}
|
|
44
|
+
*/
|
|
45
|
+
export function createGameLoop(opts = {}) {
|
|
46
|
+
const { update, render, fps = 0, maxStepsPerFrame = 5 } = opts;
|
|
47
|
+
const step = fps > 0 ? 1 / fps : 0; // seconds per fixed step
|
|
48
|
+
let rafId = null;
|
|
49
|
+
let last = 0;
|
|
50
|
+
let acc = 0;
|
|
51
|
+
let running = false;
|
|
52
|
+
|
|
53
|
+
function frame(now) {
|
|
54
|
+
if (!running) return;
|
|
55
|
+
const dt = last ? (now - last) / 1000 : 0;
|
|
56
|
+
last = now;
|
|
57
|
+
|
|
58
|
+
if (step > 0) {
|
|
59
|
+
acc += dt;
|
|
60
|
+
let steps = 0;
|
|
61
|
+
while (acc >= step && steps < maxStepsPerFrame) {
|
|
62
|
+
if (update) update(step);
|
|
63
|
+
acc -= step;
|
|
64
|
+
steps++;
|
|
65
|
+
}
|
|
66
|
+
if (steps === maxStepsPerFrame) acc = 0; // dropped frames — resync
|
|
67
|
+
} else if (update) {
|
|
68
|
+
update(dt);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (render) render();
|
|
72
|
+
rafId = _raf(frame);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
start() {
|
|
77
|
+
if (running) return;
|
|
78
|
+
running = true;
|
|
79
|
+
last = 0;
|
|
80
|
+
acc = 0;
|
|
81
|
+
rafId = _raf(frame);
|
|
82
|
+
},
|
|
83
|
+
stop() {
|
|
84
|
+
running = false;
|
|
85
|
+
if (rafId != null) _caf(rafId);
|
|
86
|
+
rafId = null;
|
|
87
|
+
},
|
|
88
|
+
toggle() { this.isRunning() ? this.stop() : this.start(); },
|
|
89
|
+
isRunning() { return running; },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Run a callback every animation frame, auto-stopping when the surrounding
|
|
95
|
+
* component unmounts. Returns the loop so you can stop/toggle it manually too.
|
|
96
|
+
*
|
|
97
|
+
* @param {(dt:number)=>void} fn per-frame callback, dt in seconds
|
|
98
|
+
* @param {object} [opts] same options as createGameLoop (minus update)
|
|
99
|
+
* @returns {{ start, stop, toggle, isRunning }}
|
|
100
|
+
*/
|
|
101
|
+
export function useRaf(fn, opts = {}) {
|
|
102
|
+
const loop = createGameLoop({ ...opts, update: fn });
|
|
103
|
+
loop.start();
|
|
104
|
+
try { onCleanup(() => loop.stop()); } catch { /* outside a component — caller owns lifecycle */ }
|
|
105
|
+
return loop;
|
|
106
|
+
}
|