@chronogrove/ui 0.76.0 → 0.78.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 (54) hide show
  1. package/README.md +18 -11
  2. package/jest.config.cjs +1 -0
  3. package/package.json +35 -1
  4. package/src/__snapshots__/header.spec.js.snap +69 -0
  5. package/src/__snapshots__/page-header.spec.js.snap +11 -0
  6. package/src/action-button.js +83 -0
  7. package/src/action-button.spec.js +201 -0
  8. package/src/color-utils.js +37 -0
  9. package/src/color-utils.spec.js +43 -0
  10. package/src/gatsby/build-theme-ui-color-mode-head-components.js +36 -0
  11. package/src/gatsby/index.js +4 -0
  12. package/src/gatsby/index.spec.js +131 -0
  13. package/src/gatsby/on-pre-render-html-sort.js +21 -0
  14. package/src/gatsby/on-route-update-color-mode.js +14 -0
  15. package/src/header.js +28 -0
  16. package/src/header.spec.js +47 -0
  17. package/src/lazy-load.js +41 -0
  18. package/src/lazy-load.spec.js +88 -0
  19. package/src/page-header.js +10 -0
  20. package/src/page-header.spec.js +17 -0
  21. package/src/pagination-button.js +106 -0
  22. package/src/pagination-button.spec.js +197 -0
  23. package/.turbo/turbo-test$colon$coverage.log +0 -44
  24. package/.turbo/turbo-test.log +0 -168
  25. package/coverage/clover.xml +0 -131
  26. package/coverage/coverage-final.json +0 -13
  27. package/coverage/lcov-report/base.css +0 -224
  28. package/coverage/lcov-report/block-navigation.js +0 -87
  29. package/coverage/lcov-report/browser-sync.js.html +0 -268
  30. package/coverage/lcov-report/favicon.png +0 -0
  31. package/coverage/lcov-report/index.html +0 -161
  32. package/coverage/lcov-report/prettify.css +0 -1
  33. package/coverage/lcov-report/prettify.js +0 -2
  34. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  35. package/coverage/lcov-report/sorter.js +0 -210
  36. package/coverage/lcov-report/src/button.js.html +0 -160
  37. package/coverage/lcov-report/src/color-mode/browser-sync.js.html +0 -268
  38. package/coverage/lcov-report/src/color-mode/constants.js.html +0 -121
  39. package/coverage/lcov-report/src/color-mode/head-inline.js.html +0 -304
  40. package/coverage/lcov-report/src/color-mode/index.html +0 -176
  41. package/coverage/lcov-report/src/color-mode/index.js.html +0 -124
  42. package/coverage/lcov-report/src/color-mode/normalize.js.html +0 -112
  43. package/coverage/lcov-report/src/color-mode/resolve-theme-colors.js.html +0 -154
  44. package/coverage/lcov-report/src/color-toggle.js.html +0 -142
  45. package/coverage/lcov-report/src/emotion-cache.js.html +0 -151
  46. package/coverage/lcov-report/src/helpers/index.html +0 -116
  47. package/coverage/lcov-report/src/helpers/isDarkMode.js.html +0 -91
  48. package/coverage/lcov-report/src/index.html +0 -161
  49. package/coverage/lcov-report/src/provider.js.html +0 -124
  50. package/coverage/lcov-report/src/skip-nav/SkipNavContent.js.html +0 -133
  51. package/coverage/lcov-report/src/skip-nav/SkipNavLink.js.html +0 -301
  52. package/coverage/lcov-report/src/skip-nav/index.html +0 -131
  53. package/coverage/lcov-report/src/theme.js.html +0 -2143
  54. package/coverage/lcov.info +0 -309
package/README.md CHANGED
@@ -16,17 +16,24 @@ Use **`pnpm publish`** for releases so `workspace:` dependencies in dependents a
16
16
 
17
17
  Prefer deep imports so bundles stay lean:
18
18
 
