@clidey/dory 0.47.0 → 0.48.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.
Files changed (46) hide show
  1. package/bin/dist/dory.js +103 -45
  2. package/bin/dist/dory.js.map +1 -1
  3. package/index.html +11 -15
  4. package/package.json +35 -29
  5. package/scripts/render-routes.mjs +45 -14
  6. package/src/components/routes.tsx +19 -22
  7. package/src/components/store.ts +5 -5
  8. package/src/config/mdx.js +7 -3
  9. package/src/config/mdx.ts +6 -2
  10. package/src/entry-server.tsx +53 -20
  11. package/src/main.tsx +9 -1
  12. package/src/mdx/code.tsx +8 -5
  13. package/src/mdx/fence.tsx +10 -2
  14. package/src/mdx/icon.tsx +21 -14
  15. package/src/mdx/mermaid.tsx +10 -1
  16. package/src/mdx/processor.test.ts +47 -0
  17. package/src/mdx/step.tsx +3 -3
  18. package/src/plugins/analytics-injector.ts +7 -5
  19. package/src/plugins/docs-assets.ts +5 -4
  20. package/src/plugins/frontmatter-dev-server.ts +8 -159
  21. package/src/plugins/frontmatter-generator.ts +37 -129
  22. package/src/plugins/html-files-middleware.test.ts +21 -0
  23. package/src/plugins/html-files-middleware.ts +27 -5
  24. package/src/plugins/html-metadata-injector.ts +94 -53
  25. package/src/plugins/llm-txt-dev-server.ts +25 -178
  26. package/src/plugins/llm-txt-generator.ts +94 -133
  27. package/src/plugins/prerender.ts +156 -50
  28. package/src/plugins/robots-generator.ts +15 -16
  29. package/src/plugins/sanitize.test.ts +104 -0
  30. package/src/plugins/sanitize.ts +90 -59
  31. package/src/plugins/shared.ts +139 -0
  32. package/src/plugins/sitemap-generator.ts +58 -44
  33. package/src/theme.scss +138 -15
  34. package/src/types/config.ts +42 -2
  35. package/src/types/globals.d.ts +7 -0
  36. package/src/ui/header.tsx +12 -4
  37. package/src/ui/layout.tsx +66 -47
  38. package/src/ui/table-of-content.tsx +19 -2
  39. package/src/utils/colors.ts +38 -18
  40. package/src/utils/fonts.ts +55 -12
  41. package/src/utils/theme.ts +29 -0
  42. package/vite.config.embed-loader.ts +3 -11
  43. package/vite.config.embed-widget.ts +3 -10
  44. package/vite.config.ts +11 -7
  45. package/scripts/generate-frontmatter-json.js +0 -154
  46. package/scripts/generate-llm-txt.js +0 -115
package/src/theme.scss CHANGED
@@ -1,39 +1,162 @@
1
1
  :root {
2
- font-synthesis: none;
3
2
  text-rendering: optimizeLegibility;
4
3
  -webkit-font-smoothing: antialiased;
5
4
  -moz-osx-font-smoothing: grayscale;
6
5
 
7
6
  // Brand colors (dynamically set from dory.json via loadColors())
8
- // These variables are set at runtime:
9
- // --brand-primary: primary color
10
- // --brand-light: light mode accent
11
- // --brand-dark: dark mode accent
12
- // --brand-foreground: legacy/fallback (set to primary)
13
-
14
- // Default fallback colors (will be overridden by loadColors())
15
7
  --brand-foreground: #0ea5e9;
16
8
  --brand-primary: #0ea5e9;
17
9
  --brand-light: #38bdf8;
18
10
  --brand-dark: #0284c7;
19
11
 
12
+ // Font families
13
+ --font-body: inherit;
14
+ --font-heading: inherit;
15
+ --font-mono: inherit;
16
+
20
17
  // ShadCN/clidey/ux compatible primary color
21
- // Using light color for light mode
22
18
  --primary: var(--brand-light);
23
19
  --primary-rgb: var(--brand-light-rgb);
24
-
20
+
25
21
  --ring: rgba(0, 0, 0, 0.1);
26
22
  }
27
23
 
28
- // Dark mode overrides
29
24
  :root.dark {
30
- // Use dark color for dark mode
31
25
  --primary: var(--brand-dark);
32
26
  --primary-rgb: var(--brand-dark-rgb);
33
-
34
27
  --ring: rgba(255, 255, 255, 0.1);
35
28
  }
