@xyd-js/host 0.1.0-build.158

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/app/root.tsx ADDED
@@ -0,0 +1,579 @@
1
+ import React, { useEffect } from "react";
2
+ import { Links, Meta, Outlet, Scripts, ScrollRestoration, redirect } from "react-router";
3
+
4
+ import type { Settings, Appearance, ThemeFont, Font, UserPreferences } from "@xyd-js/core";
5
+ import * as contentClass from "@xyd-js/components/content"; // TODO: move to appearance
6
+
7
+ // @ts-ignore
8
+ import virtualSettings from "virtual:xyd-settings";
9
+ // @ts-ignore
10
+ import { presetUrls } from "virtual:xyd-theme-presets"
11
+
12
+ import colorSchemeScript from "./scripts/colorSchemeScript.ts?raw";
13
+ import bannerHeightScript from "./scripts/bannerHeight.ts?raw";
14
+
15
+ import openfeatureScript from "./scripts/openfeature.js?raw";
16
+ import abTestingScript from "./scripts/abtesting.ts?raw";
17
+
18
+ import growthbookScript from "./scripts/growthbook.js?raw";
19
+ import openfeatureGrowthbookScript from "./scripts/openfeature.growthbook.js?raw";
20
+
21
+ import launchdarklyScript from "./scripts/launchdarkly.js?raw";
22
+ import openfeatureLaunchdarklyScript from "./scripts/openfeature.launchdarkly.js?raw";
23
+
24
+ const { settings, userPreferences } = virtualSettings as { settings: Settings, settingsClone: Settings, userPreferences: UserPreferences }
25
+
26
+ export function HydrateFallback() {
27
+ return <div></div>
28
+ }
29
+
30
+ export function loader({ request }: { request: any }) {
31
+ if (process.env.NODE_ENV === "production") {
32
+ return
33
+ }
34
+
35
+ const slug = getPathname(request.url || "index") || "index"
36
+
37
+ if (settings?.redirects) {
38
+ const shouldRedirect = settings.redirects.find((redirect: any) => redirect.source === slug)
39
+ if (shouldRedirect) {
40
+ return redirect(shouldRedirect.destination)
41
+ }
42
+ }
43
+ }
44
+
45
+ export function Layout({ children }: { children: React.ReactNode }) {
46
+ const colorScheme = clientColorScheme() || settings?.theme?.appearance?.colorScheme || "os"
47
+
48
+ const { component: UserAppearance, classes: UserAppearanceClasses } = userAppearance()
49
+
50
+ return (
51
+ <html
52
+ data-color-scheme={colorScheme}
53
+ data-color-primary={settings?.theme?.appearance?.colors?.primary ? "true" : undefined}
54
+ >
55
+
56
+ <head>
57
+ <PreloadScripts />
58
+ <DefaultMetas />
59
+ <CssLayerFix />
60
+
61
+ <UserFavicon />
62
+ <UserHeadScripts />
63
+ <UserFonts />
64
+
65
+ <Meta />
66
+ <Links />
67
+ <PresetStyles />
68
+ </head>
69
+
70
+ <body className={UserAppearanceClasses}>
71
+ {children}
72
+ <ScrollRestoration />
73
+ <Scripts />
74
+ <UserStyleTokens />
75
+ <UserPreferenceStyles />
76
+ {UserAppearance}
77
+ {/* TODO: in the future better solution? */}
78
+ </body>
79
+ </html>
80
+ );
81
+ }
82
+
83
+ function PreloadScripts() {
84
+ return <>
85
+ <ABTestingScript />
86
+ <ColorSchemeScript />
87
+ {/* TODO: in the future better solution? */}
88
+ <BannerHeightScript />
89
+ </>
90
+ }
91
+
92
+ function clientColorScheme() {
93
+ if (typeof window === "undefined") {
94
+ return
95
+ }
96
+
97
+ try {
98
+ var theme = localStorage.getItem('xyd-color-scheme') || 'auto';
99
+ var isDark = false;
100
+
101
+ if (theme === 'dark') {
102
+ isDark = true;
103
+ } else if (theme === 'auto') {
104
+ isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
105
+ }
106
+
107
+ return isDark ? "dark" : undefined
108
+ } catch (e) {
109
+ // Fallback to system preference if localStorage fails
110
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
111
+ return "dark"
112
+ }
113
+ }
114
+
115
+ return undefined
116
+ }
117
+
118
+ export default function App() {
119
+ return <Outlet />;
120
+ }
121
+
122
+ function getPathname(url: string) {
123
+ const parsedUrl = new URL(url);
124
+ return parsedUrl.pathname.replace(/^\//, '');
125
+ }
126
+
127
+
128
+ function DefaultMetas() {
129
+ return <>
130
+ <meta charSet="utf-8" />
131
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
132
+ </>
133
+ }
134
+
135
+ function ColorSchemeScript() {
136
+ return <script
137
+ dangerouslySetInnerHTML={{
138
+ __html: colorSchemeScript
139
+ }}
140
+ />
141
+ }
142
+
143
+ // TODO: check if it match good
144
+ function BannerHeightScript() {
145
+ const appearance = settings?.theme?.appearance
146
+
147
+ useEffect(() => {
148
+ const bannerHeight = document.querySelector("xyd-banner")?.clientHeight ?? 0;
149
+ if (!bannerHeight) {
150
+ return
151
+ }
152
+ document.documentElement.style.setProperty("--xyd-banner-height", `${String(bannerHeight)}px`)
153
+
154
+ if (!appearance?.banner?.fixed) {
155
+ return
156
+ }
157
+
158
+ document.documentElement.style.setProperty("--xyd-banner-height-dynamic", `${String(bannerHeight)}px`)
159
+ }, [])
160
+
161
+ return null
162
+
163
+ return <script
164
+ dangerouslySetInnerHTML={{
165
+ __html: bannerHeightScript
166
+ }}
167
+ />
168
+ }
169
+
170
+ function UserFavicon() {
171
+ const faviconPath = settings?.theme?.favicon
172
+
173
+ if (!faviconPath) {
174
+ return null
175
+ }
176
+
177
+ return <link rel="icon" type="image/png" sizes="32x32" href={faviconPath}></link>
178
+ }
179
+
180
+ // TODO: !!! in the future more developer-friendly code + bundle optimization !!!
181
+ function ABTestingScript() {
182
+ const abtesting = settings?.integrations?.abtesting
183
+ const providers = settings?.integrations?.abtesting?.providers || {}
184
+
185
+ if (!providers || !Object.keys(providers).length) {
186
+ return null
187
+ }
188
+
189
+ const scripts = [
190
+ openfeatureScript,
191
+ ]
192
+
193
+ if (providers?.growthbook) {
194
+ scripts.push(growthbookScript)
195
+ scripts.push(openfeatureGrowthbookScript)
196
+ }
197
+
198
+ if (providers?.launchdarkly) {
199
+ scripts.push(launchdarklyScript)
200
+ scripts.push(openfeatureLaunchdarklyScript)
201
+ }
202
+
203
+ scripts.push(abTestingScript)
204
+
205
+ const allScripts = scripts.join('\n');
206
+
207
+ // Inject settings into the script
208
+ const scriptWithSettings = `
209
+ window.__xydAbTestingSettings = ${JSON.stringify(abtesting)};
210
+ ${allScripts}
211
+ `;
212
+
213
+ return <script
214
+ dangerouslySetInnerHTML={{
215
+ __html: scriptWithSettings
216
+ }}
217
+ />
218
+ }
219
+
220
+ // TODO: better than <style>?
221
+ function UserStyleTokens() {
222
+ const userCss = generateUserCss(settings?.theme?.appearance)
223
+
224
+ if (!userCss) {
225
+ return null
226
+ }
227
+
228
+ return <>
229
+ <style
230
+ data-appearance
231
+ dangerouslySetInnerHTML={{
232
+ __html: userCss
233
+ }}
234
+ />
235
+ </>
236
+ }
237
+
238
+ function UserPreferenceStyles() {
239
+ const themeColors = userPreferences?.themeColors
240
+ if (!themeColors) {
241
+ return null
242
+ }
243
+
244
+ const coderPreferences = tokensToCss({
245
+ "--user-codetabs-bgcolor": "none",
246
+ "--user-codetabs-color": "none",
247
+ "--user-codetabs-color--active": "currentColor",
248
+ "--user-codetabs-color--hover": "none",
249
+ "--user-coder-code-border-color": "none",
250
+ "--xyd-coder-code-mark-bgcolor": `color-mix(in srgb, ${themeColors.foreground} 10%, transparent)`
251
+ });
252
+ const css = [
253
+ coderPreferences
254
+ ].filter(Boolean).join('\n\n');
255
+
256
+ return <>
257
+ <style dangerouslySetInnerHTML={{ __html: css }} />
258
+ </>
259
+ }
260
+
261
+ // TODO: better than <style>?
262
+ function userAppearance() {
263
+ const theme = {
264
+ searchWidth: settings?.theme?.appearance?.search?.fullWidth ? "100%" : undefined,
265
+ buttonsRounded: cssVarSize("--xyd-border-radius", settings?.theme?.appearance?.buttons?.rounded, "lg"),
266
+ scrollbarColor: settings?.theme?.appearance?.sidebar?.scrollbarColor || undefined
267
+ }
268
+
269
+ const userAppearanceCss = tokensToCss({
270
+ "--xyd-search-width": theme.searchWidth || undefined,
271
+ "--xyd-button-border-radius": theme.buttonsRounded || undefined,
272
+ "--decorator-sidebar-scroll-bgcolor": theme.scrollbarColor || undefined
273
+ })
274
+
275
+ if (!userAppearanceCss) {
276
+ return {
277
+ component: null,
278
+ classes: ""
279
+ }
280
+ }
281
+
282
+ const classes = []
283
+ if (settings?.theme?.appearance?.search?.fullWidth) {
284
+ classes.push(contentClass.SearchButtonFullWidth)
285
+ }
286
+
287
+ return {
288
+ component: <style
289
+ dangerouslySetInnerHTML={{
290
+ __html: userAppearanceCss
291
+ }}
292
+ />,
293
+ classes: classes.join(" ")
294
+ }
295
+ }
296
+
297
+ function generateUserCss(appearance?: Appearance): string {
298
+ if (!appearance) return '';
299
+
300
+ const { colors, cssTokens } = appearance;
301
+
302
+ const lightTokens = {
303
+ ...(colors?.primary ? generateColorTokens(colors.primary) : {}),
304
+ ...(cssTokens ? cssTokens : {}),
305
+ // ...(sidebar?.scrollbarColor ? { "--xyd-toc-scroll-bgcolor": sidebar.scrollbarColor } : {})
306
+ };
307
+ const darkTokens = {
308
+ ...(colors?.light ? generateColorTokens(colors.light) : {}),
309
+ ...(cssTokens ? cssTokens : {}),
310
+ // ...(sidebar?.scrollbarColor ? { "--xyd-toc-scroll-bgcolor": sidebar.scrollbarColor } : {})
311
+ };
312
+
313
+ const lightCss = tokensToCss(lightTokens);
314
+ const darkCss = generateDarkCss(darkTokens);
315
+
316
+ return [lightCss, darkCss].filter(Boolean).join('\n\n');
317
+ }
318
+
319
+ // TODO: typesafe css variables?
320
+ function generateColorTokens(primary: string): Record<string, string> {
321
+ return {
322
+ "--color-primary": primary,
323
+ "--xyd-sidebar-item-bgcolor--active": 'color-mix(in srgb, var(--color-primary) 10%, transparent)',
324
+ "--xyd-sidebar-item-color--active": 'var(--color-primary)',
325
+ "--xyd-toc-item-color--active": 'var(--color-primary)',
326
+ "--theme-color-primary": 'var(--color-primary)',
327
+ "--theme-color-primary-active": 'var(--color-primary)',
328
+ "--color-primary--active": 'color-mix(in srgb, var(--color-primary) 85%, transparent)',
329
+ };
330
+ }
331
+
332
+ function tokensToCss(tokens: Record<string, string | boolean | undefined>, wrapInRoot = true): string {
333
+ if (!Object.keys(tokens).length) {
334
+ return '';
335
+ }
336
+
337
+ const props = Object.entries(tokens)
338
+ .filter(([key, value]) => value !== undefined)
339
+ .map(([key, value]) => `${key}: ${value};`)
340
+ .join('\n');
341
+
342
+ return wrapInRoot ? `:root {\n${props}\n}` : props;
343
+ }
344
+
345
+ function generateDarkCss(tokens: Record<string, string>): string {
346
+ if (!Object.keys(tokens).length) {
347
+ return '';
348
+ }
349
+
350
+ const raw = tokensToCss(tokens, false);
351
+ return [
352
+ `[data-color-scheme="dark"] {\n${raw}\n}`,
353
+ `@media (prefers-color-scheme: dark) {`,
354
+ ` :root:not([data-color-scheme="light"]):not([data-color-scheme="dark"]) {\n ${raw.replace(/\n/g, '\n ')}\n }`,
355
+ `}`
356
+ ].join('\n');
357
+ }
358
+
359
+ function UserHeadScripts() {
360
+ const head = settings?.theme?.head || []
361
+
362
+ if (!head || !head.length) {
363
+ return null
364
+ }
365
+
366
+ return head.map(([tag, rawProps, content]: [string, Record<string, string | boolean>, string?], index: number) => {
367
+ const props: Record<string, any> = { ...rawProps }
368
+
369
+ const onload = props.onLoad || props.onload
370
+
371
+ // Convert onLoad from string to function
372
+ if (typeof onload === 'string') {
373
+ const fnBody = onload
374
+ props.onLoad = () => {
375
+ // eslint-disable-next-line no-new-func
376
+ new Function(fnBody)()
377
+ }
378
+ }
379
+
380
+ delete props.onload
381
+
382
+ if (content) {
383
+ return React.createElement(tag, {
384
+ key: index,
385
+ ...props,
386
+ dangerouslySetInnerHTML: { __html: content },
387
+ })
388
+ }
389
+
390
+ return React.createElement(tag, { key: index, ...props })
391
+ })
392
+ }
393
+
394
+ function PresetStyles() {
395
+ const appearance = settings?.theme?.appearance
396
+ const appearancePresets = appearance?.presets || []
397
+
398
+ return Object.entries(presetUrls).map(([name, url]) => {
399
+ if (appearancePresets.includes(name)) {
400
+ return <link rel="stylesheet" href={url as string} key={name} />
401
+ }
402
+
403
+ return null
404
+ })
405
+ }
406
+
407
+ const cssVarSize = (
408
+ cssTokenPrefix: string,
409
+ size?: "lg" | "md" | "sm" | boolean,
410
+ defaultSize?: "lg" | "md" | "sm"
411
+ ) => {
412
+ if (!size) {
413
+ return undefined
414
+ }
415
+
416
+ if (typeof size === "boolean") {
417
+ return cssVarSize(cssTokenPrefix, defaultSize)
418
+ }
419
+
420
+ const sizes = {
421
+ lg: "large",
422
+ md: "medium",
423
+ sm: "small"
424
+ }
425
+
426
+ const found = sizes[size]
427
+
428
+ if (!found) {
429
+ if (defaultSize) {
430
+ return cssVarSize(cssTokenPrefix, defaultSize)
431
+ }
432
+
433
+ return undefined
434
+ }
435
+
436
+ return `var(${cssTokenPrefix}-${found})`
437
+ }
438
+
439
+ function UserFonts() {
440
+ const fontConfig = settings?.theme?.fonts
441
+
442
+ if (!fontConfig) {
443
+ return null
444
+ }
445
+
446
+ const fontCss = generateFontCss(fontConfig)
447
+
448
+ if (!fontCss) {
449
+ return null
450
+ }
451
+
452
+
453
+ return <>
454
+ <style
455
+ data-fonts
456
+ dangerouslySetInnerHTML={{
457
+ __html: fontCss
458
+ }}
459
+ />
460
+ </>
461
+ }
462
+
463
+
464
+ // TODO: its a fix for css layers - in the future better solution? is <style> order better?
465
+ function CssLayerFix() {
466
+ return <style>
467
+ @layer reset, defaults, defaultfix, components, fabric, templates, decorators, themes, themedecorator, presets, user, overrides;
468
+ </style>
469
+ }
470
+
471
+ function generateFontCss(fontConfig: ThemeFont): string {
472
+ if (!fontConfig) return ''
473
+
474
+ // Handle single font configuration
475
+ if ('family' in fontConfig || 'src' in fontConfig) {
476
+ return generateSingleFontCss(fontConfig, 'body')
477
+ }
478
+
479
+ // Handle separate body and coder fonts
480
+ const { body, coder } = fontConfig as { body: ThemeFont; coder: ThemeFont }
481
+ const bodyCss = body ? generateSingleFontCss(body, 'body') : ''
482
+ const coderCss = coder ? generateSingleFontCss(coder, 'coder') : ''
483
+
484
+ return [bodyCss, coderCss].filter(Boolean).join('\n\n')
485
+ }
486
+
487
+ function generateSingleFontCss(font: ThemeFont, type: 'body' | 'coder'): string {
488
+ if (Array.isArray(font)) {
489
+ // Generate all font-face declarations
490
+ const fontFaces = font.map(f => generateFontFace(f)).join('\n\n')
491
+
492
+ // Use only the first font for CSS variables
493
+ const firstFont = font[0]
494
+ const cssVars = generateCssVars(firstFont, type)
495
+
496
+ return `${fontFaces}
497
+
498
+ @layer user {
499
+ :root {
500
+ ${cssVars}
501
+ }
502
+ }
503
+ `
504
+ }
505
+
506
+ if (!("src" in font)) {
507
+ return ''
508
+ }
509
+
510
+ if (!font.src) return ''
511
+
512
+ const fontFace = generateFontFace(font)
513
+ const cssVars = generateCssVars(font, type)
514
+
515
+ return `${fontFace}
516
+
517
+ @layer user {
518
+ :root {
519
+ ${cssVars}
520
+ }
521
+ }
522
+ `
523
+ }
524
+
525
+ function fontFormat(font: Font) {
526
+ switch (font.format) {
527
+ case "woff2":
528
+ return "woff2"
529
+ case "woff":
530
+ return "woff"
531
+ case "ttf":
532
+ return "ttf"
533
+ }
534
+
535
+ if (font.src?.endsWith(".woff2")) {
536
+ return "woff2"
537
+ }
538
+
539
+ if (font.src?.endsWith(".woff")) {
540
+ return "woff"
541
+ }
542
+
543
+ if (font.src?.endsWith(".ttf")) {
544
+ return "ttf"
545
+ }
546
+
547
+ return ""
548
+ }
549
+
550
+ function generateFontFace(font: Font): string {
551
+ const fontFamily = font.family || 'font'
552
+ const fontWeight = font.weight || '400'
553
+ const format = fontFormat(font)
554
+
555
+ if (format) {
556
+ return `@font-face {
557
+ font-family: '${fontFamily}';
558
+ font-weight: ${fontWeight};
559
+ src: url('${font.src}') format('${format}');
560
+ font-display: swap;
561
+ }`
562
+ } else {
563
+ return `@import url('${font.src}');`
564
+ }
565
+ }
566
+
567
+ function generateCssVars(font: Font, type: 'body' | 'coder'): string {
568
+ const fontFamily = font.family || `font-${type}`
569
+ const fontWeight = font.weight || '400'
570
+
571
+ const cssVars = {
572
+ [`--font-${type}-family`]: fontFamily,
573
+ [`--font-${type}-weight`]: fontWeight,
574
+ }
575
+
576
+ return Object.entries(cssVars)
577
+ .map(([key, value]) => `${key}: ${value};`)
578
+ .join('\n ')
579
+ }
package/app/routes.ts ADDED
@@ -0,0 +1,35 @@
1
+ import {
2
+ index,
3
+ route,
4
+ } from "@react-router/dev/routes";
5
+ import {pathRoutes} from "./pathRoutes";
6
+
7
+ // Declare the global property type
8
+ declare global {
9
+ var __xydBasePath: string;
10
+ }
11
+
12
+ const basePath = globalThis.__xydBasePath
13
+
14
+ const navigation = __xydSettings?.navigation || {sidebar: []};
15
+ const docsRoutes = pathRoutes(basePath, navigation)
16
+
17
+ // TODO: !!!! if not routes found then '*' !!!
18
+ export const routes = [
19
+ ...docsRoutes,
20
+
21
+ // TODO: in the future better sitemap + robots.txt
22
+ route("/sitemap.xml", "./sitemap.ts"),
23
+ route("/robots.txt", "./robots.ts"),
24
+ route(
25
+ "/.well-known/appspecific/com.chrome.devtools.json",
26
+ "./debug-null.tsx",
27
+ ),
28
+ ]
29
+
30
+ if (globalThis.__xydStaticFiles?.length) {
31
+ routes.push(route("/public/*", "./public.ts"))
32
+ }
33
+
34
+ export default routes
35
+