19
- | Import path | Contents |
20
- | ------------------------------- | -------------------------------------------------------------------------------------------------- |
21
- | `@chronogrove/ui` | `ChronogroveThemeProvider` |
22
- | `@chronogrove/ui/theme` | Default Theme UI theme object + named exports |
23
- | `@chronogrove/ui/provider` | `ChronogroveThemeProvider` |
24
- | `@chronogrove/ui/color-mode` | Storage key, reconcile event, SSR inline builders, `resolveChronogroveSurfaceColors`, browser sync |
25
- | `@chronogrove/ui/emotion-cache` | `createChronogroveEmotionCache`, `getChronogroveEmotionCache` |
26
- | `@chronogrove/ui/button` | Theme UI `components` button |
27
- | `@chronogrove/ui/color-toggle` | Theme UI + `@theme-toggles/react` toggle |
28
- | `@chronogrove/ui/skip-nav` | `SkipNavLink`, `SkipNavContent` |
29
- | `@chronogrove/ui/is-dark-mode` | `colorMode === 'dark'` helper |
19
+ | Import path | Contents |
20
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------- |
21
+ | `@chronogrove/ui` | `ChronogroveThemeProvider` |
22
+ | `@chronogrove/ui/theme` | Default Theme UI theme object + named exports |
23
+ | `@chronogrove/ui/provider` | `ChronogroveThemeProvider` |
24
+ | `@chronogrove/ui/color-mode` | Storage key, reconcile event, SSR inline builders, `resolveChronogroveSurfaceColors`, browser sync |
25
+ | `@chronogrove/ui/emotion-cache` | `createChronogroveEmotionCache`, `getChronogroveEmotionCache` |
26
+ | `@chronogrove/ui/button` | Theme UI `components` button |
27
+ | `@chronogrove/ui/color-toggle` | Theme UI + `@theme-toggles/react` toggle |
28
+ | `@chronogrove/ui/skip-nav` | `SkipNavLink`, `SkipNavContent` |
29
+ | `@chronogrove/ui/is-dark-mode` | `colorMode === 'dark'` helper |
30
+ | `@chronogrove/ui/color-utils` | `hexToRgb`, `hexToRgba`, `BUTTON_PRIMARY_COLORS` |
31
+ | `@chronogrove/ui/action-button` | Outline CTA as `<button>` or `<a>` |
32
+ | `@chronogrove/ui/pagination-button` | Compact paginator control |
33
+ | `@chronogrove/ui/lazy-load` | Defer children until in viewport (`react-intersection-observer`) |
34
+ | `@chronogrove/ui/header` | Masthead shell (`variant: styles.Header`) |
35
+ | `@chronogrove/ui/page-header` | Blog-style `h1` heading (`p-name`) |
36
+ | `@chronogrove/ui/gatsby` | Color-mode Gatsby SSR/browser helpers |
30
37
 
31
38
  ## Next.js (App Router)
32
39
 
package/jest.config.cjs CHANGED
@@ -15,6 +15,7 @@ module.exports = {
15
15
  'src/index.js',
16
16
  'src/skip-nav/index.js',
17
17
  'src/color-mode/index.js',
18
+ 'src/gatsby/index.js',
18
19
  'src/theme.js'
19
20
  ],
20
21
  moduleNameMapper: {
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@chronogrove/ui",
3
- "version": "0.76.0",
3
+ "version": "0.78.0",
4
4
  "description": "Chronogrove Theme UI theme, color mode helpers, and shared UI primitives",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/chrisvogt/gatsby-theme-chronogrove.git",
9
+ "directory": "packages/ui"
10
+ },
6
11
  "sideEffects": false,