36
-
29
+
30
+ // Apply background from CSS variables
31
+ html, body, #app {
32
+ background-color: var(--background);
33
+ color: var(--foreground);
34
+ }
35
+
36
+ // === THEMED MODE: override Tailwind utility classes ===
37
+ [data-dory-themed="true"] {
38
+ // Navigation sidebar text
39
+ .dory-navigation {
40
+ a {
41
+ color: var(--muted-foreground) !important;
42
+ }
43
+
44
+ a:hover {
45
+ color: var(--foreground) !important;
46
+ }
47
+
48
+ a.font-semibold {
49
+ color: var(--brand-foreground) !important;
50
+ }
51
+
52
+ h2, h3 {
53
+ color: var(--foreground) !important;
54
+ }
55
+
56
+ ul[role="list"] {
57
+ border-color: var(--border) !important;
58
+ }
59
+ }
60
+
61
+ // Override hardcoded Tailwind text colors
62
+ .text-slate-500,
63
+ .text-slate-400,
64
+ .text-slate-600,
65
+ .text-zinc-500,
66
+ .text-zinc-400,
67
+ .text-gray-500,
68
+ .text-gray-400 {
69
+ color: var(--muted-foreground) !important;
70
+ }
71
+
72
+ .text-slate-900,
73
+ .text-zinc-900,
74
+ .text-gray-900 {
75
+ color: var(--foreground) !important;
76
+ }
77
+
78
+ // Override hardcoded Tailwind border colors
79
+ .border-slate-100,
80
+ .border-slate-200,
81
+ .border-slate-800,
82
+ .border-zinc-200,
83
+ .border-zinc-800 {
84
+ border-color: var(--border) !important;
85
+ }
86
+
87
+ // Header
88
+ .dory-header {
89
+ border-color: var(--border) !important;
90
+ }
91
+
92
+ // Accordion & divider borders — subtle low opacity
93
+ [data-slot="accordion-item"],
94
+ hr,
95
+ .border-t,
96
+ .border-b {
97
+ border-color: color-mix(in oklch, var(--foreground) 8%, transparent) !important;
98
+ }
99
+ }
100
+
101
+ // === FONT OVERRIDES ===
102
+ [data-dory-themed="true"] {
103
+ // Page title — large display serif
104
+ .dory-page-title {
105
+ font-family: var(--font-heading, var(--font-body, inherit));
106
+ font-weight: 380;
107
+ font-variation-settings: "opsz" 144, "SOFT" 30;
108
+ letter-spacing: -0.02em;
109
+ }
110
+
111
+ // Subtitle / eyebrow — mono, muted amber
112
+ .dory-subtitle {
113
+ font-family: var(--font-mono, monospace);
114
+ color: var(--brand-foreground);
115
+ opacity: 0.8;
116
+ letter-spacing: 0.15em;
117
+ font-size: 11px;
118
+ }
119
+
120
+ // Article headings — serif with proper optical sizing
121
+ article h2 {
122
+ font-family: var(--font-heading, var(--font-body, inherit));
123
+ font-weight: 400;
124
+ font-variation-settings: "opsz" 48, "SOFT" 50;
125
+ letter-spacing: -0.015em;
126
+ }
127
+
128
+ article h3,
129
+ article h4 {
130
+ font-family: var(--font-body, inherit);
131
+ font-weight: 600;
132
+ }
133
+
134
+ code, pre {
135
+ font-family: var(--font-mono, monospace);
136
+ }
137
+ }
138
+
139
+ // === CODE BLOCK STYLING ===
140
+ // Make code blocks translucent to match app feel
141
+ [data-dory-themed="true"] {
142
+ [data-slot="card"],
143
+ .bg-card {
144
+ background-color: transparent !important;
145
+ border-color: var(--border) !important;
146
+ }
147
+
148
+ pre {
149
+ background-color: transparent !important;
150
+ }
151
+
152
+ // Inline code
153
+ .dory-mdx-code {
154
+ background-color: transparent !important;
155
+ border: 1px solid var(--border);
156
+ }
157
+ }
158
+
159
+ // === DEFAULT STYLES ===
37
160
  article h2:first-of-type {
38
161
  display: none;
39
162
  }
