@chronogrove/ui 0.76.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/.turbo/turbo-test$colon$coverage.log +44 -0
- package/.turbo/turbo-test.log +168 -0
- package/LICENSE +9 -0
- package/README.md +41 -0
- package/babel.config.cjs +9 -0
- package/coverage/clover.xml +131 -0
- package/coverage/coverage-final.json +13 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/browser-sync.js.html +268 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +161 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/button.js.html +160 -0
- package/coverage/lcov-report/src/color-mode/browser-sync.js.html +268 -0
- package/coverage/lcov-report/src/color-mode/constants.js.html +121 -0
- package/coverage/lcov-report/src/color-mode/head-inline.js.html +304 -0
- package/coverage/lcov-report/src/color-mode/index.html +176 -0
- package/coverage/lcov-report/src/color-mode/index.js.html +124 -0
- package/coverage/lcov-report/src/color-mode/normalize.js.html +112 -0
- package/coverage/lcov-report/src/color-mode/resolve-theme-colors.js.html +154 -0
- package/coverage/lcov-report/src/color-toggle.js.html +142 -0
- package/coverage/lcov-report/src/emotion-cache.js.html +151 -0
- package/coverage/lcov-report/src/helpers/index.html +116 -0
- package/coverage/lcov-report/src/helpers/isDarkMode.js.html +91 -0
- package/coverage/lcov-report/src/index.html +161 -0
- package/coverage/lcov-report/src/provider.js.html +124 -0
- package/coverage/lcov-report/src/skip-nav/SkipNavContent.js.html +133 -0
- package/coverage/lcov-report/src/skip-nav/SkipNavLink.js.html +301 -0
- package/coverage/lcov-report/src/skip-nav/index.html +131 -0
- package/coverage/lcov-report/src/theme.js.html +2143 -0
- package/coverage/lcov.info +309 -0
- package/jest.config.cjs +32 -0
- package/jest.setup.cjs +1 -0
- package/package.json +73 -0
- package/src/__snapshots__/theme.spec.js.snap +1027 -0
- package/src/button.js +25 -0
- package/src/button.spec.js +16 -0
- package/src/color-mode/browser-sync.js +61 -0
- package/src/color-mode/browser-sync.node.spec.js +15 -0
- package/src/color-mode/browser-sync.spec.js +137 -0
- package/src/color-mode/constants.js +12 -0
- package/src/color-mode/head-inline.js +73 -0
- package/src/color-mode/head-inline.spec.js +33 -0
- package/src/color-mode/index.js +13 -0
- package/src/color-mode/normalize.js +9 -0
- package/src/color-mode/normalize.spec.js +17 -0
- package/src/color-mode/resolve-theme-colors.js +23 -0
- package/src/color-mode/resolve-theme-colors.spec.js +39 -0
- package/src/color-toggle.js +19 -0
- package/src/color-toggle.spec.js +35 -0
- package/src/emotion-cache.js +22 -0
- package/src/emotion-cache.spec.js +30 -0
- package/src/helpers/isDarkMode.js +2 -0
- package/src/helpers/isDarkMode.spec.js +9 -0
- package/src/index.js +1 -0
- package/src/provider.js +13 -0
- package/src/provider.spec.js +25 -0
- package/src/skip-nav/SkipNavContent.js +16 -0
- package/src/skip-nav/SkipNavContent.spec.js +22 -0
- package/src/skip-nav/SkipNavLink.js +72 -0
- package/src/skip-nav/SkipNavLink.spec.js +56 -0
- package/src/skip-nav/index.js +2 -0
- package/src/theme.js +686 -0
- package/src/theme.spec.js +56 -0
- package/test-utils/mock-theme-toggles-react.js +10 -0
- package/turbo.json +12 -0
package/src/button.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** @jsx jsx */
|
|
2
|
+
import { jsx } from 'theme-ui'
|
|
3
|
+
|
|
4
|
+
const Button = ({ variant = 'primary', ...props }) => (
|
|
5
|
+
<button
|
|
6
|
+
{...props}
|
|
7
|
+
sx={{
|
|
8
|
+
appearance: 'none',
|
|
9
|
+
display: 'inline-block',
|
|
10
|
+
textAlign: 'center',
|
|
11
|
+
lineHeight: 'inherit',
|
|
12
|
+
textDecoration: 'none',
|
|
13
|
+
fontSize: 'inherit',
|
|
14
|
+
fontWeight: 'bold',
|
|
15
|
+
m: 0,
|
|
16
|
+
px: 3,
|
|
17
|
+
py: 2,
|
|
18
|
+
border: 0,
|
|
19
|
+
borderRadius: 4,
|
|
20
|
+
variant: `buttons.${variant}`
|
|
21
|
+
}}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
export default Button
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
7
|
+
import Button from './button'
|
|
8
|
+
|
|
9
|
+
describe('Button', () => {
|
|
10
|
+
it('renders with label and handles click', () => {
|
|
11
|
+
const onClick = jest.fn()
|
|
12
|
+
render(<Button onClick={onClick}>Save</Button>)
|
|
13
|
+
fireEvent.click(screen.getByRole('button', { name: /save/i }))
|
|
14
|
+
expect(onClick).toHaveBeenCalledTimes(1)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { THEME_UI_COLOR_MODE_STORAGE_KEY } from './constants.js'
|
|
2
|
+
import { normalizeThemeUiColorMode } from './normalize.js'
|
|
3
|
+
|
|
4
|
+
export function resolveThemeUiColorMode(storageKey = THEME_UI_COLOR_MODE_STORAGE_KEY) {
|
|
5
|
+
let mode
|
|
6
|
+
try {
|
|
7
|
+
mode = typeof window !== 'undefined' ? window.localStorage.getItem(storageKey) : null
|
|
8
|
+
} catch {
|
|
9
|
+
mode = null
|
|
10
|
+
}
|
|
11
|
+
mode = normalizeThemeUiColorMode(mode)
|
|
12
|
+
if (mode) {
|
|
13
|
+
return mode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof document !== 'undefined') {
|
|
17
|
+
const htmlElement = document.documentElement
|
|
18
|
+
const domAttributeMode = normalizeThemeUiColorMode(htmlElement?.getAttribute('data-theme-ui-color-mode'))
|
|
19
|
+
if (domAttributeMode) {
|
|
20
|
+
return domAttributeMode
|
|
21
|
+
}
|
|
22
|
+
if (htmlElement?.classList?.contains('theme-ui-dark')) {
|
|
23
|
+
return 'dark'
|
|
24
|
+
}
|
|
25
|
+
if (htmlElement?.classList?.contains('theme-ui-default') || htmlElement?.classList?.contains('theme-ui-light')) {
|
|
26
|
+
return 'default'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const prefersDark =
|
|
31
|
+
typeof window !== 'undefined' &&
|
|
32
|
+
typeof window.matchMedia === 'function' &&
|
|
33
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
34
|
+
|
|
35
|
+
return prefersDark ? 'dark' : 'default'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function syncThemeUiColorMode(storageKey = THEME_UI_COLOR_MODE_STORAGE_KEY) {
|
|
39
|
+
if (typeof document === 'undefined') {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
const htmlElement = document.documentElement
|
|
43
|
+
if (!htmlElement) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
const mode = resolveThemeUiColorMode(storageKey)
|
|
47
|
+
Array.from(htmlElement.classList)
|
|
48
|
+
.filter(className => className.startsWith('theme-ui-'))
|
|
49
|
+
.forEach(className => htmlElement.classList.remove(className))
|
|
50
|
+
htmlElement.classList.add(`theme-ui-${mode}`)
|
|
51
|
+
htmlElement.setAttribute('data-theme-ui-color-mode', mode)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function scheduleThemeUiColorModeSync(storageKey = THEME_UI_COLOR_MODE_STORAGE_KEY) {
|
|
55
|
+
syncThemeUiColorMode(storageKey)
|
|
56
|
+
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
|
|
57
|
+
window.requestAnimationFrame(() => syncThemeUiColorMode(storageKey))
|
|
58
|
+
} else {
|
|
59
|
+
setTimeout(() => syncThemeUiColorMode(storageKey), 0)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolveThemeUiColorMode, syncThemeUiColorMode } from './browser-sync'
|
|
6
|
+
|
|
7
|
+
describe('browser-sync (node)', () => {
|
|
8
|
+
it('resolve falls back to default without browser APIs', () => {
|
|
9
|
+
expect(resolveThemeUiColorMode()).toBe('default')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('sync is a no-op without document', () => {
|
|
13
|
+
expect(() => syncThemeUiColorMode()).not.toThrow()
|
|
14
|
+
})
|
|
15
|
+
})
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolveThemeUiColorMode, scheduleThemeUiColorModeSync, syncThemeUiColorMode } from './browser-sync'
|
|
6
|
+
|
|
7
|
+
describe('resolveThemeUiColorMode', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
document.documentElement.className = ''
|
|
10
|
+
document.documentElement.removeAttribute('data-theme-ui-color-mode')
|
|
11
|
+
window.localStorage.clear()
|
|
12
|
+
window.matchMedia = jest.fn(() => ({
|
|
13
|
+
matches: false,
|
|
14
|
+
media: '(prefers-color-scheme: dark)',
|
|
15
|
+
addListener: jest.fn(),
|
|
16
|
+
removeListener: jest.fn(),
|
|
17
|
+
addEventListener: jest.fn(),
|
|
18
|
+
removeEventListener: jest.fn(),
|
|
19
|
+
dispatchEvent: jest.fn(),
|
|
20
|
+
onchange: null
|
|
21
|
+
}))
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('reads normalized mode from localStorage', () => {
|
|
25
|
+
window.localStorage.setItem('theme-ui-color-mode', 'dark')
|
|
26
|
+
expect(resolveThemeUiColorMode()).toBe('dark')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('reads from data-theme-ui-color-mode on documentElement', () => {
|
|
30
|
+
document.documentElement.setAttribute('data-theme-ui-color-mode', 'dark')
|
|
31
|
+
expect(resolveThemeUiColorMode()).toBe('dark')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('infers default from theme-ui-light class', () => {
|
|
35
|
+
document.documentElement.classList.add('theme-ui-light')
|
|
36
|
+
expect(resolveThemeUiColorMode()).toBe('default')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('infers dark from theme-ui-dark class when storage is empty', () => {
|
|
40
|
+
document.documentElement.classList.add('theme-ui-dark')
|
|
41
|
+
expect(resolveThemeUiColorMode()).toBe('dark')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('treats unreadable localStorage as empty', () => {
|
|
45
|
+
jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
|
46
|
+
throw new Error('blocked')
|
|
47
|
+
})
|
|
48
|
+
expect(resolveThemeUiColorMode()).toBe('default')
|
|
49
|
+
Storage.prototype.getItem.mockRestore()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('falls back to prefers-color-scheme: dark', () => {
|
|
53
|
+
window.matchMedia = jest.fn(() => ({
|
|
54
|
+
matches: true,
|
|
55
|
+
media: '(prefers-color-scheme: dark)',
|
|
56
|
+
addListener: jest.fn(),
|
|
57
|
+
removeListener: jest.fn(),
|
|
58
|
+
addEventListener: jest.fn(),
|
|
59
|
+
removeEventListener: jest.fn(),
|
|
60
|
+
dispatchEvent: jest.fn(),
|
|
61
|
+
onchange: null
|
|
62
|
+
}))
|
|
63
|
+
expect(resolveThemeUiColorMode()).toBe('dark')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('syncThemeUiColorMode', () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
document.documentElement.className = ''
|
|
70
|
+
document.documentElement.removeAttribute('data-theme-ui-color-mode')
|
|
71
|
+
window.localStorage.clear()
|
|
72
|
+
window.matchMedia = jest.fn(() => ({
|
|
73
|
+
matches: false,
|
|
74
|
+
media: '(prefers-color-scheme: dark)',
|
|
75
|
+
addListener: jest.fn(),
|
|
76
|
+
removeListener: jest.fn(),
|
|
77
|
+
addEventListener: jest.fn(),
|
|
78
|
+
removeEventListener: jest.fn(),
|
|
79
|
+
dispatchEvent: jest.fn(),
|
|
80
|
+
onchange: null
|
|
81
|
+
}))
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('applies theme-ui class and data attribute from resolved mode', () => {
|
|
85
|
+
window.localStorage.setItem('theme-ui-color-mode', 'dark')
|
|
86
|
+
syncThemeUiColorMode()
|
|
87
|
+
expect(document.documentElement.classList.contains('theme-ui-dark')).toBe(true)
|
|
88
|
+
expect(document.documentElement.getAttribute('data-theme-ui-color-mode')).toBe('dark')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('strips prior theme-ui-* classes before applying', () => {
|
|
92
|
+
document.documentElement.classList.add('theme-ui-light', 'theme-ui-other')
|
|
93
|
+
window.localStorage.setItem('theme-ui-color-mode', 'dark')
|
|
94
|
+
syncThemeUiColorMode()
|
|
95
|
+
expect(document.documentElement.classList.contains('theme-ui-light')).toBe(false)
|
|
96
|
+
expect(document.documentElement.classList.contains('theme-ui-other')).toBe(false)
|
|
97
|
+
expect(document.documentElement.classList.contains('theme-ui-dark')).toBe(true)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('returns early when documentElement is missing', () => {
|
|
101
|
+
const spy = jest.spyOn(document, 'documentElement', 'get').mockReturnValue(null)
|
|
102
|
+
expect(() => syncThemeUiColorMode()).not.toThrow()
|
|
103
|
+
spy.mockRestore()
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('scheduleThemeUiColorModeSync', () => {
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
jest.useFakeTimers()
|
|
110
|
+
document.documentElement.className = ''
|
|
111
|
+
window.localStorage.setItem('theme-ui-color-mode', 'light')
|
|
112
|
+
window.requestAnimationFrame = jest.fn(cb => {
|
|
113
|
+
cb()
|
|
114
|
+
return 1
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
afterEach(() => {
|
|
119
|
+
jest.runOnlyPendingTimers()
|
|
120
|
+
jest.useRealTimers()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('runs sync immediately and again on requestAnimationFrame when available', () => {
|
|
124
|
+
const spy = jest.spyOn(window, 'requestAnimationFrame')
|
|
125
|
+
scheduleThemeUiColorModeSync()
|
|
126
|
+
expect(document.documentElement.classList.contains('theme-ui-default')).toBe(true)
|
|
127
|
+
expect(spy).toHaveBeenCalled()
|
|
128
|
+
spy.mockRestore()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('uses setTimeout when requestAnimationFrame is missing', () => {
|
|
132
|
+
delete window.requestAnimationFrame
|
|
133
|
+
scheduleThemeUiColorModeSync()
|
|
134
|
+
jest.runAllTimers()
|
|
135
|
+
expect(document.documentElement.classList.contains('theme-ui-default')).toBe(true)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Theme UI localStorage key (aligned with theme-ui/color-mode). */
|
|
2
|
+
export const THEME_UI_COLOR_MODE_STORAGE_KEY = 'theme-ui-color-mode'
|
|
3
|
+
|
|
4
|
+
/** Dispatched on route changes so hosts can reconcile React color-mode context with `localStorage`. */
|
|
5
|
+
export const RECONCILE_COLOR_MODE_EVENT = 'theme-ui-reconcile-color-mode'
|
|
6
|
+
|
|
7
|
+
/** `key` props for Gatsby `onPreRenderHTML` head ordering (color-mode first). */
|
|
8
|
+
export const CHRONOGROVE_COLOR_MODE_HEAD_PRIORITY_KEYS = [
|
|
9
|
+
'theme-ui-no-flash',
|
|
10
|
+
'html-bg-color',
|
|
11
|
+
'theme-ui-color-mode-fallback'
|
|
12
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { THEME_UI_COLOR_MODE_STORAGE_KEY } from './constants.js'
|
|
2
|
+
|
|
3
|
+
function q(str) {
|
|
4
|
+
return JSON.stringify(str)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function buildThemeUiNoFlashInlineScript(storageKey = THEME_UI_COLOR_MODE_STORAGE_KEY) {
|
|
8
|
+
const key = q(storageKey)
|
|
9
|
+
return `
|
|
10
|
+
(function() {
|
|
11
|
+
try {
|
|
12
|
+
var mode = localStorage.getItem(${key});
|
|
13
|
+
if (!mode) {
|
|
14
|
+
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
15
|
+
mode = prefersDark ? 'dark' : 'default';
|
|
16
|
+
localStorage.setItem(${key}, mode);
|
|
17
|
+
}
|
|
18
|
+
if (mode === 'light') {
|
|
19
|
+
mode = 'default';
|
|
20
|
+
}
|
|
21
|
+
var htmlElement = document.documentElement;
|
|
22
|
+
var classesToRemove = [];
|
|
23
|
+
for (var i = 0; i < htmlElement.classList.length; i++) {
|
|
24
|
+
var className = htmlElement.classList[i];
|
|
25
|
+
if (className.indexOf('theme-ui-') === 0) {
|
|
26
|
+
classesToRemove.push(className);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (var j = 0; j < classesToRemove.length; j++) {
|
|
30
|
+
htmlElement.classList.remove(classesToRemove[j]);
|
|
31
|
+
}
|
|
32
|
+
htmlElement.classList.add('theme-ui-' + mode);
|
|
33
|
+
htmlElement.setAttribute('data-theme-ui-color-mode', mode);
|
|
34
|
+
} catch (e) {}
|
|
35
|
+
})();
|
|
36
|
+
`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildHtmlBackgroundInlineScript({
|
|
40
|
+
storageKey = THEME_UI_COLOR_MODE_STORAGE_KEY,
|
|
41
|
+
defaultBackgroundHex,
|
|
42
|
+
darkBackgroundHex
|
|
43
|
+
}) {
|
|
44
|
+
const key = q(storageKey)
|
|
45
|
+
const lightBg = q(defaultBackgroundHex)
|
|
46
|
+
const darkBg = q(darkBackgroundHex)
|
|
47
|
+
return `
|
|
48
|
+
(function() {
|
|
49
|
+
try {
|
|
50
|
+
var mode = localStorage.getItem(${key});
|
|
51
|
+
if (!mode) {
|
|
52
|
+
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
53
|
+
mode = prefersDark ? 'dark' : 'default';
|
|
54
|
+
localStorage.setItem(${key}, mode);
|
|
55
|
+
}
|
|
56
|
+
var bgColor = mode === 'dark' ? ${darkBg} : ${lightBg};
|
|
57
|
+
document.documentElement.style.backgroundColor = bgColor;
|
|
58
|
+
} catch (e) {}
|
|
59
|
+
})();
|
|
60
|
+
`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildThemeUiColorModeFallbackCss({
|
|
64
|
+
defaultTextHex,
|
|
65
|
+
defaultTextMutedHex,
|
|
66
|
+
darkTextHex,
|
|
67
|
+
darkTextMutedHex
|
|
68
|
+
}) {
|
|
69
|
+
return `
|
|
70
|
+
:root[data-theme-ui-color-mode="default"], :root[data-theme-ui-color-mode="default"] * { --theme-ui-colors-text: ${defaultTextHex} !important; --theme-ui-colors-text-muted: ${defaultTextMutedHex} !important; }
|
|
71
|
+
:root[data-theme-ui-color-mode="dark"], :root[data-theme-ui-color-mode="dark"] * { --theme-ui-colors-text: ${darkTextHex} !important; --theme-ui-colors-text-muted: ${darkTextMutedHex} !important; }
|
|
72
|
+
`.trim()
|
|
73
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildHtmlBackgroundInlineScript,
|
|
3
|
+
buildThemeUiColorModeFallbackCss,
|
|
4
|
+
buildThemeUiNoFlashInlineScript
|
|
5
|
+
} from './head-inline.js'
|
|
6
|
+
|
|
7
|
+
describe('head-inline scripts', () => {
|
|
8
|
+
it('buildThemeUiNoFlashInlineScript references storage and data attribute', () => {
|
|
9
|
+
const s = buildThemeUiNoFlashInlineScript()
|
|
10
|
+
expect(s).toContain('theme-ui-color-mode')
|
|
11
|
+
expect(s).toContain('data-theme-ui-color-mode')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('buildHtmlBackgroundInlineScript embeds background hexes', () => {
|
|
15
|
+
const s = buildHtmlBackgroundInlineScript({
|
|
16
|
+
defaultBackgroundHex: '#aaa',
|
|
17
|
+
darkBackgroundHex: '#bbb'
|
|
18
|
+
})
|
|
19
|
+
expect(s).toContain('#aaa')
|
|
20
|
+
expect(s).toContain('#bbb')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('buildThemeUiColorModeFallbackCss sets CSS vars', () => {
|
|
24
|
+
const css = buildThemeUiColorModeFallbackCss({
|
|
25
|
+
defaultTextHex: '#111',
|
|
26
|
+
defaultTextMutedHex: '#333',
|
|
27
|
+
darkTextHex: '#fff',
|
|
28
|
+
darkTextMutedHex: '#ccc'
|
|
29
|
+
})
|
|
30
|
+
expect(css).toContain('--theme-ui-colors-text: #111')
|
|
31
|
+
expect(css).toContain('--theme-ui-colors-text: #fff')
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
THEME_UI_COLOR_MODE_STORAGE_KEY,
|
|
3
|
+
RECONCILE_COLOR_MODE_EVENT,
|
|
4
|
+
CHRONOGROVE_COLOR_MODE_HEAD_PRIORITY_KEYS
|
|
5
|
+
} from './constants.js'
|
|
6
|
+
export { normalizeThemeUiColorMode } from './normalize.js'
|
|
7
|
+
export { resolveChronogroveSurfaceColors } from './resolve-theme-colors.js'
|
|
8
|
+
export {
|
|
9
|
+
buildThemeUiNoFlashInlineScript,
|
|
10
|
+
buildHtmlBackgroundInlineScript,
|
|
11
|
+
buildThemeUiColorModeFallbackCss
|
|
12
|
+
} from './head-inline.js'
|
|
13
|
+
export { resolveThemeUiColorMode, syncThemeUiColorMode, scheduleThemeUiColorModeSync } from './browser-sync.js'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { normalizeThemeUiColorMode } from './normalize'
|
|
2
|
+
|
|
3
|
+
describe('normalizeThemeUiColorMode', () => {
|
|
4
|
+
it('maps light to default', () => {
|
|
5
|
+
expect(normalizeThemeUiColorMode('light')).toBe('default')
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
it('passes through dark and default', () => {
|
|
9
|
+
expect(normalizeThemeUiColorMode('dark')).toBe('dark')
|
|
10
|
+
expect(normalizeThemeUiColorMode('default')).toBe('default')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('returns null for unknown values', () => {
|
|
14
|
+
expect(normalizeThemeUiColorMode('sepia')).toBe(null)
|
|
15
|
+
expect(normalizeThemeUiColorMode(null)).toBe(null)
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function pickColor(value) {
|
|
2
|
+
if (typeof value === 'string') {
|
|
3
|
+
return value
|
|
4
|
+
}
|
|
5
|
+
return null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Surface colors used by inline SSR/CSR scripts and fallbacks.
|
|
10
|
+
* Pass the same Theme UI theme object as `ThemeUIProvider`.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveChronogroveSurfaceColors(theme) {
|
|
13
|
+
const colors = theme?.colors || {}
|
|
14
|
+
const dark = colors.modes?.dark || {}
|
|
15
|
+
return {
|
|
16
|
+
defaultBackgroundHex: pickColor(colors.background) || '#fdf8f5',
|
|
17
|
+
darkBackgroundHex: pickColor(dark.background) || '#14141F',
|
|
18
|
+
defaultTextHex: pickColor(colors.text) || '#111',
|
|
19
|
+
defaultTextMutedHex: pickColor(colors.textMuted) || '#333',
|
|
20
|
+
darkTextHex: pickColor(dark.text) || '#fff',
|
|
21
|
+
darkTextMutedHex: pickColor(dark.textMuted) || '#d8d8d8'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { resolveChronogroveSurfaceColors } from './resolve-theme-colors.js'
|
|
2
|
+
|
|
3
|
+
describe('resolveChronogroveSurfaceColors', () => {
|
|
4
|
+
it('reads string colors from Theme UI-shaped theme', () => {
|
|
5
|
+
const theme = {
|
|
6
|
+
colors: {
|
|
7
|
+
background: '#fdf8f5',
|
|
8
|
+
text: '#111',
|
|
9
|
+
textMuted: '#333',
|
|
10
|
+
modes: {
|
|
11
|
+
dark: {
|
|
12
|
+
background: '#14141F',
|
|
13
|
+
text: '#fff',
|
|
14
|
+
textMuted: '#d8d8d8'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
expect(resolveChronogroveSurfaceColors(theme)).toEqual({
|
|
20
|
+
defaultBackgroundHex: '#fdf8f5',
|
|
21
|
+
darkBackgroundHex: '#14141F',
|
|
22
|
+
defaultTextHex: '#111',
|
|
23
|
+
defaultTextMutedHex: '#333',
|
|
24
|
+
darkTextHex: '#fff',
|
|
25
|
+
darkTextMutedHex: '#d8d8d8'
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('uses fallbacks when theme is empty', () => {
|
|
30
|
+
expect(resolveChronogroveSurfaceColors(null)).toEqual({
|
|
31
|
+
defaultBackgroundHex: '#fdf8f5',
|
|
32
|
+
darkBackgroundHex: '#14141F',
|
|
33
|
+
defaultTextHex: '#111',
|
|
34
|
+
defaultTextMutedHex: '#333',
|
|
35
|
+
darkTextHex: '#fff',
|
|
36
|
+
darkTextMutedHex: '#d8d8d8'
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useColorMode } from 'theme-ui'
|
|
3
|
+
import { Expand } from '@theme-toggles/react'
|
|
4
|
+
import isDarkMode from '@chronogrove/ui/is-dark-mode'
|
|
5
|
+
|
|
6
|
+
export default function ColorToggle() {
|
|
7
|
+
const [colorMode, setColorMode] = useColorMode()
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Expand
|
|
11
|
+
className='theme-toggle'
|
|
12
|
+
toggled={isDarkMode(colorMode)}
|
|
13
|
+
toggle={() => setColorMode(colorMode === 'default' ? 'dark' : 'default')}
|
|
14
|
+
duration={750}
|
|
15
|
+
aria-label='Toggle color mode'
|
|
16
|
+
id='theme-toggle'
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
7
|
+
import ColorToggle from './color-toggle'
|
|
8
|
+
|
|
9
|
+
const mockUseColorMode = jest.fn()
|
|
10
|
+
|
|
11
|
+
jest.mock('theme-ui', () => ({
|
|
12
|
+
useColorMode: (...args) => mockUseColorMode(...args)
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
describe('ColorToggle', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockUseColorMode.mockReset()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('toggles from default to dark', () => {
|
|
21
|
+
const setColorMode = jest.fn()
|
|
22
|
+
mockUseColorMode.mockReturnValue(['default', setColorMode])
|
|
23
|
+
render(<ColorToggle />)
|
|
24
|
+
fireEvent.click(screen.getByRole('button', { name: /toggle color mode/i }))
|
|
25
|
+
expect(setColorMode).toHaveBeenCalledWith('dark')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('toggles from dark to default', () => {
|
|
29
|
+
const setColorMode = jest.fn()
|
|
30
|
+
mockUseColorMode.mockReturnValue(['dark', setColorMode])
|
|
31
|
+
render(<ColorToggle />)
|
|
32
|
+
fireEvent.click(screen.getByRole('button', { name: /toggle color mode/i }))
|
|
33
|
+
expect(setColorMode).toHaveBeenCalledWith('default')
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import createCache from '@emotion/cache'
|
|
2
|
+
|
|
3
|
+
const CACHE_KEY = 'css'
|
|
4
|
+
|
|
5
|
+
export function createChronogroveEmotionCache() {
|
|
6
|
+
const insertionPoint =
|
|
7
|
+
typeof document !== 'undefined' ? document.querySelector('meta[name="emotion-insertion-point"]') : undefined
|
|
8
|
+
|
|
9
|
+
return createCache({
|
|
10
|
+
key: CACHE_KEY,
|
|
11
|
+
insertionPoint: insertionPoint || undefined
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let emotionCacheSingleton
|
|
16
|
+
|
|
17
|
+
export function getChronogroveEmotionCache() {
|
|
18
|
+
if (!emotionCacheSingleton) {
|
|
19
|
+
emotionCacheSingleton = createChronogroveEmotionCache()
|
|
20
|
+
}
|
|
21
|
+
return emotionCacheSingleton
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getChronogroveEmotionCache } from './emotion-cache'
|
|
6
|
+
|
|
7
|
+
describe('getChronogroveEmotionCache', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
document.querySelectorAll('style[data-emotion]').forEach(el => el.remove())
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('creates a cache with emotion key css', () => {
|
|
13
|
+
const cache1 = getChronogroveEmotionCache()
|
|
14
|
+
const cache2 = getChronogroveEmotionCache()
|
|
15
|
+
expect(cache1).toBe(cache2)
|
|
16
|
+
expect(cache1.key).toBe('css')
|
|
17
|
+
expect(typeof cache1.insert).toBe('function')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('uses emotion insertion point meta when present', () => {
|
|
21
|
+
const meta = document.createElement('meta')
|
|
22
|
+
meta.setAttribute('name', 'emotion-insertion-point')
|
|
23
|
+
document.head.appendChild(meta)
|
|
24
|
+
jest.isolateModules(() => {
|
|
25
|
+
const { getChronogroveEmotionCache: getFresh } = require('./emotion-cache')
|
|
26
|
+
expect(getFresh()).toBeTruthy()
|
|
27
|
+
})
|
|
28
|
+
document.head.removeChild(meta)
|
|
29
|
+
})
|
|
30
|
+
})
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ChronogroveThemeProvider } from './provider.js'
|
package/src/provider.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Global } from '@emotion/react'
|
|
3
|
+
import { ThemeUIProvider, InitializeColorMode } from 'theme-ui'
|
|
4
|
+
|
|
5
|
+
export function ChronogroveThemeProvider({ theme, children }) {
|
|
6
|
+
return (
|
|
7
|
+
<ThemeUIProvider theme={theme}>
|
|
8
|
+
<InitializeColorMode />
|
|
9
|
+
<Global styles={theme.global} />
|
|
10
|
+
{children}
|
|
11
|
+
</ThemeUIProvider>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import { render, screen } from '@testing-library/react'
|
|
7
|
+
import { ChronogroveThemeProvider } from './provider'
|
|
8
|
+
|
|
9
|
+
const baseTheme = {
|
|
10
|
+
config: {},
|
|
11
|
+
fonts: { body: 'system-ui' },
|
|
12
|
+
colors: { text: '#111', background: '#fff' },
|
|
13
|
+
global: {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('ChronogroveThemeProvider', () => {
|
|
17
|
+
it('renders children', () => {
|
|
18
|
+
render(
|
|
19
|
+
<ChronogroveThemeProvider theme={baseTheme}>
|
|
20
|
+
<span>hello</span>
|
|
21
|
+
</ChronogroveThemeProvider>
|
|
22
|
+
)
|
|
23
|
+
expect(screen.getByText('hello')).toBeInTheDocument()
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react'
|
|
2
|
+
|
|
3
|
+
const SkipNavContent = forwardRef(function SkipNavContent(
|
|
4
|
+
{ as: Comp = 'div', id = 'skip-nav-content', children, ...props },
|
|
5
|
+
forwardedRef
|
|
6
|
+
) {
|
|
7
|
+
return (
|
|
8
|
+
<Comp {...props} ref={forwardedRef} id={id} data-skip-nav-content='' tabIndex={-1} style={{ outline: 'none' }}>
|
|
9
|
+
{children}
|
|
10
|
+
</Comp>
|
|
11
|
+
)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
SkipNavContent.displayName = 'SkipNavContent'
|
|
15
|
+
|
|
16
|
+
export default SkipNavContent
|