@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.
- package/README.md +18 -11
- package/jest.config.cjs +1 -0
- package/package.json +35 -1
- package/src/__snapshots__/header.spec.js.snap +69 -0
- package/src/__snapshots__/page-header.spec.js.snap +11 -0
- package/src/action-button.js +83 -0
- package/src/action-button.spec.js +201 -0
- package/src/color-utils.js +37 -0
- package/src/color-utils.spec.js +43 -0
- package/src/gatsby/build-theme-ui-color-mode-head-components.js +36 -0
- package/src/gatsby/index.js +4 -0
- package/src/gatsby/index.spec.js +131 -0
- package/src/gatsby/on-pre-render-html-sort.js +21 -0
- package/src/gatsby/on-route-update-color-mode.js +14 -0
- package/src/header.js +28 -0
- package/src/header.spec.js +47 -0
- package/src/lazy-load.js +41 -0
- package/src/lazy-load.spec.js +88 -0
- package/src/page-header.js +10 -0
- package/src/page-header.spec.js +17 -0
- package/src/pagination-button.js +106 -0
- package/src/pagination-button.spec.js +197 -0
- package/.turbo/turbo-test$colon$coverage.log +0 -44
- package/.turbo/turbo-test.log +0 -168
- package/coverage/clover.xml +0 -131
- package/coverage/coverage-final.json +0 -13
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/browser-sync.js.html +0 -268
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -161
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/src/button.js.html +0 -160
- package/coverage/lcov-report/src/color-mode/browser-sync.js.html +0 -268
- package/coverage/lcov-report/src/color-mode/constants.js.html +0 -121
- package/coverage/lcov-report/src/color-mode/head-inline.js.html +0 -304
- package/coverage/lcov-report/src/color-mode/index.html +0 -176
- package/coverage/lcov-report/src/color-mode/index.js.html +0 -124
- package/coverage/lcov-report/src/color-mode/normalize.js.html +0 -112
- package/coverage/lcov-report/src/color-mode/resolve-theme-colors.js.html +0 -154
- package/coverage/lcov-report/src/color-toggle.js.html +0 -142
- package/coverage/lcov-report/src/emotion-cache.js.html +0 -151
- package/coverage/lcov-report/src/helpers/index.html +0 -116
- package/coverage/lcov-report/src/helpers/isDarkMode.js.html +0 -91
- package/coverage/lcov-report/src/index.html +0 -161
- package/coverage/lcov-report/src/provider.js.html +0 -124
- package/coverage/lcov-report/src/skip-nav/SkipNavContent.js.html +0 -133
- package/coverage/lcov-report/src/skip-nav/SkipNavLink.js.html +0 -301
- package/coverage/lcov-report/src/skip-nav/index.html +0 -131
- package/coverage/lcov-report/src/theme.js.html +0 -2143
- 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
|
|
20
|
-
|
|
|
21
|
-
| `@chronogrove/ui`
|
|
22
|
-
| `@chronogrove/ui/theme`
|
|
23
|
-
| `@chronogrove/ui/provider`
|
|
24
|
-
| `@chronogrove/ui/color-mode`
|
|
25
|
-
| `@chronogrove/ui/emotion-cache`
|
|
26
|
-
| `@chronogrove/ui/button`
|
|
27
|
-
| `@chronogrove/ui/color-toggle`
|
|
28
|
-
| `@chronogrove/ui/skip-nav`
|
|
29
|
-
| `@chronogrove/ui/is-dark-mode`
|
|
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
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chronogrove/ui",
|
|
3
|
-
"version": "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,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'
|