@clidey/dory 0.46.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 (52) 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 +55 -15
  6. package/src/components/routes.tsx +38 -93
  7. package/src/components/search.tsx +1 -0
  8. package/src/components/store.ts +64 -88
  9. package/src/config/mdx.js +7 -3
  10. package/src/config/mdx.ts +6 -2
  11. package/src/entry-server.tsx +189 -66
  12. package/src/main.tsx +27 -5
  13. package/src/mdx/api-playground.tsx +2 -0
  14. package/src/mdx/code.tsx +8 -5
  15. package/src/mdx/fence.tsx +10 -2
  16. package/src/mdx/icon.tsx +21 -14
  17. package/src/mdx/latex.tsx +5 -0
  18. package/src/mdx/mdx.tsx +19 -11
  19. package/src/mdx/mermaid.tsx +10 -1
  20. package/src/mdx/processor.test.ts +47 -0
  21. package/src/mdx/step.tsx +3 -3
  22. package/src/mdx/websocket-playground.tsx +2 -0
  23. package/src/plugins/analytics-injector.ts +7 -5
  24. package/src/plugins/docs-assets.ts +5 -4
  25. package/src/plugins/frontmatter-dev-server.ts +23 -137
  26. package/src/plugins/frontmatter-generator.ts +64 -111
  27. package/src/plugins/html-files-middleware.test.ts +21 -0
  28. package/src/plugins/html-files-middleware.ts +27 -5
  29. package/src/plugins/html-metadata-injector.ts +94 -53
  30. package/src/plugins/llm-txt-dev-server.ts +25 -178
  31. package/src/plugins/llm-txt-generator.ts +94 -133
  32. package/src/plugins/prerender.ts +156 -50
  33. package/src/plugins/robots-generator.ts +15 -16
  34. package/src/plugins/sanitize.test.ts +104 -0
  35. package/src/plugins/sanitize.ts +91 -60
  36. package/src/plugins/shared.ts +139 -0
  37. package/src/plugins/sitemap-generator.ts +58 -44
  38. package/src/theme.scss +138 -15
  39. package/src/types/config.ts +42 -2
  40. package/src/types/globals.d.ts +7 -0
  41. package/src/ui/header.tsx +12 -4
  42. package/src/ui/layout.tsx +72 -52
  43. package/src/ui/table-of-content.tsx +19 -2
  44. package/src/utils/colors.ts +38 -18
  45. package/src/utils/fonts.ts +55 -12
  46. package/src/utils/theme.ts +29 -0
  47. package/vite.config.embed-loader.ts +3 -11
  48. package/vite.config.embed-widget.ts +3 -10
  49. package/vite.config.ssr.ts +2 -1
  50. package/vite.config.ts +13 -8
  51. package/scripts/generate-frontmatter-json.js +0 -154
  52. 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 } 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,9 +28,19 @@ 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
- const [loading, setLoading] = useState(true);
29
- const pathname = usePathname();
41
+ const [loading, setLoading] = useState(!isFrontmatterReady());
42
+ const rawPathname = usePathname();
43
+ const pathname = rawPathname === '/' ? ALL_NAVIGATION[0]?.groups[0]?.pages[0]?.href ?? '/' : rawPathname;
30
44
  const { showNotification } = useNotification();
31
45
  const isEmbedded = useIsEmbedded();
32
46
 
@@ -34,12 +48,17 @@ export default function Layout({ children }: LayoutProps) {
34
48
  window.open(`${window.location.origin}${pathname}.mdx`, '_blank');
35
49
  }, [pathname]);
36
50
 
37
- const handleCopyMDX = useCallback(() => {
38
- ALL_PAGES[pathFromFilename(pathname)]().then((mdx) => {
39
- navigator.clipboard.writeText(mdx.default.toString());
40
- });
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');
@@ -69,9 +88,9 @@ export default function Layout({ children }: LayoutProps) {
69
88
  ], [handleOpenMDX, handleCopyMDX, handleOpenChatGPT, handleOpenAnthropic]);
70
89
 
71
90
  useEffect(() => {
72
- // Try to preload frontmatter first (optimized approach)
91
+ // Try to preload frontmatter first (optimized approach).
92
+ // If inline data was available from SSR, preloadFrontmatter() resolves immediately.
73
93
  preloadFrontmatter().then(() => {
74
- // If preloading succeeds, we're done
75
94
  setLoading(false);
76
95
  }).catch(() => {
77
96
  // Fallback to the original approach
@@ -87,6 +106,7 @@ export default function Layout({ children }: LayoutProps) {
87
106
  });
88
107
  }, [pathname]);
89
108
 
109
+
90
110
  const { title: group, group: title, page } = useMemo(() => {
91
111
  const group = ALL_NAVIGATION.flatMap((tab) =>
92
112
  tab.groups.find((group) =>
@@ -101,7 +121,9 @@ export default function Layout({ children }: LayoutProps) {
101
121
  // Update document title and meta tags when page changes
102
122
  useEffect(() => {
103
123
  if (page?.title) {
104
- 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;
105
127
 
106
128
  // Update Open Graph title
107
129
  const ogTitle = document.querySelector('meta[property="og:title"]');
@@ -151,44 +173,20 @@ export default function Layout({ children }: LayoutProps) {
151
173
  }
152
174
  canonical.setAttribute('href', `${window.location.origin}${window.location.pathname}`);
153
175
 
154
- // Add/update JSON-LD structured data
155
- const structuredData = {
156
- '@context': 'https://schema.org',
157
- '@graph': [
158
- {
159
- '@type': 'Article',
160
- 'headline': page?.title || document.title,
161
- 'description': page?.description || '',
162
- },
163
- {
164
- '@type': 'BreadcrumbList',
165
- 'itemListElement': [
166
- {
167
- '@type': 'ListItem',
168
- 'position': 1,
169
- 'name': 'Home',
170
- 'item': window.location.origin
171
- },
172
- ...(page?.title ? [{
173
- '@type': 'ListItem',
174
- 'position': 2,
175
- 'name': page.title,
176
- 'item': window.location.href
177
- }] : [])
178
- ]
179
- }
180
- ]
181
- };
182
-
183
- let ldScript = document.querySelector('script[type="application/ld+json"]');
184
- if (!ldScript) {
185
- ldScript = document.createElement('script');
186
- ldScript.setAttribute('type', 'application/ld+json');
187
- document.head.appendChild(ldScript);
188
- }
189
- 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.
190
178
  }, [page]);
191
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
+
192
190
  const tab = useMemo(() => {
193
191
  return completeFrontMatter.find((page) => page.path === pathname);
194
192
  }, [pathname, loading]);
@@ -225,6 +223,28 @@ export default function Layout({ children }: LayoutProps) {
225
223
 
226
224
  if (loading) return <div className="h-[25vh] flex grow"><Loading /></div>;
227
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
+
228
248
  return (
229
249
  <div className="flex w-full flex-col">
230
250
  <Header />
@@ -248,14 +268,14 @@ export default function Layout({ children }: LayoutProps) {
248
268
  <div className="flex justify-between items-start">
249
269
  <header className="mb-8">
250
270
  {title && (
251
- <h1 className="font-display text-lg tracking-tight">
271
+ <p className="dory-subtitle text-sm font-medium tracking-widest uppercase mb-2">
252
272
  {title}
253
- </h1>
273
+ </p>
254
274
  )}
255
275
  {group && (
256
- <p className="text-4xl tracking-tight">
276
+ <h1 className="dory-page-title text-4xl tracking-tight">
257
277
  {group}
258
- </p>
278
+ </h1>
259
279
  )}
260
280
  </header>
261
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
  }