Skip to content

Commit 0b8ddf2

Browse files
committed
Simplify theme changing on the snippet, and update documentation
1 parent a607de7 commit 0b8ddf2

9 files changed

Lines changed: 398 additions & 26 deletions

File tree

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

viewers/snippet/src/config/theme.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,18 @@ export interface ThemeConfig {
146146
preference?: ThemePreference;
147147

148148
/**
149-
* Custom themes to override built-in defaults.
150-
* You can provide one or both themes.
149+
* Color overrides for light mode.
150+
* Only specify the colors you want to change.
151+
* @example { accent: { primary: '#9333ea' } }
151152
*/
152-
themes?: {
153-
light?: Theme | DeepPartial<ThemeColors>;
154-
dark?: Theme | DeepPartial<ThemeColors>;
155-
};
153+
light?: DeepPartial<ThemeColors>;
154+
155+
/**
156+
* Color overrides for dark mode.
157+
* Only specify the colors you want to change.
158+
* @example { accent: { primary: '#a855f7' } }
159+
*/
160+
dark?: DeepPartial<ThemeColors>;
156161
}
157162

158163
// ─────────────────────────────────────────────────────────
@@ -327,21 +332,11 @@ export function createTheme(
327332
}
328333

329334
/**
330-
* Merges theme configuration, handling both full Theme objects and partial overrides
335+
* Applies color overrides to a base theme
331336
*/
332-
export function resolveTheme(
333-
config: Theme | DeepPartial<ThemeColors> | undefined,
334-
base: Theme,
335-
): Theme {
336-
if (!config) return base;
337-
338-
// Check if it's a full Theme object
339-
if ('name' in config && 'colors' in config) {
340-
return config as Theme;
341-
}
342-
343-
// It's a partial color override
344-
return createTheme(base, config as DeepPartial<ThemeColors>);
337+
export function resolveTheme(overrides: DeepPartial<ThemeColors> | undefined, base: Theme): Theme {
338+
if (!overrides) return base;
339+
return createTheme(base, overrides);
345340
}
346341

347342
// ─────────────────────────────────────────────────────────

viewers/snippet/src/embedpdf.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ type ContainerConfig = PDFViewerConfig & {
8686
target: Element;
8787
};
8888

89-
customElements.define('embedpdf-container', EmbedPdfContainer);
89+
if (typeof customElements !== 'undefined' && !customElements.get('embedpdf-container')) {
90+
customElements.define('embedpdf-container', EmbedPdfContainer);
91+
}
9092

9193
/**
9294
* Initialize the EmbedPDF viewer

viewers/snippet/src/web-components/container.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {
1919
registerIcons as globalRegisterIcons,
2020
} from '@/config/icon-registry';
2121

22-
export class EmbedPdfContainer extends HTMLElement {
22+
const BaseElement = typeof HTMLElement !== 'undefined' ? HTMLElement : class {};
23+
24+
export class EmbedPdfContainer extends (BaseElement as typeof HTMLElement) {
2325
private root: ShadowRoot;
2426
private _config?: PDFViewerConfig;
2527
private _registryPromise: Promise<PluginRegistry>;
@@ -128,9 +130,9 @@ export class EmbedPdfContainer extends HTMLElement {
128130
// Get base theme
129131
const baseTheme = colorScheme === 'dark' ? darkTheme : lightTheme;
130132

131-
// Apply custom overrides if provided
132-
const customThemeConfig = themeConfig?.themes?.[colorScheme];
133-
return resolveTheme(customThemeConfig, baseTheme);
133+
// Apply custom overrides if provided (now directly on themeConfig.light/dark)
134+
const colorOverrides = themeConfig?.[colorScheme];
135+
return resolveTheme(colorOverrides, baseTheme);
134136
}
135137

136138
/**
@@ -199,7 +201,11 @@ export class EmbedPdfContainer extends HTMLElement {
199201
if (typeof theme === 'string') {
200202
this._config.theme = { ...this._config.theme, preference: theme };
201203
} else {
202-
this._config.theme = theme;
204+
// Merge new config with existing, preserving unspecified values
205+
this._config.theme = {
206+
...this._config.theme,
207+
...theme,
208+
};
203209
}
204210
this.setupTheme();
205211

website/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@embedpdf/plugin-viewport": "workspace:*",
4242
"@embedpdf/plugin-zoom": "workspace:*",
4343
"@embedpdf/snippet": "workspace:*",
44+
"@embedpdf/react-pdf-viewer": "workspace:*",
4445
"@headlessui/react": "^2.2.0",
4546
"@mdx-js/loader": "^3.0.0",
4647
"@mdx-js/react": "^3.0.0",
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client'
2+
3+
import React from 'react'
4+
import { useTheme } from 'next-themes'
5+
6+
interface ExampleWrapperProps {
7+
children: React.ReactElement<{ themePreference?: 'light' | 'dark' }>
8+
}
9+
10+
/**
11+
* Wrapper component that passes the website's theme as a prop to examples.
12+
* This keeps the theme syncing logic out of the example code shown to users.
13+
*
14+
* The child component will receive a `themePreference` prop with value 'light' or 'dark'.
15+
*/
16+
export function ExampleWrapper({ children }: ExampleWrapperProps) {
17+
const { resolvedTheme } = useTheme()
18+
const themePreference = resolvedTheme === 'dark' ? 'dark' : 'light'
19+
20+
// Clone the child and pass themePreference as a prop
21+
return React.cloneElement(children, { themePreference })
22+
}
23+
24+
export default ExampleWrapper
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export default {
22
introduction: 'Introduction',
3+
theme: 'Theme',
34
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
'use client'
2+
import { PDFViewer, PDFViewerRef } from '@embedpdf/react-pdf-viewer'
3+
import { useRef, useState, useEffect } from 'react'
4+
5+
// Available brand colors to choose from
6+
const brandColors = [
7+
{
8+
name: 'Purple',
9+
primary: '#9333ea',
10+
hover: '#7e22ce',
11+
active: '#6b21a8',
12+
light: '#f3e8ff',
13+
darkLight: '#3b0764',
14+
},
15+
{
16+
name: 'Blue',
17+
primary: '#2563eb',
18+
hover: '#1d4ed8',
19+
active: '#1e40af',
20+
light: '#dbeafe',
21+
darkLight: '#1e3a8a',
22+
},
23+
{
24+
name: 'Green',
25+
primary: '#16a34a',
26+
hover: '#15803d',
27+
active: '#166534',
28+
light: '#dcfce7',
29+
darkLight: '#14532d',
30+
},
31+
{
32+
name: 'Orange',
33+
primary: '#ea580c',
34+
hover: '#c2410c',
35+
active: '#9a3412',
36+
light: '#ffedd5',
37+
darkLight: '#7c2d12',
38+
},
39+
{
40+
name: 'Pink',
41+
primary: '#db2777',
42+
hover: '#be185d',
43+
active: '#9d174d',
44+
light: '#fce7f3',
45+
darkLight: '#831843',
46+
},
47+
]
48+
49+
interface ThemeExampleProps {
50+
/** Theme preference: 'light' or 'dark' - synced with the website theme */
51+
themePreference?: 'light' | 'dark'
52+
}
53+
54+
export default function ThemeExample({
55+
themePreference = 'light',
56+
}: ThemeExampleProps) {
57+
const viewerRef = useRef<PDFViewerRef>(null)
58+
const [selectedColor, setSelectedColor] = useState(brandColors[0])
59+
60+
// Update viewer theme when themePreference prop changes
61+
useEffect(() => {
62+
viewerRef.current?.container?.setTheme({
63+
preference: themePreference,
64+
})
65+
}, [themePreference])
66+
67+
const changeColor = (color: (typeof brandColors)[0]) => {
68+
setSelectedColor(color)
69+
// Update the theme colors at runtime
70+
viewerRef.current?.container?.setTheme({
71+
preference: themePreference,
72+
light: {
73+
accent: {
74+
primary: color.primary,
75+
primaryHover: color.hover,
76+
primaryActive: color.active,
77+
primaryLight: color.light,
78+
primaryForeground: '#ffffff',
79+
},
80+
},
81+
dark: {
82+
accent: {
83+
primary: color.primary,
84+
primaryHover: color.hover,
85+
primaryActive: color.active,
86+
primaryLight: color.darkLight,
87+
primaryForeground: '#ffffff',
88+
},
89+
},
90+
})
91+
}
92+
93+
return (
94+
<div className="flex flex-col gap-4">
95+
{/* Color Selection */}
96+
<div className="flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800">
97+
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
98+
Choose brand color:
99+
</span>
100+
<div className="flex gap-2">
101+
{brandColors.map((color) => (
102+
<button
103+
key={color.name}
104+
onClick={() => changeColor(color)}
105+
className={`h-8 w-8 rounded-full border-2 transition-transform hover:scale-110 ${
106+
selectedColor.name === color.name
107+
? 'border-gray-900 ring-2 ring-offset-2 dark:border-white'
108+
: 'border-transparent'
109+
}`}
110+
style={{ backgroundColor: color.primary }}
111+
title={color.name}
112+
/>
113+
))}
114+
</div>
115+
<span className="text-sm text-gray-500 dark:text-gray-400">
116+
Selected: <strong>{selectedColor.name}</strong>
117+
</span>
118+
</div>
119+
120+
{/* Viewer Container */}
121+
<div className="h-[600px] w-full overflow-hidden rounded-xl border border-gray-300 shadow-lg dark:border-gray-600">
122+
<PDFViewer
123+
ref={viewerRef}
124+
config={{
125+
src: 'https://snippet.embedpdf.com/ebook.pdf',
126+
theme: {
127+
preference: themePreference,
128+
light: {
129+
accent: {
130+
primary: selectedColor.primary,
131+
primaryHover: selectedColor.hover,
132+
primaryActive: selectedColor.active,
133+
primaryLight: selectedColor.light,
134+
primaryForeground: '#ffffff',
135+
},
136+
},
137+
dark: {
138+
accent: {
139+
primary: selectedColor.primary,
140+
primaryHover: selectedColor.hover,
141+
primaryActive: selectedColor.active,
142+
primaryLight: selectedColor.darkLight,
143+
primaryForeground: '#ffffff',
144+
},
145+
},
146+
},
147+
}}
148+
style={{ width: '100%', height: '100%' }}
149+
/>
150+
</div>
151+
</div>
152+
)
153+
}

0 commit comments

Comments
 (0)