@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 +21 -0
- package/README.md +114 -0
- package/package.json +39 -0
- package/src/index.d.ts +119 -0
- package/src/index.js +420 -0
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
|
+
}
|