7
12
  "exports": {
8
13
  ".": {
@@ -40,6 +45,34 @@
40
45
  "./is-dark-mode": {
41
46
  "import": "./src/helpers/isDarkMode.js",
42
47
  "default": "./src/helpers/isDarkMode.js"
48
+ },
49
+ "./color-utils": {
50
+ "import": "./src/color-utils.js",
51
+ "default": "./src/color-utils.js"
52
+ },
53
+ "./action-button": {
54
+ "import": "./src/action-button.js",
55
+ "default": "./src/action-button.js"
56
+ },
57
+ "./pagination-button": {
58
+ "import": "./src/pagination-button.js",
59
+ "default": "./src/pagination-button.js"
60
+ },
61
+ "./lazy-load": {
62
+ "import": "./src/lazy-load.js",
63
+ "default": "./src/lazy-load.js"
64
+ },
65
+ "./header": {
66
+ "import": "./src/header.js",
67
+ "default": "./src/header.js"
68
+ },
69
+ "./page-header": {
70
+ "import": "./src/page-header.js",
71
+ "default": "./src/page-header.js"
72
+ },
73
+ "./gatsby": {
74
+ "import": "./src/gatsby/index.js",
75
+ "default": "./src/gatsby/index.js"
43
76
  }
44
77
  },
45
78
  "peerDependencies": {
@@ -52,6 +85,7 @@
52
85
  "@theme-toggles/react": "^4.1.0",
53
86
  "@theme-ui/components": "^0.17.4",
54
87
  "@theme-ui/presets": "^0.17.4",
88
+ "react-intersection-observer": "^10.0.3",
55
89
  "theme-ui": "^0.17.4"
56
90
  },
57
91
  "devDependencies": {
@@ -0,0 +1,69 @@
1
+ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
+
3
+ exports[`Header renders with children 1`] = `
4
+ <DocumentFragment>
5
+ <header
6
+ class="css-1u8qly9"
7
+ role="banner"
8
+ >
9
+ <div
10
+ class="css-1u8qly9"
11
+ >
12
+ <h1>
13
+ Test Header
14
+ </h1>
15
+ </div>
16
+ </header>
17
+ </DocumentFragment>
18
+ `;
19
+
20
+ exports[`Header renders with custom styles 1`] = `
21
+ <DocumentFragment>
22
+ <header
23
+ class="css-1u8qly9"
24
+ role="banner"
25
+ >
26
+ <div
27
+ class="css-10n8us"
28
+ >
29
+ <h1>
30
+ Test Header with Styles
31
+ </h1>
32
+ </div>
33
+ </header>
34
+ </DocumentFragment>
35
+ `;
36
+
37
+ exports[`Header renders with empty styles object 1`] = `
38
+ <DocumentFragment>
39
+ <header
40
+ class="css-1u8qly9"
41
+ role="banner"
42
+ >
43
+ <div
44
+ class="css-1u8qly9"
45
+ >
46
+ <h1>
47
+ Test Header with Empty Styles
48
+ </h1>
49
+ </div>
50
+ </header>
51
+ </DocumentFragment>
52
+ `;
53
+
54
+ exports[`Header renders without styles prop 1`] = `
55
+ <DocumentFragment>
56
+ <header
57
+ class="css-1u8qly9"
58
+ role="banner"
59
+ >
60
+ <div
61
+ class="css-1u8qly9"
62
+ >
63
+ <h1>
64
+ Test Header without Styles
65
+ </h1>
66
+ </div>
67
+ </header>
68
+ </DocumentFragment>
69
+ `;
@@ -0,0 +1,11 @@
1
+ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
+
3
+ exports[`PageHeader matches snapshot 1`] = `
4
+ <DocumentFragment>
5
+ <h1
6
+ class="p-name css-1sus4jf"
7
+ >
8
+ Hello, World!
9
+ </h1>
10
+ </DocumentFragment>
11
+ `;
@@ -0,0 +1,83 @@
1
+ /** @jsx jsx */
2
+ import React from 'react'
3
+ import { jsx, useThemeUI } from 'theme-ui'
4
+ import isDarkMode from './helpers/isDarkMode.js'
5
+ import { hexToRgb } from './color-utils.js'
6
+
7
+ const ActionButton = ({ children, href, onClick, variant = 'primary', size = 'medium', icon, ...props }) => {
8
+ const { colorMode, theme } = useThemeUI()
9
+ const darkModeActive = isDarkMode(colorMode)
10
+
11
+ const colorVariants = {
12
+ secondary: { light: '#666', dark: '#888' },
13
+ success: { light: '#28a745', dark: '#4CAF50' },
14
+ warning: { light: '#ffc107', dark: '#FF9800' },
15
+ danger: { light: '#dc3545', dark: '#f44336' }
16
+ }
17
+ const isPrimary = variant === 'primary' || !colorVariants[variant]
18
+ const primaryColor = isPrimary
19
+ ? (theme?.colors?.primary ?? '#422EA3')
20
+ : darkModeActive
21
+ ? colorVariants[variant].dark
22
+ : colorVariants[variant].light
23
+ const rgb = isPrimary ? (theme?.colors?.primaryRgb ?? '66, 46, 163') : hexToRgb(primaryColor)
24
+
25
+ const sizeVariants = {
26
+ small: { fontSize: ['11px', '12px'], padding: '6px 10px', gap: 1 },
27
+ medium: { fontSize: ['12px', '13px'], padding: '8px 12px', gap: 1 },
28
+ large: { fontSize: ['13px', '14px'], padding: '10px 16px', gap: 2 }
29
+ }
30
+ const sizeStyles = sizeVariants[size] || sizeVariants.medium
31
+
32
+ const baseStyles = {
33
+ color: primaryColor,
34
+ textDecoration: 'none',
35
+ fontWeight: 'medium',
36
+ fontSize: sizeStyles.fontSize,
37
+ display: 'inline-flex',
38
+ alignItems: 'center',
39
+ gap: sizeStyles.gap,
40
+ padding: sizeStyles.padding,
41
+ borderRadius: '6px',
42
+ background: `rgba(${rgb}, 0.1)`,
43
+ border: `1px solid rgba(${rgb}, 0.2)`,
44
+ transition: 'all 0.2s ease',
45
+ cursor: 'pointer',
46
+ outline: 'none',
47
+ '&:hover': {
48
+ background: `rgba(${rgb}, 0.2)`,
49
+ textDecoration: 'none',
50
+ transform: 'scale(1.02)'
51
+ },
52
+ '&:focus': {
53
+ outline: 'none',
54
+ boxShadow: `0 0 0 2px ${primaryColor}20`
55
+ },
56
+ '&:active': {
57
+ transform: 'scale(0.98)'
58
+ }
59
+ }
60
+
61
+ const content = (
62
+ <>
63
+ {children}
64
+ {icon && icon}
65
+ </>
66
+ )
67
+
68
+ if (href) {
69
+ return (
70
+ <a href={href} sx={baseStyles} {...props}>
71
+ {content}
72
+ </a>
73
+ )
74
+ }
75
+
76
+ return (
77
+ <button type='button' onClick={onClick} sx={baseStyles} {...props}>
78
+ {content}
79
+ </button>
80
+ )
81
+ }
82
+
83
+ export default ActionButton
@@ -0,0 +1,201 @@
1
+ /** @jsx jsx */
2
+ import { jsx } from 'theme-ui'
3
+ import { render, screen, fireEvent } from '@testing-library/react'
4
+ import { ThemeUIProvider } from 'theme-ui'
5
+
6
+ import ActionButton from './action-button.js'
7
+ import { BUTTON_PRIMARY_COLORS } from './color-utils.js'
8
+
9
+ const mockUseThemeUI = jest.fn(() => ({
10
+ colorMode: 'default',
11
+ theme: {
12
+ colors: {
13
+ primary: BUTTON_PRIMARY_COLORS.light,
14
+ primaryRgb: '66, 46, 163'
15
+ }
16
+ }
17
+ }))
18
+
19
+ jest.mock('theme-ui', () => ({
20
+ ...jest.requireActual('theme-ui'),
21
+ useThemeUI: () => mockUseThemeUI()
22
+ }))
23
+
24
+ const mockTheme = {
25
+ colors: {
26
+ primary: BUTTON_PRIMARY_COLORS.light,
27
+ primaryRgb: '66, 46, 163',
28
+ modes: {
29
+ dark: {
30
+ text: '#ffffff',
31
+ background: '#000000'
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ const renderWithProviders = (component, customTheme = null) => {
38
+ const themeToUse = customTheme ?? mockTheme
39
+ return render(<ThemeUIProvider theme={themeToUse}>{component}</ThemeUIProvider>)
40
+ }
41
+
42
+ describe('ActionButton', () => {
43
+ it('renders as a button by default', () => {
44
+ renderWithProviders(<ActionButton>Test Button</ActionButton>)
45
+
46
+ const button = screen.getByRole('button', { name: /test button/i })
47
+ expect(button).toBeInTheDocument()
48
+ expect(button.tagName).toBe('BUTTON')
49
+ })
50
+
51
+ it('renders as a link when href is provided', () => {
52
+ renderWithProviders(<ActionButton href='/test'>Test Link</ActionButton>)
53
+
54
+ const link = screen.getByRole('link')
55
+ expect(link).toBeInTheDocument()
56
+ expect(link.tagName).toBe('A')
57
+ expect(link).toHaveAttribute('href', '/test')
58
+ })
59
+
60
+ it('handles click events', () => {
61
+ const handleClick = jest.fn()
62
+ renderWithProviders(<ActionButton onClick={handleClick}>Click Me</ActionButton>)
63
+
64
+ const button = screen.getByRole('button', { name: /click me/i })
65
+ fireEvent.click(button)
66
+
67
+ expect(handleClick).toHaveBeenCalledTimes(1)
68
+ })
69
+
70
+ it('applies primary variant styles by default', () => {
71
+ renderWithProviders(<ActionButton>Primary Button</ActionButton>)
72
+
73
+ const button = screen.getByRole('button', { name: /primary button/i })
74
+ expect(button).toHaveStyle({ fontWeight: 'medium' })
75
+ expect(button).toBeInTheDocument()
76
+ })
77
+
78
+ it('applies secondary variant styles', () => {
79
+ renderWithProviders(<ActionButton variant='secondary'>Secondary Button</ActionButton>)
80
+
81
+ const button = screen.getByRole('button', { name: /secondary button/i })
82
+ expect(button).toHaveStyle({
83
+ color: '#666'
84
+ })
85
+ })
86
+
87
+ it('applies small size styles', () => {
88
+ renderWithProviders(<ActionButton size='small'>Small Button</ActionButton>)
89
+
90
+ const button = screen.getByRole('button', { name: /small button/i })
91
+ expect(button).toHaveStyle({
92
+ fontSize: '11px',
93
+ padding: '6px 10px'
94
+ })
95
+ })
96
+
97
+ it('applies large size styles', () => {
98
+ renderWithProviders(<ActionButton size='large'>Large Button</ActionButton>)
99
+
100
+ const button = screen.getByRole('button', { name: /large button/i })
101
+ expect(button).toHaveStyle({
102
+ fontSize: '13px',
103
+ padding: '10px 16px'
104
+ })
105
+ })
106
+
107
+ it('renders with icon', () => {
108
+ const TestIcon = () => <span data-testid='test-icon'>→</span>
109
+ renderWithProviders(<ActionButton icon={<TestIcon />}>Button with Icon</ActionButton>)
110
+
111
+ expect(screen.getByTestId('test-icon')).toBeInTheDocument()
112
+ expect(screen.getByRole('button', { name: /button with icon/i })).toBeInTheDocument()
113
+ })
114
+
115
+ it('passes through additional props', () => {
116
+ renderWithProviders(
117
+ <ActionButton data-testid='custom-button' aria-label='Custom label'>
118
+ Custom Button
119
+ </ActionButton>
120
+ )
121
+
122
+ const button = screen.getByTestId('custom-button')
123
+ expect(button).toHaveAttribute('aria-label', 'Custom label')
124
+ })
125
+
126
+ it('has proper accessibility attributes', () => {
127
+ renderWithProviders(<ActionButton>Accessible Button</ActionButton>)
128
+
129
+ const button = screen.getByRole('button', { name: /accessible button/i })
130
+ expect(button).toHaveAttribute('type', 'button')
131
+ })
132
+
133
+ it('falls back to primary variant when invalid variant is provided', () => {
134
+ renderWithProviders(<ActionButton variant='invalid'>Invalid Variant</ActionButton>)
135
+
136
+ const button = screen.getByRole('button', { name: /invalid variant/i })
137
+ expect(button).toBeInTheDocument()
138
+ expect(button).toHaveStyle({ fontWeight: 'medium' })
139
+ })
140
+
141
+ it('falls back to medium size when invalid size is provided', () => {
142
+ renderWithProviders(<ActionButton size='invalid'>Invalid Size</ActionButton>)
143
+
144
+ const button = screen.getByRole('button', { name: /invalid size/i })
145
+ expect(button).toHaveStyle({
146
+ fontSize: '12px',
147
+ padding: '8px 12px'
148
+ })
149
+ })
150
+
151
+ describe('theme fallbacks', () => {
152
+ beforeEach(() => {
153
+ mockUseThemeUI.mockReturnValue({
154
+ colorMode: 'default',
155
+ theme: {
156
+ colors: {
157
+ primary: BUTTON_PRIMARY_COLORS.light,
158
+ primaryRgb: '66, 46, 163'
159
+ }
160
+ }
161
+ })
162
+ })
163
+
164
+ it('uses fallback primary color when theme.colors.primary is undefined', () => {
165
+ mockUseThemeUI.mockReturnValueOnce({
166
+ colorMode: 'default',
167
+ theme: { colors: {} }
168
+ })
169
+
170
+ renderWithProviders(<ActionButton>Fallback Test</ActionButton>)
171
+
172
+ const button = screen.getByRole('button', { name: /fallback test/i })
173
+ expect(button).toBeInTheDocument()
174
+ expect(button).toHaveStyle({ fontWeight: 'medium' })
175
+ })
176
+
177
+ it('uses fallback primaryRgb when theme.colors.primaryRgb is undefined', () => {
178
+ mockUseThemeUI.mockReturnValueOnce({
179
+ colorMode: 'default',
180
+ theme: { colors: { primary: '#422EA3' } }
181
+ })
182
+
183
+ renderWithProviders(<ActionButton>Fallback RGB Test</ActionButton>)
184
+
185
+ const button = screen.getByRole('button', { name: /fallback rgb test/i })
186
+ expect(button).toBeInTheDocument()
187
+ })
188
+
189
+ it('uses fallback when theme itself is undefined', () => {
190
+ mockUseThemeUI.mockReturnValueOnce({
191
+ colorMode: 'default',
192
+ theme: undefined
193
+ })
194
+
195
+ renderWithProviders(<ActionButton>Undefined Theme Test</ActionButton>)
196
+
197
+ const button = screen.getByRole('button', { name: /undefined theme test/i })
198
+ expect(button).toBeInTheDocument()
199
+ })
200
+ })
201
+ })
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared color utilities and constants for consistent button and UI styling.
3
+ * Single source of truth for primary button colors and hex conversion.
4
+ */
5
+
6
+ /** Fallback RGB string when hex is invalid (blue) */
7
+ const HEX_TO_RGB_FALLBACK = '74, 158, 255'
8
+
9
+ /**
10
+ * Converts hex color to RGB string for use in rgba()
11
+ * @param {string} hex - Hex color code (with or without #)
12
+ * @returns {string} RGB values as comma-separated string (e.g. "66, 46, 163")
13
+ */
14
+ export const hexToRgb = hex => {
15
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
16
+ if (!result) return HEX_TO_RGB_FALLBACK
17
+ return `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`
18
+ }
19
+
20
+ /**
21
+ * Converts hex color to rgba string
22
+ * @param {string} hex - Hex color code (with or without #)
23
+ * @param {number} alpha - Alpha value 0–1
24
+ * @returns {string} rgba() CSS value
25
+ */
26
+ export const hexToRgba = (hex, alpha) => {
27
+ return `rgba(${hexToRgb(hex)}, ${alpha})`
28
+ }
29
+
30
+ /**
31
+ * Primary button/CTA colors for light and dark mode.
32
+ * Aligns with theme colors.primary and theme.colors.primaryRgb.
33
+ */
34
+ export const BUTTON_PRIMARY_COLORS = {
35
+ light: '#422EA3',
36
+ dark: '#4a9eff'
37
+ }
@@ -0,0 +1,43 @@
1
+ import { hexToRgb, hexToRgba, BUTTON_PRIMARY_COLORS } from './color-utils'
2
+
3
+ describe('hexToRgb', () => {
4
+ it('converts hex with hash to RGB', () => {
5
+ expect(hexToRgb('#422EA3')).toBe('66, 46, 163')
6
+ })
7
+
8
+ it('converts hex without hash to RGB', () => {
9
+ expect(hexToRgb('4a9eff')).toBe('74, 158, 255')
10
+ })
11
+
12
+ it('returns fallback for invalid hex', () => {
13
+ expect(hexToRgb('invalid')).toBe('74, 158, 255')
14
+ })
15
+
16
+ it('returns fallback for empty string', () => {
17
+ expect(hexToRgb('')).toBe('74, 158, 255')
18
+ })
19
+ })
20
+
21
+ describe('hexToRgba', () => {
22
+ it('converts hex to rgba string', () => {
23
+ expect(hexToRgba('#422EA3', 0.5)).toBe('rgba(66, 46, 163, 0.5)')
24
+ })
25
+
26
+ it('accepts hex without hash', () => {
27
+ expect(hexToRgba('4a9eff', 1)).toBe('rgba(74, 158, 255, 1)')
28
+ })
29
+
30
+ it('uses fallback RGB for invalid hex', () => {
31
+ expect(hexToRgba('invalid', 0.2)).toBe('rgba(74, 158, 255, 0.2)')
32
+ })
33
+ })
34
+
35
+ describe('BUTTON_PRIMARY_COLORS', () => {
36
+ it('exports light mode color', () => {
37
+ expect(BUTTON_PRIMARY_COLORS.light).toBe('#422EA3')
38
+ })
39
+
40
+ it('exports dark mode color', () => {
41
+ expect(BUTTON_PRIMARY_COLORS.dark).toBe('#4a9eff')
42
+ })
43
+ })
@@ -0,0 +1,36 @@
1
+ import React from 'react'
2
+
3
+ import { resolveChronogroveSurfaceColors } from '../color-mode/resolve-theme-colors.js'
4
+ import {
5
+ buildThemeUiNoFlashInlineScript,
6
+ buildHtmlBackgroundInlineScript,
7
+ buildThemeUiColorModeFallbackCss
8
+ } from '../color-mode/head-inline.js'
9
+
10
+ /**
11
+ * React head elements for Theme UI color mode: no-flash script, HTML background script, fallback CSS.
12
+ * Compose with your own meta tags (e.g. Emotion insertion point) in `onRenderBody`.
13
+ *
14
+ * @param {{ theme: object }} options — Theme UI theme object (same as `ThemeUIProvider`)
15
+ * @returns {import('react').ReactElement[]}
16
+ */
17
+ export function buildThemeUiColorModeHeadComponents({ theme }) {
18
+ const surface = resolveChronogroveSurfaceColors(theme)
19
+ const colorModeScript = buildThemeUiNoFlashInlineScript()
20
+ const htmlBackgroundScript = buildHtmlBackgroundInlineScript({
21
+ defaultBackgroundHex: surface.defaultBackgroundHex,
22
+ darkBackgroundHex: surface.darkBackgroundHex
23
+ })
24
+ const colorModeFallbackCSS = buildThemeUiColorModeFallbackCss({
25
+ defaultTextHex: surface.defaultTextHex,
26
+ defaultTextMutedHex: surface.defaultTextMutedHex,
27
+ darkTextHex: surface.darkTextHex,
28
+ darkTextMutedHex: surface.darkTextMutedHex
29
+ })
30
+
31
+ return [
32
+ <script key='theme-ui-no-flash' dangerouslySetInnerHTML={{ __html: colorModeScript }} />,
33
+ <script key='html-bg-color' dangerouslySetInnerHTML={{ __html: htmlBackgroundScript }} />,
34
+ <style key='theme-ui-color-mode-fallback' dangerouslySetInnerHTML={{ __html: colorModeFallbackCSS }} />
35
+ ]
36
+ }
@@ -0,0 +1,4 @@
1
+ export { buildThemeUiColorModeHeadComponents } from './build-theme-ui-color-mode-head-components.js'
2
+ export { onPreRenderHTMLSortThemeUiColorModeFirst } from './on-pre-render-html-sort.js'
3
+ export { onRouteUpdateThemeUiColorMode } from './on-route-update-color-mode.js'
4
+ export { RECONCILE_COLOR_MODE_EVENT } from '../color-mode/constants.js'