@arraypress/theme-injector 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ArrayPress Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # @arraypress/theme-injector
2
+
3
+ CSS custom property theme engine. Injects theme configuration as CSS variables on `:root`. Supports background presets, auto-contrast colors, typography, border radius, surface styles, and custom CSS. Works with any CSS-variable-based design system.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @arraypress/theme-injector
9
+ ```
10
+
11
+ **Peer dependencies:**
12
+
13
+ ```bash
14
+ npm install @arraypress/color-utils @arraypress/google-fonts
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```js
20
+ import { injectTheme } from '@arraypress/theme-injector';
21
+
22
+ // Apply a dark theme with Inter font and green accent
23
+ injectTheme({
24
+ font: 'inter',
25
+ headingFont: 'serif',
26
+ headingWeight: 'bold',
27
+ background: 'dark',
28
+ accentColor: '#06d6a0',
29
+ surface: 'shadow',
30
+ borderRadius: 'rounded',
31
+ contentWidth: 'standard',
32
+ sectionPadding: 'default',
33
+ });
34
+
35
+ // Custom background with auto-contrast
36
+ injectTheme({
37
+ font: 'poppins',
38
+ background: 'custom',
39
+ customBg: '#1a1a2e',
40
+ accentColor: '#e94560',
41
+ surface: 'flat',
42
+ borderRadius: 'pill',
43
+ });
44
+
45
+ // Inject custom CSS
46
+ injectTheme({
47
+ background: 'white',
48
+ customCSS: '.hero { min-height: 80vh; }',
49
+ });
50
+ ```
51
+
52
+ ## Theme Config
53
+
54
+ | Key | Type | Description |
55
+ |-----|------|-------------|
56
+ | `font` | `string` | Body font key (e.g. `'inter'`, `'poppins'`, `'system'`) |
57
+ | `headingFont` | `string` | Heading font key. `'inherit'` matches body font |
58
+ | `headingWeight` | `string` | `'normal'`, `'bold'`, `'black'` |
59
+ | `baseSize` | `string` | `'compact'` (14px), `'default'` (16px), `'large'` (18px), `'xlarge'` (20px) |
60
+ | `headingSize` | `string` | `'small'` through `'massive'` (scale multiplier) |
61
+ | `lineHeight` | `string` | `'tight'`, `'default'`, `'relaxed'`, `'loose'` |
62
+ | `letterSpacing` | `string` | `'tight'`, `'default'`, `'wide'`, `'wider'` |
63
+ | `background` | `string` | `'dark'`, `'soft-dark'`, `'light'`, `'white'`, `'custom'` |
64
+ | `customBg` | `string` | Custom background hex (when `background: 'custom'`) |
65
+ | `customCard` | `string` | Custom card hex (auto-generated if omitted) |
66
+ | `customBorder` | `string` | Custom border color (auto-generated if omitted) |
67
+ | `textColor` | `string` | Override foreground text color |
68
+ | `mutedTextColor` | `string` | Override muted text color |
69
+ | `accentColor` | `string` | Primary/accent hex. Default `'#06d6a0'` |
70
+ | `buttonTextColor` | `string` | Button text color. Auto-calculated if omitted |
71
+ | `surface` | `string` | `'bordered'`, `'shadow'`, `'flat'`, `'glass'` |
72
+ | `borderRadius` | `string` | `'sharp'`, `'rounded'`, `'pill'` |
73
+ | `buttonStyle` | `string` | `'rounded'`, `'sharp'`, `'pill'` |
74
+ | `linkColor` | `string` | Link color |
75
+ | `linkHoverColor` | `string` | Link hover color |
76
+ | `contentWidth` | `string` | `'narrow'` (960px), `'standard'` (1200px), `'wide'` (1440px) |
77
+ | `sectionPadding` | `string` | `'compact'` (2rem), `'default'` (3rem), `'spacious'` (5rem) |
78
+ | `customCSS` | `string` | Raw CSS injected into `<style>` tag |
79
+
80
+ ## Exported Constants
81
+
82
+ - `BACKGROUNDS` — 4 color scheme presets (dark, soft-dark, light, white)
83
+ - `FONT_WEIGHTS` — Heading weight mappings (normal=500, bold=700, black=900)
84
+ - `BASE_SIZES` — Root font sizes (compact, default, large, xlarge)
85
+ - `HEADING_SCALES` — Heading size multipliers (small through massive)
86
+ - `LINE_HEIGHTS` — Line height values (tight, default, relaxed, loose)
87
+ - `LETTER_SPACINGS` — Letter spacing values (tight, default, wide, wider)
88
+ - `CONTENT_WIDTHS` — Max content widths (narrow, standard, wide)
89
+ - `SECTION_PADDINGS` — Section padding values (compact, default, spacious)
90
+ - `RADII` — Border radius presets with xl/lg/md/sm/full values
91
+
92
+ ## CSS Variables Set
93
+
94
+ The injector sets these CSS custom properties on `:root`:
95
+
96
+ - `--font-sans` — Body font family
97
+ - `--heading-font` — Heading font family
98
+ - `--heading-weight` — Heading font weight
99
+ - `--heading-scale` — Heading size multiplier
100
+ - `--line-height` — Body line height
101
+ - `--color-background`, `--color-foreground` — Page colors
102
+ - `--color-card`, `--color-card-foreground` — Card colors
103
+ - `--color-secondary`, `--color-secondary-foreground` — Secondary colors
104
+ - `--color-muted`, `--color-muted-foreground` — Muted colors
105
+ - `--color-border` — Border color
106
+ - `--color-primary`, `--color-primary-foreground` — Accent colors
107
+ - `--card-shadow` — Card shadow value
108
+ - `--color-link`, `--color-link-hover` — Link colors
109
+ - `--content-width` — Max content width
110
+ - `--section-padding` — Section vertical padding
111
+
112
+ ## License
113
+
114
+ MIT
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@arraypress/theme-injector",
3
+ "version": "1.0.0",
4
+ "description": "CSS custom property theme engine. Injects theme configuration as CSS variables on :root. Supports background presets, auto-contrast colors, typography, border radius, surface styles, and custom CSS.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "types": "src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./src/index.js",
11
+ "types": "./src/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "src"
16
+ ],
17
+ "scripts": {
18
+ "test": "node --test tests/theme-injector.test.js"
19
+ },
20
+ "keywords": [
21
+ "theme",
22
+ "css-variables",
23
+ "custom-properties",
24
+ "dark-mode",
25
+ "light-mode",
26
+ "design-system",
27
+ "injector"
28
+ ],
29
+ "author": "David Sherlock",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/arraypress/theme-injector.git"
34
+ },
35
+ "peerDependencies": {
36
+ "@arraypress/color-utils": ">=1",
37
+ "@arraypress/google-fonts": ">=1"
38
+ }
39
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @arraypress/theme-injector — TypeScript definitions.
3
+ */
4
+
5
+ /** Color scheme preset values. */
6
+ export interface ColorScheme {
7
+ /** Background color. */
8
+ bg: string;
9
+ /** Card background color. */
10
+ card: string;
11
+ /** Secondary background color. */
12
+ secondary: string;
13
+ /** Muted background color. */
14
+ muted: string;
15
+ /** Border color (may be rgba). */
16
+ border: string;
17
+ /** Foreground (text) color. */
18
+ fg: string;
19
+ /** Muted foreground color. */
20
+ mutedFg: string;
21
+ }
22
+
23
+ /** Border radius preset values. */
24
+ export interface RadiusPreset {
25
+ xl: string;
26
+ lg: string;
27
+ md: string;
28
+ sm: string;
29
+ full: string;
30
+ }
31
+
32
+ /** Background color scheme presets. */
33
+ export declare const BACKGROUNDS: Record<'dark' | 'soft-dark' | 'light' | 'white', ColorScheme>;
34
+
35
+ /** Heading font weight mappings. */
36
+ export declare const FONT_WEIGHTS: Record<'normal' | 'bold' | 'black', string>;
37
+
38
+ /** Base font size presets. */
39
+ export declare const BASE_SIZES: Record<'compact' | 'default' | 'large' | 'xlarge', string>;
40
+
41
+ /** Heading size scale multipliers. */
42
+ export declare const HEADING_SCALES: Record<'small' | 'default' | 'large' | 'xlarge' | 'xxlarge' | 'massive', string>;
43
+
44
+ /** Line height presets. */
45
+ export declare const LINE_HEIGHTS: Record<'tight' | 'default' | 'relaxed' | 'loose', string>;
46
+
47
+ /** Letter spacing presets. */
48
+ export declare const LETTER_SPACINGS: Record<'tight' | 'default' | 'wide' | 'wider', string>;
49
+
50
+ /** Content width presets. */
51
+ export declare const CONTENT_WIDTHS: Record<'narrow' | 'standard' | 'wide', string>;
52
+
53
+ /** Section vertical padding presets. */
54
+ export declare const SECTION_PADDINGS: Record<'compact' | 'default' | 'spacious', string>;
55
+
56
+ /** Border radius presets. */
57
+ export declare const RADII: Record<'sharp' | 'rounded' | 'pill', RadiusPreset>;
58
+
59
+ /** Theme configuration object. */
60
+ export interface ThemeConfig {
61
+ /** Body font key (e.g. `'inter'`, `'poppins'`, `'system'`). */
62
+ font?: string;
63
+ /** Heading font key. Use `'inherit'` to match body font. */
64
+ headingFont?: string;
65
+ /** Heading weight: `'normal'`, `'bold'`, or `'black'`. */
66
+ headingWeight?: 'normal' | 'bold' | 'black';
67
+ /** Base font size: `'compact'`, `'default'`, `'large'`, `'xlarge'`. */
68
+ baseSize?: 'compact' | 'default' | 'large' | 'xlarge';
69
+ /** Heading scale: `'small'` through `'massive'`. */
70
+ headingSize?: 'small' | 'default' | 'large' | 'xlarge' | 'xxlarge' | 'massive';
71
+ /** Line height: `'tight'`, `'default'`, `'relaxed'`, `'loose'`. */
72
+ lineHeight?: 'tight' | 'default' | 'relaxed' | 'loose';
73
+ /** Letter spacing: `'tight'`, `'default'`, `'wide'`, `'wider'`. */
74
+ letterSpacing?: 'tight' | 'default' | 'wide' | 'wider';
75
+ /** Background preset or `'custom'`. */
76
+ background?: 'dark' | 'soft-dark' | 'light' | 'white' | 'custom';
77
+ /** Custom background hex color (when `background` is `'custom'`). */
78
+ customBg?: string;
79
+ /** Custom card hex color (when `background` is `'custom'`). Auto-generated if omitted. */
80
+ customCard?: string;
81
+ /** Custom border color (when `background` is `'custom'`). Auto-generated if omitted. */
82
+ customBorder?: string;
83
+ /** Override foreground text color. */
84
+ textColor?: string;
85
+ /** Override muted text color. */
86
+ mutedTextColor?: string;
87
+ /** Primary/accent color hex. Default `'#06d6a0'`. */
88
+ accentColor?: string;
89
+ /** Override button text color. Auto-calculated from accent luminance if omitted. */
90
+ buttonTextColor?: string;
91
+ /** Surface style: `'bordered'` (default), `'shadow'`, `'flat'`, `'glass'`. */
92
+ surface?: 'bordered' | 'shadow' | 'flat' | 'glass';
93
+ /** Border radius preset: `'sharp'`, `'rounded'`, `'pill'`. */
94
+ borderRadius?: 'sharp' | 'rounded' | 'pill';
95
+ /** Alias for `borderRadius`. */
96
+ radius?: 'sharp' | 'rounded' | 'pill';
97
+ /** Button style data attribute. */
98
+ buttonStyle?: 'rounded' | 'sharp' | 'pill';
99
+ /** Link color (any CSS color). */
100
+ linkColor?: string;
101
+ /** Link hover color (any CSS color). */
102
+ linkHoverColor?: string;
103
+ /** Content width: `'narrow'`, `'standard'`, `'wide'`. */
104
+ contentWidth?: 'narrow' | 'standard' | 'wide';
105
+ /** Section padding: `'compact'`, `'default'`, `'spacious'`. */
106
+ sectionPadding?: 'compact' | 'default' | 'spacious';
107
+ /** Raw CSS string injected into a `<style>` tag. */
108
+ customCSS?: string;
109
+ }
110
+
111
+ /**
112
+ * Inject a theme configuration as CSS custom properties on `:root`.
113
+ *
114
+ * Sets typography, colors, surface styles, border radius, layout, and custom CSS.
115
+ * SSR safe — no-op when `document` is not available.
116
+ *
117
+ * @param theme - Theme configuration object.
118
+ */
119
+ export declare function injectTheme(theme: ThemeConfig | null | undefined): void;
package/src/index.js ADDED
@@ -0,0 +1,420 @@
1
+ /**
2
+ * @arraypress/theme-injector
3
+ *
4
+ * CSS custom property theme engine. Injects theme configuration as CSS variables
5
+ * on `:root`. Supports background presets, auto-contrast colors, typography,
6
+ * border radius, surface styles, and custom CSS.
7
+ *
8
+ * Works with any CSS-variable-based design system. SSR safe.
9
+ */
10
+
11
+ import { luminance, adjust } from '@arraypress/color-utils';
12
+ import { getFontFamily, loadFont } from '@arraypress/google-fonts';
13
+
14
+ /**
15
+ * Background color scheme presets.
16
+ *
17
+ * Each preset defines a complete color palette: background, card, secondary,
18
+ * muted, border, foreground, and muted foreground colors.
19
+ *
20
+ * @type {Record<string, { bg: string, card: string, secondary: string, muted: string, border: string, fg: string, mutedFg: string }>}
21
+ */
22
+ export const BACKGROUNDS = {
23
+ dark: {
24
+ bg: '#09090b',
25
+ card: '#111113',
26
+ secondary: '#18181b',
27
+ muted: '#18181b',
28
+ border: 'rgba(255,255,255,0.06)',
29
+ fg: '#fafafa',
30
+ mutedFg: '#a1a1aa',
31
+ },
32
+ 'soft-dark': {
33
+ bg: '#111113',
34
+ card: '#1a1a1e',
35
+ secondary: '#222226',
36
+ muted: '#222226',
37
+ border: 'rgba(255,255,255,0.08)',
38
+ fg: '#fafafa',
39
+ mutedFg: '#a1a1aa',
40
+ },
41
+ light: {
42
+ bg: '#fafafa',
43
+ card: '#ffffff',
44
+ secondary: '#f4f4f5',
45
+ muted: '#f4f4f5',
46
+ border: 'rgba(0,0,0,0.08)',
47
+ fg: '#09090b',
48
+ mutedFg: '#71717a',
49
+ },
50
+ white: {
51
+ bg: '#ffffff',
52
+ card: '#ffffff',
53
+ secondary: '#f4f4f5',
54
+ muted: '#f4f4f5',
55
+ border: 'rgba(0,0,0,0.06)',
56
+ fg: '#09090b',
57
+ mutedFg: '#71717a',
58
+ },
59
+ };
60
+
61
+ /**
62
+ * Heading font weight mappings.
63
+ *
64
+ * Maps human-readable weight names to CSS font-weight numeric values.
65
+ *
66
+ * @type {Record<string, string>}
67
+ */
68
+ export const FONT_WEIGHTS = {
69
+ normal: '500',
70
+ bold: '700',
71
+ black: '900',
72
+ };
73
+
74
+ /**
75
+ * Base font size presets.
76
+ *
77
+ * Applied to the root element `font-size`, affecting all `rem`-based sizing.
78
+ *
79
+ * @type {Record<string, string>}
80
+ */
81
+ export const BASE_SIZES = {
82
+ compact: '14px',
83
+ default: '16px',
84
+ large: '18px',
85
+ xlarge: '20px',
86
+ };
87
+
88
+ /**
89
+ * Heading size scale multipliers.
90
+ *
91
+ * Applied as `--heading-scale` CSS variable. Headings use
92
+ * `calc(base-heading-size * var(--heading-scale))` for proportional scaling.
93
+ *
94
+ * @type {Record<string, string>}
95
+ */
96
+ export const HEADING_SCALES = {
97
+ small: '0.85',
98
+ default: '1',
99
+ large: '1.15',
100
+ xlarge: '1.3',
101
+ xxlarge: '1.5',
102
+ massive: '1.8',
103
+ };
104
+
105
+ /**
106
+ * Line height presets.
107
+ *
108
+ * @type {Record<string, string>}
109
+ */
110
+ export const LINE_HEIGHTS = {
111
+ tight: '1.3',
112
+ default: '1.5',
113
+ relaxed: '1.7',
114
+ loose: '1.9',
115
+ };
116
+
117
+ /**
118
+ * Letter spacing presets.
119
+ *
120
+ * @type {Record<string, string>}
121
+ */
122
+ export const LETTER_SPACINGS = {
123
+ tight: '-0.02em',
124
+ default: '0',
125
+ wide: '0.02em',
126
+ wider: '0.05em',
127
+ };
128
+
129
+ /**
130
+ * Content width presets for the main content container.
131
+ *
132
+ * @type {Record<string, string>}
133
+ */
134
+ export const CONTENT_WIDTHS = {
135
+ narrow: '960px',
136
+ standard: '1200px',
137
+ wide: '1440px',
138
+ };
139
+
140
+ /**
141
+ * Section vertical padding presets.
142
+ *
143
+ * @type {Record<string, string>}
144
+ */
145
+ export const SECTION_PADDINGS = {
146
+ compact: '2rem',
147
+ default: '3rem',
148
+ spacious: '5rem',
149
+ };
150
+
151
+ /**
152
+ * Border radius presets.
153
+ *
154
+ * Each preset defines radius values for xl, lg, md, sm, and full sizes.
155
+ *
156
+ * @type {Record<string, { xl: string, lg: string, md: string, sm: string, full: string }>}
157
+ */
158
+ export const RADII = {
159
+ sharp: { xl: '0px', lg: '0px', md: '0px', sm: '0px', full: '0px' },
160
+ rounded: { xl: '12px', lg: '8px', md: '6px', sm: '4px', full: '9999px' },
161
+ pill: { xl: '24px', lg: '16px', md: '12px', sm: '8px', full: '9999px' },
162
+ };
163
+
164
+ /**
165
+ * Apply a complete color scheme to the document root.
166
+ *
167
+ * Sets CSS custom properties for background, foreground, card, secondary,
168
+ * muted, and border colors. Also sets `document.body` inline styles for
169
+ * background-color and color.
170
+ *
171
+ * @param {HTMLElement} root - The document root element (`document.documentElement`).
172
+ * @param {string} bg - Background color.
173
+ * @param {string} card - Card background color.
174
+ * @param {string} secondary - Secondary background color.
175
+ * @param {string} muted - Muted background color.
176
+ * @param {string} border - Border color.
177
+ * @param {string} fg - Foreground (text) color.
178
+ * @param {string} mutedFg - Muted foreground color.
179
+ * @returns {void}
180
+ */
181
+ function applyColorScheme(root, bg, card, secondary, muted, border, fg, mutedFg) {
182
+ root.style.setProperty('--color-background', bg);
183
+ root.style.setProperty('--color-foreground', fg);
184
+ root.style.setProperty('--color-card', card);
185
+ root.style.setProperty('--color-card-foreground', fg);
186
+ root.style.setProperty('--color-secondary', secondary);
187
+ root.style.setProperty('--color-secondary-foreground', fg);
188
+ root.style.setProperty('--color-muted', muted);
189
+ root.style.setProperty('--color-muted-foreground', mutedFg);
190
+ root.style.setProperty('--color-border', border);
191
+ document.body.style.backgroundColor = bg;
192
+ document.body.style.color = fg;
193
+ }
194
+
195
+ /**
196
+ * Inject a theme configuration as CSS custom properties on `:root`.
197
+ *
198
+ * This is the main entry point. Pass a theme config object and it will:
199
+ * - Set typography CSS variables (font family, heading font, weights, sizes)
200
+ * - Load required Google Fonts
201
+ * - Apply background color schemes (preset or custom with auto-contrast)
202
+ * - Set accent/primary colors with auto-calculated foreground contrast
203
+ * - Configure surface styles (bordered, shadow, flat, glass)
204
+ * - Apply border radius presets
205
+ * - Set button style data attributes
206
+ * - Configure layout (content width, section padding)
207
+ * - Inject custom CSS
208
+ * - Write a `<style>` tag with `!important` overrides for cascade safety
209
+ *
210
+ * SSR safe — no-op when `document` is not available.
211
+ *
212
+ * @param {object} theme - Theme configuration object.
213
+ * @param {string} [theme.font] - Body font key (e.g. `'inter'`, `'poppins'`, `'system'`).
214
+ * @param {string} [theme.headingFont] - Heading font key. Use `'inherit'` to match body font.
215
+ * @param {string} [theme.headingWeight] - Heading weight: `'normal'`, `'bold'`, or `'black'`.
216
+ * @param {string} [theme.baseSize] - Base font size: `'compact'`, `'default'`, `'large'`, `'xlarge'`.
217
+ * @param {string} [theme.headingSize] - Heading scale: `'small'`, `'default'`, `'large'`, `'xlarge'`, `'xxlarge'`, `'massive'`.
218
+ * @param {string} [theme.lineHeight] - Line height: `'tight'`, `'default'`, `'relaxed'`, `'loose'`.
219
+ * @param {string} [theme.letterSpacing] - Letter spacing: `'tight'`, `'default'`, `'wide'`, `'wider'`.
220
+ * @param {string} [theme.background] - Background preset: `'dark'`, `'soft-dark'`, `'light'`, `'white'`, or `'custom'`.
221
+ * @param {string} [theme.customBg] - Custom background hex color (when `background` is `'custom'`).
222
+ * @param {string} [theme.customCard] - Custom card hex color (when `background` is `'custom'`). Auto-generated if omitted.
223
+ * @param {string} [theme.customBorder] - Custom border color (when `background` is `'custom'`). Auto-generated if omitted.
224
+ * @param {string} [theme.textColor] - Override foreground text color (any CSS color).
225
+ * @param {string} [theme.mutedTextColor] - Override muted text color (any CSS color).
226
+ * @param {string} [theme.accentColor] - Primary/accent color hex (default `'#06d6a0'`).
227
+ * @param {string} [theme.buttonTextColor] - Override button text color. Auto-calculated from accent luminance if omitted.
228
+ * @param {string} [theme.surface] - Surface style: `'bordered'` (default), `'shadow'`, `'flat'`, `'glass'`.
229
+ * @param {string} [theme.borderRadius] - Border radius preset: `'sharp'`, `'rounded'`, `'pill'`.
230
+ * @param {string} [theme.radius] - Alias for `borderRadius`.
231
+ * @param {string} [theme.buttonStyle] - Button style data attribute: `'rounded'`, `'sharp'`, `'pill'`.
232
+ * @param {string} [theme.linkColor] - Link color (any CSS color).
233
+ * @param {string} [theme.linkHoverColor] - Link hover color (any CSS color).
234
+ * @param {string} [theme.contentWidth] - Content width: `'narrow'`, `'standard'`, `'wide'`.
235
+ * @param {string} [theme.sectionPadding] - Section padding: `'compact'`, `'default'`, `'spacious'`.
236
+ * @param {string} [theme.customCSS] - Raw CSS string injected into a `<style>` tag.
237
+ * @returns {void}
238
+ */
239
+ export function injectTheme(theme) {
240
+ if (!theme) return;
241
+ if (typeof document === 'undefined') return;
242
+
243
+ const root = document.documentElement;
244
+
245
+ // --- Typography ---
246
+
247
+ const fontFamily = getFontFamily(theme.font);
248
+ if (theme.font && fontFamily) {
249
+ root.style.setProperty('--font-sans', fontFamily);
250
+ } else if (theme.font) {
251
+ root.style.removeProperty('--font-sans');
252
+ }
253
+
254
+ // Heading font (separate from body)
255
+ if (theme.headingFont && theme.headingFont !== 'inherit') {
256
+ const headingFamily = getFontFamily(theme.headingFont);
257
+ if (headingFamily) {
258
+ root.style.setProperty('--heading-font', headingFamily);
259
+ }
260
+ } else {
261
+ root.style.removeProperty('--heading-font');
262
+ }
263
+
264
+ if (theme.headingWeight) {
265
+ root.style.setProperty('--heading-weight', FONT_WEIGHTS[theme.headingWeight] || '700');
266
+ }
267
+
268
+ if (theme.baseSize) {
269
+ root.style.fontSize = BASE_SIZES[theme.baseSize] || '16px';
270
+ }
271
+
272
+ if (theme.headingSize) {
273
+ root.style.setProperty('--heading-scale', HEADING_SCALES[theme.headingSize] || '1');
274
+ }
275
+
276
+ // Line height
277
+ if (theme.lineHeight && theme.lineHeight !== 'default') {
278
+ root.style.setProperty('--line-height', LINE_HEIGHTS[theme.lineHeight] || '1.5');
279
+ document.body.style.lineHeight = LINE_HEIGHTS[theme.lineHeight] || '1.5';
280
+ } else {
281
+ root.style.removeProperty('--line-height');
282
+ document.body.style.removeProperty('line-height');
283
+ }
284
+
285
+ // Letter spacing
286
+ if (theme.letterSpacing && theme.letterSpacing !== 'default') {
287
+ document.body.style.letterSpacing = LETTER_SPACINGS[theme.letterSpacing] || '0';
288
+ } else {
289
+ document.body.style.removeProperty('letter-spacing');
290
+ }
291
+
292
+ // --- Background & Colors ---
293
+
294
+ if (theme.background === 'custom') {
295
+ const bg = theme.customBg || '#09090b';
296
+ const isLight = luminance(bg) > 128;
297
+ const card = theme.customCard || (isLight ? adjust(bg, -8) : adjust(bg, 12));
298
+ const secondary = isLight ? adjust(bg, -15) : adjust(bg, 18);
299
+ const muted = secondary;
300
+ const border = theme.customBorder || (isLight ? 'rgba(0,0,0,0.08)' : 'rgba(255,255,255,0.06)');
301
+ const fg = isLight ? '#09090b' : '#fafafa';
302
+ const mutedFg = isLight ? '#71717a' : '#a1a1aa';
303
+ applyColorScheme(root, bg, card, secondary, muted, border, fg, mutedFg);
304
+ } else if (theme.background && BACKGROUNDS[theme.background]) {
305
+ const b = BACKGROUNDS[theme.background];
306
+ applyColorScheme(root, b.bg, b.card, b.secondary, b.muted, b.border, b.fg, b.mutedFg);
307
+ }
308
+
309
+ // Text color overrides (after background to allow custom override)
310
+ if (theme.textColor) {
311
+ root.style.setProperty('--color-foreground', theme.textColor);
312
+ root.style.setProperty('--color-card-foreground', theme.textColor);
313
+ document.body.style.color = theme.textColor;
314
+ }
315
+ if (theme.mutedTextColor) {
316
+ root.style.setProperty('--color-muted-foreground', theme.mutedTextColor);
317
+ }
318
+
319
+ // --- Accent/Primary Color ---
320
+
321
+ const accent = (theme.accentColor || '#06d6a0').trim();
322
+ root.style.setProperty('--color-primary', accent);
323
+ const primaryFg = theme.buttonTextColor || (luminance(accent) > 160 ? '#000000' : '#ffffff');
324
+ root.style.setProperty('--color-primary-foreground', primaryFg);
325
+
326
+ // --- Surface Style ---
327
+
328
+ const bgHex = theme.background === 'custom'
329
+ ? (theme.customBg || '#09090b')
330
+ : (theme.background === 'light' || theme.background === 'white') ? '#fafafa' : '#09090b';
331
+ const isLightTheme = luminance(bgHex) > 128;
332
+
333
+ if (theme.surface === 'glass') {
334
+ root.style.setProperty('--card-shadow', 'none');
335
+ document.body.setAttribute('data-card-style', 'glass');
336
+ } else if (theme.surface === 'shadow') {
337
+ const shadow = isLightTheme
338
+ ? '0 1px 3px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06)'
339
+ : '0 2px 8px rgba(0,0,0,0.3), 0 1px 3px rgba(0,0,0,0.2)';
340
+ root.style.setProperty('--card-shadow', shadow);
341
+ document.body.setAttribute('data-card-style', 'shadow');
342
+ } else if (theme.surface === 'flat') {
343
+ root.style.setProperty('--card-shadow', 'none');
344
+ document.body.setAttribute('data-card-style', 'flat');
345
+ } else {
346
+ root.style.removeProperty('--card-shadow');
347
+ document.body.removeAttribute('data-card-style');
348
+ }
349
+
350
+ // --- Border Radius ---
351
+
352
+ const radiusKey = theme.borderRadius || theme.radius;
353
+ if (radiusKey && radiusKey !== 'rounded') {
354
+ document.body.setAttribute('data-radius', radiusKey);
355
+ } else {
356
+ document.body.removeAttribute('data-radius');
357
+ }
358
+
359
+ // --- Button Style ---
360
+
361
+ if (theme.buttonStyle && theme.buttonStyle !== 'rounded') {
362
+ document.documentElement.dataset.buttonStyle = theme.buttonStyle;
363
+ } else {
364
+ delete document.documentElement.dataset.buttonStyle;
365
+ }
366
+
367
+ // --- Link Colors ---
368
+
369
+ if (theme.linkColor) root.style.setProperty('--color-link', theme.linkColor);
370
+ if (theme.linkHoverColor) root.style.setProperty('--color-link-hover', theme.linkHoverColor);
371
+
372
+ // --- Layout ---
373
+
374
+ if (theme.contentWidth) {
375
+ root.style.setProperty('--content-width', CONTENT_WIDTHS[theme.contentWidth] || '1200px');
376
+ }
377
+
378
+ if (theme.sectionPadding) {
379
+ root.style.setProperty('--section-padding', SECTION_PADDINGS[theme.sectionPadding] || '3rem');
380
+ }
381
+
382
+ // --- Load Google Fonts ---
383
+
384
+ loadFont(theme.font);
385
+ loadFont(theme.headingFont);
386
+
387
+ // --- Cascade Override Style Tag ---
388
+
389
+ const overrides = [];
390
+ const props = root.style;
391
+ for (let i = 0; i < props.length; i++) {
392
+ const prop = props[i];
393
+ if (prop.startsWith('--')) {
394
+ overrides.push(`${prop}: ${props.getPropertyValue(prop)} !important`);
395
+ }
396
+ }
397
+ if (overrides.length > 0) {
398
+ let styleEl = document.getElementById('sugarcart-theme-overrides');
399
+ if (!styleEl) {
400
+ styleEl = document.createElement('style');
401
+ styleEl.id = 'sugarcart-theme-overrides';
402
+ document.head.appendChild(styleEl);
403
+ }
404
+ styleEl.textContent = `:root { ${overrides.join('; ')}; }`;
405
+ }
406
+
407
+ // --- Custom CSS ---
408
+
409
+ let customStyleEl = document.getElementById('sugarcart-custom-css');
410
+ if (theme.customCSS) {
411
+ if (!customStyleEl) {
412
+ customStyleEl = document.createElement('style');
413
+ customStyleEl.id = 'sugarcart-custom-css';
414
+ document.head.appendChild(customStyleEl);
415
+ }
416
+ customStyleEl.textContent = theme.customCSS;
417
+ } else if (customStyleEl) {
418
+ customStyleEl.textContent = '';
419
+ }
420
+ }