@@ -69,4 +192,4 @@ article img {
69
192
  outline: none !important;
70
193
  border: 0px;
71
194
  box-shadow: none !important;
72
- }
195
+ }
@@ -6,12 +6,29 @@ export interface DoryColors {
6
6
  primary: string;
7
7
  light: string;
8
8
  dark: string;
9
+ background?: string;
10
+ surface?: string;
11
+ surfaceElevated?: string;
12
+ foreground?: string;
13
+ mutedForeground?: string;
14
+ border?: string;
15
+ accent?: string;
16
+ }
17
+
18
+ export interface DoryFontDefinition {
19
+ family: string;
20
+ source?: string;
21
+ format?: string;
22
+ weight?: string;
9
23
  }
10
24
 
11
25
  export interface DoryFonts {
12
26
  family: string;
13
- source: string;
14
- format: string;
27
+ source?: string;
28
+ format?: string;
29
+ stylesheet?: string;
30
+ heading?: DoryFontDefinition;
31
+ mono?: DoryFontDefinition;
15
32
  }
16
33
 
17
34
  export interface DoryLogo {
@@ -85,6 +102,27 @@ export interface DoryAnalytics {
85
102
  };
86
103
  }
87
104
 
105
+ export interface DorySeo {
106
+ /** Applied to page titles, e.g. "%s — WhoDB"; %s is the page title. Default: no template. */
107
+ titleTemplate?: string;
108
+ keywords?: string[];
109
+ author?: string;
110
+ /** Absolute URL or path resolved against config.url. */
111
+ ogImage?: string;
112
+ twitterCard?: 'summary' | 'summary_large_image';
113
+ /** e.g. "en_US" */
114
+ locale?: string;
115
+ /** Extra lines appended to robots.txt. */
116
+ robotsTxt?: string[];
117
+ /** Site-wide meta robots noindex (for staging environments). */
118
+ noindex?: boolean;
119
+ }
120
+
121
+ export interface DoryTheme {
122
+ mode?: 'light' | 'dark' | 'system';
123
+ radius?: string;
124
+ }
125
+
88
126
  export interface DoryConfig {
89
127
  name: string;
90
128
  title?: string;
@@ -95,8 +133,10 @@ export interface DoryConfig {
95
133
  favicon: string;
96
134
  colors?: DoryColors;
97
135
  fonts?: DoryFonts;
136
+ theme?: DoryTheme;
98
137
  navigation: DoryNavigation;
99
138
  logo: DoryLogo;
100
139
  embed?: DoryEmbedConfig;
101
140
  analytics?: DoryAnalytics;
141
+ seo?: DorySeo;
102
142
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Window globals injected by the SSR build (see scripts/prerender).
3
+ */
4
+ interface Window {
5
+ __DORY_FRONTMATTER__?: Array<Record<string, any>>;
6
+ __DORY_ROUTE__?: string;
7
+ }
package/src/ui/header.tsx CHANGED
@@ -11,6 +11,7 @@ import { ModeToggle } from '@clidey/ux';
11
11
  import { useIsEmbedded } from '../components/hooks';
12
12
 
13
13
  const config = docsConfig as DoryConfig;
14
+ const isThemeForced = config.theme?.mode === 'dark' || config.theme?.mode === 'light';
14
15
 
15
16
  function TopLevelNavItem({ href, children, isActive }: { href: string; children: string; isActive?: boolean }) {
16
17
  return (
@@ -30,6 +31,13 @@ function TopLevelNavItem({ href, children, isActive }: { href: string; children:
30
31
  );
31
32
  }
32
33
 
34
+ /**
35
+ * Site header with logo, top-level navigation, search, and theme toggle.
36
+ *
37
+ * SYNC NOTE: src/entry-server.tsx renders a static SSR copy of this header's
38
+ * structure (the <header> block inside render()). If you change the markup
39
+ * structure here, update entry-server.tsx to match or hydration will mismatch.
40
+ */
33
41
  export function Header({ className }: { className?: string }) {
34
42
  const [isScrolled, setIsScrolled] = useState(false);
35
43
  const pathname = usePathname();
@@ -53,13 +61,13 @@ export function Header({ className }: { className?: string }) {
53
61
  return (
54
62
  <header
55
63
  className={classNames(
56
- 'sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between bg-white px-4 py-5 shadow-md shadow-gray-900/5 sm:px-6 lg:px-8 dark:shadow-none',
64
+ 'dory-header sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between px-4 py-5 sm:px-6 lg:px-8 border-b',
57
65
  className,
58
66
  {
59
- 'dark:bg-gray-900/95 dark:backdrop-blur-sm dark:[@supports(backdrop-filter:blur(0))]:bg-neutral-900/75': isScrolled,
60
- 'dark:bg-transparent': !isScrolled,
67
+ 'backdrop-blur-sm': isScrolled,
61
68
  },
62
69
  )}
70
+ style={{ backgroundColor: 'var(--background)', borderColor: 'var(--border)' }}
63
71
  >
64
72
  <div className="relative flex justify-between gap-6 sm:gap-8 md:grow items-center w-full px-4">
65
73
  <div className="flex items-center grow gap-2">
@@ -90,7 +98,7 @@ export function Header({ className }: { className?: string }) {
90
98
  <MobileSearch />
91
99
  <Search />
92
100
  {
93
- !isEmbedded &&
101
+ !isEmbedded && !isThemeForced &&
94
102
  <ModeToggle data-testid="mode-toggle" />
95
103
  }
96
104
  <MobileNavigation />
package/src/ui/layout.tsx CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { ComponentChildren } from 'preact';
2
2
  import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
3
3
  import { Navigation } from '../components/navigation';
4
- import { ALL_NAVIGATION, ALL_OPENAPI, completeFrontMatter, loadMDXFrontMatterForPath, loadAllMDXFrontMatter, ALL_PAGES, pathFromFilename, preloadFrontmatter, ALL_ASYNCAPI, isFrontmatterReady } from '../components/store';
4
+ import { ALL_NAVIGATION, ALL_OPENAPI, completeFrontMatter, loadMDXFrontMatterForPath, loadAllMDXFrontMatter, preloadFrontmatter, ALL_ASYNCAPI, isFrontmatterReady } from '../components/store';
5
+ import docsConfig from '../../docs/dory.json' with { type: 'json' };
6
+ import type { DoryConfig } from '../types/config';
5
7
  import { usePathname } from '../components/hooks';
6
8
  import { OpenAPI } from '../mdx/open-api';
7
9
  import { AsyncAPI } from '../mdx/async-api';
@@ -16,6 +18,8 @@ import { useIsEmbedded } from '../components/hooks';
16
18
  import { ErrorBoundary } from '../components/error-boundary';
17
19
  import { cn } from '@clidey/ux';
18
20
 
21
+ const config = docsConfig as DoryConfig;
22
+
19
23
  interface LayoutProps {
20
24
  children: ComponentChildren;
21
25
  }
@@ -24,6 +28,15 @@ const prompt = (pathname: string) => `Please read and analyze the content from t
24
28
  `Then, help me understand and answer any questions I have about it. ` +
25
29
  `Please provide clear, detailed explanations and examples where relevant.`;
26
30
 
31
+ /**
32
+ * Main documentation layout: header, sidebar navigation, article content,
33
+ * table of contents, and prev/next links.
34
+ *
35
+ * SYNC NOTE: src/entry-server.tsx renders a static SSR copy of this layout's
36
+ * shell (the #container / sidebar nav / main / article wrappers in render()).
37
+ * If you change that markup structure here, update entry-server.tsx to match
38
+ * or hydration will mismatch.
39
+ */
27
40
  export default function Layout({ children }: LayoutProps) {
28
41
  const [loading, setLoading] = useState(!isFrontmatterReady());
29
42
  const rawPathname = usePathname();
@@ -35,11 +48,17 @@ export default function Layout({ children }: LayoutProps) {
35
48
  window.open(`${window.location.origin}${pathname}.mdx`, '_blank');
36
49
  }, [pathname]);
37
50
 
38
- const handleCopyMDX = useCallback(() => {
39
- const mod = ALL_PAGES[pathFromFilename(pathname)];
40
- if (mod) navigator.clipboard.writeText((mod as any).default.toString());
41
- showNotification('Copied to clipboard');
42
- }, []);
51
+ const handleCopyMDX = useCallback(async () => {
52
+ try {
53
+ const response = await fetch(`${pathname}.mdx`);
54
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
55
+ await navigator.clipboard.writeText(await response.text());
56
+ showNotification('Copied to clipboard');
57
+ } catch (error) {
58
+ console.warn(`Failed to fetch ${pathname}.mdx:`, error);
59
+ showNotification('Could not copy MDX', 'The raw MDX file is not available.');
60
+ }
61
+ }, [pathname, showNotification]);
43
62
  const handleOpenChatGPT = useCallback(() => {
44
63
  const url = `https://chat.openai.com/?q=${encodeURIComponent(prompt(pathname))}`;
45
64
  window.open(url, '_blank');
@@ -102,7 +121,9 @@ export default function Layout({ children }: LayoutProps) {
102
121
  // Update document title and meta tags when page changes
103
122
  useEffect(() => {
104
123
  if (page?.title) {
105
- document.title = page.title;
124
+ // Mirror the build-time SSR title template (config.seo.titleTemplate, e.g. "%s — Site")
125
+ const titleTemplate = (config as { seo?: { titleTemplate?: string } }).seo?.titleTemplate;
126
+ document.title = titleTemplate ? titleTemplate.replace('%s', page.title) : page.title;
106
127
 
107
128
  // Update Open Graph title
108
129
  const ogTitle = document.querySelector('meta[property="og:title"]');
@@ -152,44 +173,20 @@ export default function Layout({ children }: LayoutProps) {
152
173
  }
153
174
  canonical.setAttribute('href', `${window.location.origin}${window.location.pathname}`);
154
175
 
155
- // Add/update JSON-LD structured data
156
- const structuredData = {
157
- '@context': 'https://schema.org',
158
- '@graph': [
159
- {
160
- '@type': 'Article',
161
- 'headline': page?.title || document.title,
162
- 'description': page?.description || '',
163
- },
164
- {
165
- '@type': 'BreadcrumbList',
166
- 'itemListElement': [
167
- {
168
- '@type': 'ListItem',
169
- 'position': 1,
170
- 'name': 'Home',
171
- 'item': window.location.origin
172
- },
173
- ...(page?.title ? [{
174
- '@type': 'ListItem',
175
- 'position': 2,
176
- 'name': page.title,
177
- 'item': window.location.href
178
- }] : [])
179
- ]
180
- }
181
- ]
182
- };
183
-
184
- let ldScript = document.querySelector('script[type="application/ld+json"]');
185
- if (!ldScript) {
186
- ldScript = document.createElement('script');
187
- ldScript.setAttribute('type', 'application/ld+json');
188
- document.head.appendChild(ldScript);
189
- }
190
- ldScript.textContent = JSON.stringify(structuredData);
176
+ // JSON-LD structured data is emitted at build time (prerender) — crawlers consume
177
+ // that version, so it is intentionally not rewritten on client-side navigation.
191
178
  }, [page]);
192
179
 
180
+ const isLandingPage = useMemo(() => {
181
+ const firstTab = ALL_NAVIGATION[0];
182
+ if (!firstTab) return false;
183
+ return (
184
+ firstTab.groups.length === 1 &&
185
+ firstTab.groups[0].pages.length === 1 &&
186
+ pathname === firstTab.groups[0].pages[0].href
187
+ );
188
+ }, [pathname]);
189
+
193
190
  const tab = useMemo(() => {
194
191
  return completeFrontMatter.find((page) => page.path === pathname);
195
192
  }, [pathname, loading]);
@@ -226,6 +223,28 @@ export default function Layout({ children }: LayoutProps) {
226
223
 
227
224
  if (loading) return <div className="h-[25vh] flex grow"><Loading /></div>;
228
225
 
226
+ if (isLandingPage) {
227
+ return (
228
+ <div className="flex w-full flex-col">
229
+ <Header />
230
+ <main className="w-full">
231
+ <ErrorBoundary fallback={(error) => (
232
+ <div className="flex flex-col items-center justify-center py-16 px-6 text-center">
233
+ <p className="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">
234
+ This page failed to render
235
+ </p>
236
+ <pre className="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-md p-3 max-w-md overflow-auto">
237
+ {error.message}
238
+ </pre>
239
+ </div>
240
+ )}>
241
+ {children}
242
+ </ErrorBoundary>
243
+ </main>
244
+ </div>
245
+ );
246
+ }
247
+
229
248
  return (
230
249
  <div className="flex w-full flex-col">
231
250
  <Header />
@@ -249,14 +268,14 @@ export default function Layout({ children }: LayoutProps) {
249
268
  <div className="flex justify-between items-start">
250
269
  <header className="mb-8">
251
270
  {title && (
252
- <h1 className="font-display text-lg tracking-tight">
271
+ <p className="dory-subtitle text-sm font-medium tracking-widest uppercase mb-2">
253
272
  {title}
254
- </h1>
273
+ </p>
255
274
  )}
256
275
  {group && (
257
- <p className="text-4xl tracking-tight">
276
+ <h1 className="dory-page-title text-4xl tracking-tight">
258
277
  {group}
259
- </p>
278
+ </h1>
260
279
  )}
261
280
  </header>
262
281
  {
@@ -1,6 +1,20 @@
1
1
  import { useCallback, useEffect, useState } from 'react'
2
2
  import classNames from 'classnames'
3
3
 
4
+ /**
5
+ * Slugifies heading text matching rehype-slug (github-slugger) behavior:
6
+ * lowercase, strip punctuation, spaces to dashes. Used only as a fallback
7
+ * for headings that did not get an id at build time, so client-assigned
8
+ * anchors agree with SSR-assigned ones.
9
+ */
10
+ function slugify(text: string): string {
11
+ return text
12
+ .toLowerCase()
13
+ .trim()
14
+ .replace(/[^\p{L}\p{N}\s_-]/gu, '')
15
+ .replace(/\s+/g, '-')
16
+ }
17
+
4
18
  export type Section = {
5
19
  id: string
6
20
  title: string
@@ -33,8 +47,11 @@ export function TableOfContents() {
33
47
  const style = window.getComputedStyle(heading)
34
48
  if (style.display === 'none' || style.visibility === 'hidden') return
35
49
 
36
- const id = heading.id || heading.textContent?.toLowerCase().replace(/\s+/g, '-') || ''
37
- heading.id = id
50
+ // Prefer the build-time id from rehype-slug; only assign when missing
51
+ const id = heading.id || slugify(heading.textContent || '')
52
+ if (!heading.id && id) {
53
+ heading.id = id
54
+ }
38
55
 
39
56
  if (heading.tagName === 'H2') {
40
57
  currentSection = {
@@ -3,52 +3,72 @@ import type { DoryConfig } from '../types/config';
3
3
 
4
4
  const config = docsConfig as DoryConfig;
5
5
 
6
- /**
7
- * Converts hex color to RGB values for CSS variable usage
8
- */
9
6
  function hexToRgb(hex: string): string {
10
- // Remove # if present
11
7
  const cleanHex = hex.replace('#', '');
12
-
13
- // Parse hex values
14
8
  const r = parseInt(cleanHex.substring(0, 2), 16);
15
9
  const g = parseInt(cleanHex.substring(2, 4), 16);
16
10
  const b = parseInt(cleanHex.substring(4, 6), 16);
17
-
18
11
  return `${r} ${g} ${b}`;
19
12
  }
20
13
 
21
- /**
22
- * Load and apply brand colors from dory.json configuration
23
- * Sets CSS variables for use throughout the application
24
- */
25
14
  export function loadColors() {
26
15
  const { colors } = config;
27
16
  if (!colors) return;
28
17
 
29
- // Set CSS variables on the root element for both light and dark modes
30
18
  const root = document.documentElement;
31
19
 
32
- // Set primary color (used in both light and dark mode)
20
+ // Brand accent colors
33
21
  if (colors.primary) {
34
22
  root.style.setProperty('--brand-primary', colors.primary);
35
23
  root.style.setProperty('--brand-primary-rgb', hexToRgb(colors.primary));
24
+ root.style.setProperty('--brand-foreground', colors.primary);
36
25
  }
37
26
 
38
- // Set light mode brand color
39
27
  if (colors.light) {
40
28
  root.style.setProperty('--brand-light', colors.light);
41
29
  root.style.setProperty('--brand-light-rgb', hexToRgb(colors.light));
42
30
  }
43
31
 
44
- // Set dark mode brand color
45
32
  if (colors.dark) {
46
33
  root.style.setProperty('--brand-dark', colors.dark);
47
34
  root.style.setProperty('--brand-dark-rgb', hexToRgb(colors.dark));
48
35
  }
49
36
 
50
- // Set the legacy variable for backward compatibility
51
- if (colors.primary) {
52
- root.style.setProperty('--brand-foreground', colors.primary);
37
+ // Override shadcn/ux theme variables
38
+ if (colors.background) {
39
+ root.style.setProperty('--background', colors.background);
40
+ }
41
+
42
+ if (colors.foreground) {
43
+ root.style.setProperty('--foreground', colors.foreground);
44
+ root.style.setProperty('--card-foreground', colors.foreground);
45
+ root.style.setProperty('--popover-foreground', colors.foreground);
46
+ root.style.setProperty('--secondary-foreground', colors.foreground);
47
+ }
48
+
49
+ if (colors.surface) {
50
+ root.style.setProperty('--card', colors.surface);
51
+ root.style.setProperty('--popover', colors.surface);
52
+ root.style.setProperty('--sidebar', colors.surface);
53
+ }
54
+
55
+ if (colors.surfaceElevated) {
56
+ root.style.setProperty('--muted', colors.surfaceElevated);
57
+ root.style.setProperty('--secondary', colors.surfaceElevated);
58
+ }
59
+
60
+ if (colors.mutedForeground) {
61
+ root.style.setProperty('--muted-foreground', colors.mutedForeground);
62
+ }
63
+
64
+ if (colors.border) {
65
+ root.style.setProperty('--border', colors.border);
66
+ root.style.setProperty('--input', colors.border);
67
+ root.style.setProperty('--sidebar-border', colors.border);
68
+ }
69
+
70
+ if (colors.accent) {
71
+ root.style.setProperty('--accent', colors.accent);
72
+ root.style.setProperty('--ring', colors.accent);
53
73
  }
54
74
  }
@@ -7,17 +7,60 @@ export function loadFonts() {
7
7
  const { fonts } = config;
8
8
  if (!fonts) return;
9
9
 
10
- // Create a style element for the font face
11
- const style = document.createElement('style');
12
- style.textContent = `
13
- @font-face {
14
- font-family: '${fonts.family}';
15
- src: url('${fonts.source}') format('${fonts.format}');
16
- font-display: swap;
10
+ const root = document.documentElement;
11
+
12
+ // If a stylesheet URL is provided (e.g. Google Fonts), use that
13
+ if (fonts.stylesheet) {
14
+ const link = document.createElement('link');
15
+ link.rel = 'stylesheet';
16
+ link.href = fonts.stylesheet;
17
+ document.head.appendChild(link);
18
+ } else if (fonts.source && fonts.format) {
19
+ // Fallback: load individual font files via @font-face
20
+ const style = document.createElement('style');
21
+ let fontFaces = `
22
+ @font-face {
23
+ font-family: '${fonts.family}';
24
+ src: url('${fonts.source}') format('${fonts.format}');
25
+ font-display: swap;
26
+ }
27
+ `;
28
+
29
+ if (fonts.heading?.source && fonts.heading?.format) {
30
+ fontFaces += `
31
+ @font-face {
32
+ font-family: '${fonts.heading.family}';
33
+ src: url('${fonts.heading.source}') format('${fonts.heading.format}');
34
+ ${fonts.heading.weight ? `font-weight: ${fonts.heading.weight};` : ''}
35
+ font-display: swap;
36
+ }
37
+ `;
38
+ }
39
+
40
+ if (fonts.mono?.source && fonts.mono?.format) {
41
+ fontFaces += `
42
+ @font-face {
43
+ font-family: '${fonts.mono.family}';
44
+ src: url('${fonts.mono.source}') format('${fonts.mono.format}');
45
+ ${fonts.mono.weight ? `font-weight: ${fonts.mono.weight};` : ''}
46
+ font-display: swap;
47
+ }
48
+ `;
17
49
  }
18
- `;
19
- document.head.appendChild(style);
20
50
 
21
- // Apply the font family to the root element
22
- document.documentElement.style.fontFamily = fonts.family;
23
- }
51
+ style.textContent = fontFaces;
52
+ document.head.appendChild(style);
53
+ }
54
+
55
+ // Set CSS variables for font families
56
+ root.style.setProperty('--font-body', `'${fonts.family}', sans-serif`);
57
+ root.style.fontFamily = `'${fonts.family}', sans-serif`;
58
+
59
+ if (fonts.heading) {
60
+ root.style.setProperty('--font-heading', `'${fonts.heading.family}', serif`);
61
+ }
62
+
63
+ if (fonts.mono) {
64
+ root.style.setProperty('--font-mono', `'${fonts.mono.family}', monospace`);
65
+ }
66
+ }