@chronogrove/ui 0.78.0 → 0.80.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 +42 -19
- package/package.json +42 -6
- package/src/__snapshots__/header.spec.js.snap +8 -8
- package/src/__snapshots__/theme.spec.js.snap +14 -15
- package/src/action-button.js +6 -6
- package/src/action-button.spec.js +14 -2
- package/src/action-card-layout.js +13 -0
- package/src/action-card-layout.spec.js +13 -0
- package/src/animated-page-background/ChronogroveAnimatedPageBackground.js +153 -0
- package/src/animated-page-background/ChronogroveAnimatedPageBackground.spec.js +189 -0
- package/src/animated-page-background/ColorBends.js +309 -0
- package/src/animated-page-background/color-bends.css +13 -0
- package/src/animated-page-background/index.js +2 -0
- package/src/animated-page-background/index.spec.js +18 -0
- package/src/button.js +4 -3
- package/src/chronogrove-theme-surface-colors.js +22 -0
- package/src/color-mode/browser-sync.js +7 -0
- package/src/color-mode/browser-sync.spec.js +7 -0
- package/src/color-mode/chronogrove-head-theme.js +22 -0
- package/src/color-mode/head-inline.js +40 -5
- package/src/color-mode/head-inline.spec.js +29 -0
- package/src/color-mode/index.js +3 -0
- package/src/color-mode/resolve-theme-colors.js +18 -6
- package/src/color-mode/resolve-theme-colors.spec.js +13 -3
- package/src/color-mode/spa-navigation.js +14 -0
- package/src/color-mode/spa-navigation.spec.js +25 -0
- package/src/color-mode/use-document-color-mode-surface.js +52 -0
- package/src/color-mode/use-document-color-mode-surface.node.spec.js +12 -0
- package/src/color-mode/use-document-color-mode-surface.spec.js +154 -0
- package/src/color-toggle-styles.css +10 -0
- package/src/color-toggle.js +12 -3
- package/src/emotion-cache.node.spec.js +13 -0
- package/src/emotion-cache.spec.js +12 -0
- package/src/gatsby/build-theme-ui-color-mode-head-components.js +7 -1
- package/src/gatsby/index.spec.js +42 -0
- package/src/gatsby/on-route-update-color-mode.js +1 -14
- package/src/header.js +4 -16
- package/src/lazy-load.js +30 -11
- package/src/lazy-load.spec.js +9 -5
- package/src/next/app-shell.js +34 -0
- package/src/next/emotion-registry.js +68 -0
- package/src/next/emotion-registry.spec.js +99 -0
- package/src/next/index.js +4 -0
- package/src/next/root-layout-head.js +42 -0
- package/src/next/root-layout-head.spec.js +17 -0
- package/src/next/theme-ui-color-mode-route-sync.js +32 -0
- package/src/page-backdrop.js +42 -0
- package/src/page-backdrop.spec.js +41 -0
- package/src/pagination-button.js +4 -4
- package/src/pagination-button.spec.js +26 -2
- package/src/skip-nav/SkipNavLink.js +7 -6
- package/src/skip-nav/SkipNavLink.spec.js +11 -0
- package/src/theme.js +12 -15
- package/babel.config.cjs +0 -9
- package/jest.config.cjs +0 -33
- package/jest.setup.cjs +0 -1
- package/test-utils/mock-theme-toggles-react.js +0 -10
- package/turbo.json +0 -12
|
@@ -22,12 +22,41 @@ describe('head-inline scripts', () => {
|
|
|
22
22
|
|
|
23
23
|
it('buildThemeUiColorModeFallbackCss sets CSS vars', () => {
|
|
24
24
|
const css = buildThemeUiColorModeFallbackCss({
|
|
25
|
+
defaultBackgroundHex: '#fdf8f5',
|
|
26
|
+
darkBackgroundHex: '#14141F',
|
|
25
27
|
defaultTextHex: '#111',
|
|
26
28
|
defaultTextMutedHex: '#333',
|
|
27
29
|
darkTextHex: '#fff',
|
|
28
30
|
darkTextMutedHex: '#ccc'
|
|
29
31
|
})
|
|
32
|
+
expect(css).toContain(':root {')
|
|
33
|
+
expect(css).toContain('--theme-ui-colors-background: #fdf8f5')
|
|
30
34
|
expect(css).toContain('--theme-ui-colors-text: #111')
|
|
31
35
|
expect(css).toContain('--theme-ui-colors-text: #fff')
|
|
36
|
+
expect(css).toContain('--theme-ui-colors-panel-background:')
|
|
37
|
+
expect(css).toContain('--theme-ui-colors-panel-text:')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('buildThemeUiColorModeFallbackCss uses default background hexes when omitted', () => {
|
|
41
|
+
const css = buildThemeUiColorModeFallbackCss({
|
|
42
|
+
defaultTextHex: '#111',
|
|
43
|
+
defaultTextMutedHex: '#333',
|
|
44
|
+
darkTextHex: '#fff',
|
|
45
|
+
darkTextMutedHex: '#ccc'
|
|
46
|
+
})
|
|
47
|
+
expect(css).toContain('--theme-ui-colors-background: #fdf8f5')
|
|
48
|
+
expect(css).toContain('--theme-ui-colors-background: #14141F')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('buildThemeUiColorModeFallbackCss uses chronogrove-theme-surface-colors when called with no options', () => {
|
|
52
|
+
const css = buildThemeUiColorModeFallbackCss()
|
|
53
|
+
expect(css).toContain('--theme-ui-colors-background: #fdf8f5')
|
|
54
|
+
expect(css).toContain('--theme-ui-colors-background: #14141F')
|
|
55
|
+
expect(css).toContain('rgba(255, 255, 255, 0.45)')
|
|
56
|
+
expect(css).toContain('rgba(20, 20, 31, 0.45)')
|
|
57
|
+
expect(css).toMatch(/--theme-ui-colors-text: #111/)
|
|
58
|
+
expect(css).toMatch(/--theme-ui-colors-text-muted: #333/)
|
|
59
|
+
expect(css).toMatch(/--theme-ui-colors-text: #fff/)
|
|
60
|
+
expect(css).toMatch(/--theme-ui-colors-text-muted: #d8d8d8/)
|
|
32
61
|
})
|
|
33
62
|
})
|
package/src/color-mode/index.js
CHANGED
|
@@ -5,9 +5,12 @@ export {
|
|
|
5
5
|
} from './constants.js'
|
|
6
6
|
export { normalizeThemeUiColorMode } from './normalize.js'
|
|
7
7
|
export { resolveChronogroveSurfaceColors } from './resolve-theme-colors.js'
|
|
8
|
+
export { chronogroveHeadTheme } from './chronogrove-head-theme.js'
|
|
8
9
|
export {
|
|
9
10
|
buildThemeUiNoFlashInlineScript,
|
|
10
11
|
buildHtmlBackgroundInlineScript,
|
|
11
12
|
buildThemeUiColorModeFallbackCss
|
|
12
13
|
} from './head-inline.js'
|
|
13
14
|
export { resolveThemeUiColorMode, syncThemeUiColorMode, scheduleThemeUiColorModeSync } from './browser-sync.js'
|
|
15
|
+
export { reconcileThemeUiColorModeOnNavigation } from './spa-navigation.js'
|
|
16
|
+
export { useDocumentColorModeSurface } from './use-document-color-mode-surface.js'
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chronogroveThemeSurfaceColorsDark,
|
|
3
|
+
chronogroveThemeSurfaceColorsLight
|
|
4
|
+
} from '../chronogrove-theme-surface-colors.js'
|
|
5
|
+
|
|
1
6
|
function pickColor(value) {
|
|
2
7
|
if (typeof value === 'string') {
|
|
3
8
|
return value
|
|
@@ -12,12 +17,19 @@ function pickColor(value) {
|
|
|
12
17
|
export function resolveChronogroveSurfaceColors(theme) {
|
|
13
18
|
const colors = theme?.colors || {}
|
|
14
19
|
const dark = colors.modes?.dark || {}
|
|
20
|
+
const light = chronogroveThemeSurfaceColorsLight
|
|
21
|
+
const darkSurface = chronogroveThemeSurfaceColorsDark
|
|
15
22
|
return {
|
|
16
|
-
defaultBackgroundHex: pickColor(colors.background) ||
|
|
17
|
-
darkBackgroundHex: pickColor(dark.background) ||
|
|
18
|
-
defaultTextHex: pickColor(colors.text) ||
|
|
19
|
-
defaultTextMutedHex: pickColor(colors.textMuted) ||
|
|
20
|
-
darkTextHex: pickColor(dark.text) ||
|
|
21
|
-
darkTextMutedHex: pickColor(dark.textMuted) ||
|
|
23
|
+
defaultBackgroundHex: pickColor(colors.background) || light.background,
|
|
24
|
+
darkBackgroundHex: pickColor(dark.background) || darkSurface.background,
|
|
25
|
+
defaultTextHex: pickColor(colors.text) || light.text,
|
|
26
|
+
defaultTextMutedHex: pickColor(colors.textMuted) || light.textMuted,
|
|
27
|
+
darkTextHex: pickColor(dark.text) || darkSurface.text,
|
|
28
|
+
darkTextMutedHex: pickColor(dark.textMuted) || darkSurface.textMuted,
|
|
29
|
+
defaultPanelBackground: pickColor(colors['panel-background']) || light['panel-background'],
|
|
30
|
+
darkPanelBackground: pickColor(dark['panel-background']) || darkSurface['panel-background'],
|
|
31
|
+
/** Head fallback only; panel copy usually uses `color: 'text'` — no separate `panel-text` token required. */
|
|
32
|
+
defaultPanelText: pickColor(colors['panel-text']) || pickColor(colors.text) || light.text,
|
|
33
|
+
darkPanelText: pickColor(dark['panel-text']) || pickColor(dark.text) || darkSurface.text
|
|
22
34
|
}
|
|
23
35
|
}
|
|
@@ -7,11 +7,13 @@ describe('resolveChronogroveSurfaceColors', () => {
|
|
|
7
7
|
background: '#fdf8f5',
|
|
8
8
|
text: '#111',
|
|
9
9
|
textMuted: '#333',
|
|
10
|
+
'panel-background': 'rgba(255, 255, 255, 0.45)',
|
|
10
11
|
modes: {
|
|
11
12
|
dark: {
|
|
12
13
|
background: '#14141F',
|
|
13
14
|
text: '#fff',
|
|
14
|
-
textMuted: '#d8d8d8'
|
|
15
|
+
textMuted: '#d8d8d8',
|
|
16
|
+
'panel-background': 'rgba(20, 20, 31, 0.45)'
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
}
|
|
@@ -22,7 +24,11 @@ describe('resolveChronogroveSurfaceColors', () => {
|
|
|
22
24
|
defaultTextHex: '#111',
|
|
23
25
|
defaultTextMutedHex: '#333',
|
|
24
26
|
darkTextHex: '#fff',
|
|
25
|
-
darkTextMutedHex: '#d8d8d8'
|
|
27
|
+
darkTextMutedHex: '#d8d8d8',
|
|
28
|
+
defaultPanelBackground: 'rgba(255, 255, 255, 0.45)',
|
|
29
|
+
darkPanelBackground: 'rgba(20, 20, 31, 0.45)',
|
|
30
|
+
defaultPanelText: '#111',
|
|
31
|
+
darkPanelText: '#fff'
|
|
26
32
|
})
|
|
27
33
|
})
|
|
28
34
|
|
|
@@ -33,7 +39,11 @@ describe('resolveChronogroveSurfaceColors', () => {
|
|
|
33
39
|
defaultTextHex: '#111',
|
|
34
40
|
defaultTextMutedHex: '#333',
|
|
35
41
|
darkTextHex: '#fff',
|
|
36
|
-
darkTextMutedHex: '#d8d8d8'
|
|
42
|
+
darkTextMutedHex: '#d8d8d8',
|
|
43
|
+
defaultPanelBackground: 'rgba(255, 255, 255, 0.45)',
|
|
44
|
+
darkPanelBackground: 'rgba(20, 20, 31, 0.45)',
|
|
45
|
+
defaultPanelText: '#111',
|
|
46
|
+
darkPanelText: '#fff'
|
|
37
47
|
})
|
|
38
48
|
})
|
|
39
49
|
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { RECONCILE_COLOR_MODE_EVENT } from './constants.js'
|
|
2
|
+
import { scheduleThemeUiColorModeSync } from './browser-sync.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run after client-side navigations (e.g. Next.js App Router) so Theme UI color mode stays aligned
|
|
6
|
+
* with `localStorage` and the document. Dispatches {@link RECONCILE_COLOR_MODE_EVENT} for listeners.
|
|
7
|
+
* Same behavior as {@link onRouteUpdateThemeUiColorMode} in `@chronogrove/ui/gatsby`.
|
|
8
|
+
*/
|
|
9
|
+
export function reconcileThemeUiColorModeOnNavigation() {
|
|
10
|
+
scheduleThemeUiColorModeSync()
|
|
11
|
+
if (typeof window !== 'undefined' && typeof window.CustomEvent === 'function') {
|
|
12
|
+
window.dispatchEvent(new window.CustomEvent(RECONCILE_COLOR_MODE_EVENT))
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { reconcileThemeUiColorModeOnNavigation } from './spa-navigation.js'
|
|
6
|
+
import { RECONCILE_COLOR_MODE_EVENT } from './constants.js'
|
|
7
|
+
|
|
8
|
+
describe('reconcileThemeUiColorModeOnNavigation', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
window.localStorage.removeItem('theme-ui-color-mode')
|
|
11
|
+
document.documentElement.removeAttribute('data-theme-ui-color-mode')
|
|
12
|
+
document.documentElement.className = ''
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('syncs DOM from localStorage and dispatches reconcile event', () => {
|
|
16
|
+
window.localStorage.setItem('theme-ui-color-mode', 'dark')
|
|
17
|
+
const listener = jest.fn()
|
|
18
|
+
window.addEventListener(RECONCILE_COLOR_MODE_EVENT, listener)
|
|
19
|
+
|
|
20
|
+
reconcileThemeUiColorModeOnNavigation()
|
|
21
|
+
|
|
22
|
+
expect(document.documentElement.getAttribute('data-theme-ui-color-mode')).toBe('dark')
|
|
23
|
+
expect(listener).toHaveBeenCalled()
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useLayoutEffect } from 'react'
|
|
4
|
+
import { useColorMode, useThemeUI } from 'theme-ui'
|
|
5
|
+
|
|
6
|
+
import { resolveChronogroveSurfaceColors } from './resolve-theme-colors.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Applies Theme UI color mode + page background to `document.documentElement`. Exposed for tests;
|
|
10
|
+
* prefer {@link useDocumentColorModeSurface} in app code.
|
|
11
|
+
*/
|
|
12
|
+
export function applyDocumentColorModeSurface(colorMode, theme, surface) {
|
|
13
|
+
if (typeof document === 'undefined') {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
const normalizedColorMode = colorMode === 'light' ? 'default' : colorMode || 'default'
|
|
17
|
+
const isDark = normalizedColorMode === 'dark'
|
|
18
|
+
const bgColorRaw =
|
|
19
|
+
theme?.rawColors?.background ||
|
|
20
|
+
theme?.colors?.background ||
|
|
21
|
+
(isDark ? surface.darkBackgroundHex : surface.defaultBackgroundHex)
|
|
22
|
+
const htmlElement = document.documentElement
|
|
23
|
+
Array.from(htmlElement.classList)
|
|
24
|
+
.filter(className => className.startsWith('theme-ui-'))
|
|
25
|
+
.forEach(className => htmlElement.classList.remove(className))
|
|
26
|
+
htmlElement.classList.add(`theme-ui-${normalizedColorMode}`)
|
|
27
|
+
htmlElement.setAttribute('data-theme-ui-color-mode', normalizedColorMode)
|
|
28
|
+
htmlElement.style.backgroundColor = bgColorRaw
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Keeps `document.documentElement` aligned with Theme UI color mode — same responsibility as
|
|
33
|
+
* `RootWrapper`’s layout effect in the Gatsby theme (`theme/src/components/root-wrapper.js`):
|
|
34
|
+
* `theme-ui-*` class, `data-theme-ui-color-mode`, and inline page background from the **resolved**
|
|
35
|
+
* theme (including `rawColors` / `colors.background` for the active mode).
|
|
36
|
+
*
|
|
37
|
+
* Without this on App Router, head fallback CSS and inline scripts can disagree with React
|
|
38
|
+
* hydration, and Emotion-injected `--theme-ui-colors-*` can win the cascade so panel surfaces
|
|
39
|
+
* (`bg: 'panel-background'`) never pick up dark tokens.
|
|
40
|
+
*
|
|
41
|
+
* Call once inside `ChronogroveThemeProvider` (client boundary). Optional on Gatsby if you
|
|
42
|
+
* already use the same hook from `RootWrapper`.
|
|
43
|
+
*/
|
|
44
|
+
export function useDocumentColorModeSurface() {
|
|
45
|
+
const [colorMode] = useColorMode()
|
|
46
|
+
const { theme } = useThemeUI()
|
|
47
|
+
const surface = resolveChronogroveSurfaceColors(theme)
|
|
48
|
+
|
|
49
|
+
useLayoutEffect(() => {
|
|
50
|
+
applyDocumentColorModeSurface(colorMode, theme, surface)
|
|
51
|
+
}, [colorMode, theme, surface.darkBackgroundHex, surface.defaultBackgroundHex])
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolveChronogroveSurfaceColors } from './resolve-theme-colors.js'
|
|
6
|
+
import { applyDocumentColorModeSurface } from './use-document-color-mode-surface.js'
|
|
7
|
+
|
|
8
|
+
describe('applyDocumentColorModeSurface (node / no DOM)', () => {
|
|
9
|
+
it('returns immediately when document is not available', () => {
|
|
10
|
+
expect(applyDocumentColorModeSurface('default', {}, resolveChronogroveSurfaceColors({}))).toBeUndefined()
|
|
11
|
+
})
|
|
12
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import { render, act } from '@testing-library/react'
|
|
7
|
+
import { ThemeUIProvider } from 'theme-ui'
|
|
8
|
+
|
|
9
|
+
import { resolveChronogroveSurfaceColors } from './resolve-theme-colors.js'
|
|
10
|
+
import { applyDocumentColorModeSurface, useDocumentColorModeSurface } from './use-document-color-mode-surface.js'
|
|
11
|
+
|
|
12
|
+
function SurfaceHarness() {
|
|
13
|
+
useDocumentColorModeSurface()
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const baseTheme = {
|
|
18
|
+
config: { useLocalStorage: true, useColorSchemeMediaQuery: false },
|
|
19
|
+
colors: {
|
|
20
|
+
background: '#fdf8f5',
|
|
21
|
+
modes: {
|
|
22
|
+
dark: { background: '#14141F' }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('useDocumentColorModeSurface', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
document.documentElement.className = 'theme-ui-stale'
|
|
30
|
+
document.documentElement.removeAttribute('data-theme-ui-color-mode')
|
|
31
|
+
document.documentElement.style.backgroundColor = ''
|
|
32
|
+
window.localStorage.removeItem('theme-ui-color-mode')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('syncs documentElement class, data attribute, and background from resolved theme', () => {
|
|
36
|
+
window.localStorage.setItem('theme-ui-color-mode', 'default')
|
|
37
|
+
|
|
38
|
+
act(() => {
|
|
39
|
+
render(
|
|
40
|
+
<ThemeUIProvider theme={baseTheme}>
|
|
41
|
+
<SurfaceHarness />
|
|
42
|
+
</ThemeUIProvider>
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
expect(document.documentElement.classList.contains('theme-ui-stale')).toBe(false)
|
|
47
|
+
expect(document.documentElement.classList.contains('theme-ui-default')).toBe(true)
|
|
48
|
+
expect(document.documentElement.getAttribute('data-theme-ui-color-mode')).toBe('default')
|
|
49
|
+
expect(document.documentElement.style.backgroundColor).toBeTruthy()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('maps light color mode to theme-ui-default', () => {
|
|
53
|
+
window.localStorage.setItem('theme-ui-color-mode', 'light')
|
|
54
|
+
|
|
55
|
+
act(() => {
|
|
56
|
+
render(
|
|
57
|
+
<ThemeUIProvider theme={baseTheme}>
|
|
58
|
+
<SurfaceHarness />
|
|
59
|
+
</ThemeUIProvider>
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
expect(document.documentElement.classList.contains('theme-ui-default')).toBe(true)
|
|
64
|
+
expect(document.documentElement.getAttribute('data-theme-ui-color-mode')).toBe('default')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('applies dark surface when color mode is dark', () => {
|
|
68
|
+
window.localStorage.setItem('theme-ui-color-mode', 'dark')
|
|
69
|
+
|
|
70
|
+
act(() => {
|
|
71
|
+
render(
|
|
72
|
+
<ThemeUIProvider theme={baseTheme}>
|
|
73
|
+
<SurfaceHarness />
|
|
74
|
+
</ThemeUIProvider>
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(document.documentElement.classList.contains('theme-ui-dark')).toBe(true)
|
|
79
|
+
expect(document.documentElement.getAttribute('data-theme-ui-color-mode')).toBe('dark')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('prefers rawColors.background when set', () => {
|
|
83
|
+
window.localStorage.setItem('theme-ui-color-mode', 'default')
|
|
84
|
+
const theme = {
|
|
85
|
+
config: { useLocalStorage: true, useColorSchemeMediaQuery: false },
|
|
86
|
+
rawColors: { background: '#abcdef' },
|
|
87
|
+
colors: {
|
|
88
|
+
modes: {
|
|
89
|
+
dark: { background: '#14141F' }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
act(() => {
|
|
95
|
+
render(
|
|
96
|
+
<ThemeUIProvider theme={theme}>
|
|
97
|
+
<SurfaceHarness />
|
|
98
|
+
</ThemeUIProvider>
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
expect(document.documentElement.style.backgroundColor).toBe('rgb(171, 205, 239)')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('normalizes light and missing color modes when applying to the document', () => {
|
|
106
|
+
window.localStorage.setItem('theme-ui-color-mode', 'default')
|
|
107
|
+
document.documentElement.className = ''
|
|
108
|
+
act(() => {
|
|
109
|
+
applyDocumentColorModeSurface('light', baseTheme, resolveChronogroveSurfaceColors(baseTheme))
|
|
110
|
+
})
|
|
111
|
+
expect(document.documentElement.getAttribute('data-theme-ui-color-mode')).toBe('default')
|
|
112
|
+
|
|
113
|
+
document.documentElement.removeAttribute('data-theme-ui-color-mode')
|
|
114
|
+
act(() => {
|
|
115
|
+
applyDocumentColorModeSurface(undefined, baseTheme, resolveChronogroveSurfaceColors(baseTheme))
|
|
116
|
+
})
|
|
117
|
+
expect(document.documentElement.getAttribute('data-theme-ui-color-mode')).toBe('default')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('prefers rawColors.background over colors.background', () => {
|
|
121
|
+
act(() => {
|
|
122
|
+
applyDocumentColorModeSurface(
|
|
123
|
+
'default',
|
|
124
|
+
{ rawColors: { background: '#112233' }, colors: { background: '#eeeeee' } },
|
|
125
|
+
resolveChronogroveSurfaceColors({})
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
expect(document.documentElement.style.backgroundColor).toBe('rgb(17, 34, 51)')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('uses colors.background when raw background is absent', () => {
|
|
132
|
+
act(() => {
|
|
133
|
+
applyDocumentColorModeSurface(
|
|
134
|
+
'default',
|
|
135
|
+
{ colors: { background: '#00ff00' } },
|
|
136
|
+
resolveChronogroveSurfaceColors({})
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
expect(document.documentElement.style.backgroundColor).toBe('rgb(0, 255, 0)')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('uses surface fallbacks when theme omits background colors', () => {
|
|
143
|
+
const surface = resolveChronogroveSurfaceColors(null)
|
|
144
|
+
act(() => {
|
|
145
|
+
applyDocumentColorModeSurface('default', null, surface)
|
|
146
|
+
})
|
|
147
|
+
expect(document.documentElement.style.backgroundColor).toBe('rgb(253, 248, 245)')
|
|
148
|
+
|
|
149
|
+
act(() => {
|
|
150
|
+
applyDocumentColorModeSurface('dark', null, surface)
|
|
151
|
+
})
|
|
152
|
+
expect(document.documentElement.style.backgroundColor).toBe('rgb(20, 20, 31)')
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* Styles for `@chronogrove/ui/color-toggle` (`@theme-toggles/react` Expand). Import once in global CSS, e.g. `@import '@chronogrove/ui/color-toggle-styles';` */
|
|
2
|
+
@import '@theme-toggles/react/css/Expand.css';
|
|
3
|
+
|
|
4
|
+
.theme-toggle,
|
|
5
|
+
.theme-toggle svg {
|
|
6
|
+
color: inherit;
|
|
7
|
+
fill: currentColor;
|
|
8
|
+
width: 16px;
|
|
9
|
+
height: 16px;
|
|
10
|
+
}
|
package/src/color-toggle.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useEffect } from 'react'
|
|
2
2
|
import { useColorMode } from 'theme-ui'
|
|
3
3
|
import { Expand } from '@theme-toggles/react'
|
|
4
|
-
import
|
|
4
|
+
import { scheduleThemeUiColorModeSync } from './color-mode/browser-sync.js'
|
|
5
|
+
import isDarkMode from './helpers/isDarkMode.js'
|
|
5
6
|
|
|
6
7
|
export default function ColorToggle() {
|
|
7
8
|
const [colorMode, setColorMode] = useColorMode()
|
|
8
9
|
|
|
10
|
+
// Theme UI updates Emotion + localStorage on toggle but does not sync `document.documentElement`
|
|
11
|
+
// (`theme-ui-*` classes, `data-theme-ui-color-mode`). Chronogrove's head fallback CSS targets
|
|
12
|
+
// `:root[data-theme-ui-color-mode="..."]` with `!important` (see head-inline.js), so a stale
|
|
13
|
+
// attribute makes toggles appear to do nothing. Run after ancestor effects so localStorage matches.
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
scheduleThemeUiColorModeSync()
|
|
16
|
+
}, [colorMode])
|
|
17
|
+
|
|
9
18
|
return (
|
|
10
19
|
<Expand
|
|
11
20
|
className='theme-toggle'
|
|
12
21
|
toggled={isDarkMode(colorMode)}
|
|
13
|
-
toggle={
|
|
22
|
+
toggle={preferDark => setColorMode(preferDark ? 'dark' : 'default')}
|
|
14
23
|
duration={750}
|
|
15
24
|
aria-label='Toggle color mode'
|
|
16
25
|
id='theme-toggle'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
describe('createChronogroveEmotionCache (node / no DOM)', () => {
|
|
6
|
+
it('creates a cache without a document insertion point', () => {
|
|
7
|
+
jest.isolateModules(() => {
|
|
8
|
+
const { createChronogroveEmotionCache } = require('./emotion-cache.js')
|
|
9
|
+
const cache = createChronogroveEmotionCache()
|
|
10
|
+
expect(cache.key).toBe('css')
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -27,4 +27,16 @@ describe('getChronogroveEmotionCache', () => {
|
|
|
27
27
|
})
|
|
28
28
|
document.head.removeChild(meta)
|
|
29
29
|
})
|
|
30
|
+
|
|
31
|
+
it('passes a real insertion point meta when present', () => {
|
|
32
|
+
const meta = document.createElement('meta')
|
|
33
|
+
meta.setAttribute('name', 'emotion-insertion-point')
|
|
34
|
+
document.head.appendChild(meta)
|
|
35
|
+
jest.isolateModules(() => {
|
|
36
|
+
const { createChronogroveEmotionCache } = require('./emotion-cache')
|
|
37
|
+
const cache = createChronogroveEmotionCache()
|
|
38
|
+
expect(cache).toBeTruthy()
|
|
39
|
+
})
|
|
40
|
+
document.head.removeChild(meta)
|
|
41
|
+
})
|
|
30
42
|
})
|
|
@@ -22,10 +22,16 @@ export function buildThemeUiColorModeHeadComponents({ theme }) {
|
|
|
22
22
|
darkBackgroundHex: surface.darkBackgroundHex
|
|
23
23
|
})
|
|
24
24
|
const colorModeFallbackCSS = buildThemeUiColorModeFallbackCss({
|
|
25
|
+
defaultBackgroundHex: surface.defaultBackgroundHex,
|
|
26
|
+
darkBackgroundHex: surface.darkBackgroundHex,
|
|
25
27
|
defaultTextHex: surface.defaultTextHex,
|
|
26
28
|
defaultTextMutedHex: surface.defaultTextMutedHex,
|
|
27
29
|
darkTextHex: surface.darkTextHex,
|
|
28
|
-
darkTextMutedHex: surface.darkTextMutedHex
|
|
30
|
+
darkTextMutedHex: surface.darkTextMutedHex,
|
|
31
|
+
defaultPanelBackground: surface.defaultPanelBackground,
|
|
32
|
+
darkPanelBackground: surface.darkPanelBackground,
|
|
33
|
+
defaultPanelText: surface.defaultPanelText,
|
|
34
|
+
darkPanelText: surface.darkPanelText
|
|
29
35
|
})
|
|
30
36
|
|
|
31
37
|
return [
|
package/src/gatsby/index.spec.js
CHANGED
|
@@ -40,6 +40,8 @@ describe('@chronogrove/ui/gatsby', () => {
|
|
|
40
40
|
const fallbackStyle = fallbackStyleContainer.querySelector('style')
|
|
41
41
|
expect(fallbackStyle).toHaveTextContent(/:root\[data-theme-ui-color-mode="default"\]/)
|
|
42
42
|
expect(fallbackStyle).toHaveTextContent(/--theme-ui-colors-text: #111 !important/)
|
|
43
|
+
expect(fallbackStyle).toHaveTextContent(/--theme-ui-colors-panel-background:/)
|
|
44
|
+
expect(fallbackStyle).toHaveTextContent(/--theme-ui-colors-panel-text:/)
|
|
43
45
|
})
|
|
44
46
|
})
|
|
45
47
|
|
|
@@ -100,6 +102,46 @@ describe('@chronogrove/ui/gatsby', () => {
|
|
|
100
102
|
onPreRenderHTMLSortThemeUiColorModeFirst({ getHeadComponents, replaceHeadComponents })
|
|
101
103
|
expect(replaceHeadComponents.mock.calls[0][0].map(c => c.key)).toEqual(['html-bg-color', 'theme-ui-no-flash'])
|
|
102
104
|
})
|
|
105
|
+
|
|
106
|
+
it('treats missing component keys as empty string', () => {
|
|
107
|
+
const getHeadComponents = jest.fn(() => [{ type: 'meta' }, { key: 'theme-ui-no-flash', type: 'script' }])
|
|
108
|
+
const replaceHeadComponents = jest.fn()
|
|
109
|
+
onPreRenderHTMLSortThemeUiColorModeFirst({ getHeadComponents, replaceHeadComponents })
|
|
110
|
+
const sorted = replaceHeadComponents.mock.calls[0][0]
|
|
111
|
+
expect(sorted[0].key).toBe('theme-ui-no-flash')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('handles null or undefined head entries when sorting', () => {
|
|
115
|
+
const getHeadComponents = jest.fn(() => [
|
|
116
|
+
null,
|
|
117
|
+
undefined,
|
|
118
|
+
{ key: 'theme-ui-no-flash', type: 'script' },
|
|
119
|
+
{ key: 'other', type: 'meta' }
|
|
120
|
+
])
|
|
121
|
+
const replaceHeadComponents = jest.fn()
|
|
122
|
+
onPreRenderHTMLSortThemeUiColorModeFirst({ getHeadComponents, replaceHeadComponents })
|
|
123
|
+
const sorted = replaceHeadComponents.mock.calls[0][0]
|
|
124
|
+
expect(sorted[0].key).toBe('theme-ui-no-flash')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('treats explicit null keys like missing keys', () => {
|
|
128
|
+
const getHeadComponents = jest.fn(() => [
|
|
129
|
+
{ key: null, type: 'meta' },
|
|
130
|
+
{ key: 'theme-ui-no-flash', type: 'script' }
|
|
131
|
+
])
|
|
132
|
+
const replaceHeadComponents = jest.fn()
|
|
133
|
+
onPreRenderHTMLSortThemeUiColorModeFirst({ getHeadComponents, replaceHeadComponents })
|
|
134
|
+
const sorted = replaceHeadComponents.mock.calls[0][0]
|
|
135
|
+
expect(sorted[0].key).toBe('theme-ui-no-flash')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('sorts when the right-hand entry is null', () => {
|
|
139
|
+
const getHeadComponents = jest.fn(() => [{ key: 'theme-ui-no-flash', type: 'script' }, null])
|
|
140
|
+
const replaceHeadComponents = jest.fn()
|
|
141
|
+
onPreRenderHTMLSortThemeUiColorModeFirst({ getHeadComponents, replaceHeadComponents })
|
|
142
|
+
const sorted = replaceHeadComponents.mock.calls[0][0]
|
|
143
|
+
expect(sorted[0].key).toBe('theme-ui-no-flash')
|
|
144
|
+
})
|
|
103
145
|
})
|
|
104
146
|
|
|
105
147
|
describe('onRouteUpdateThemeUiColorMode', () => {
|
|
@@ -1,14 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { scheduleThemeUiColorModeSync } from '../color-mode/browser-sync.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Call from Gatsby `onRouteUpdate` so Theme UI color mode stays aligned with `localStorage` and the
|
|
6
|
-
* document after client-side navigations. Dispatches {@link RECONCILE_COLOR_MODE_EVENT} for app code
|
|
7
|
-
* that listens (e.g. React context reconciliation).
|
|
8
|
-
*/
|
|
9
|
-
export function onRouteUpdateThemeUiColorMode() {
|
|
10
|
-
scheduleThemeUiColorModeSync()
|
|
11
|
-
if (typeof window !== 'undefined' && typeof window.CustomEvent === 'function') {
|
|
12
|
-
window.dispatchEvent(new window.CustomEvent(RECONCILE_COLOR_MODE_EVENT))
|
|
13
|
-
}
|
|
14
|
-
}
|
|
1
|
+
export { reconcileThemeUiColorModeOnNavigation as onRouteUpdateThemeUiColorMode } from '../color-mode/spa-navigation.js'
|
package/src/header.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx } from 'theme-ui'
|
|
1
|
+
import { Box } from '@theme-ui/components'
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Header
|
|
@@ -8,20 +7,9 @@ import { jsx } from 'theme-ui'
|
|
|
8
7
|
*/
|
|
9
8
|
const Header = ({ children, styles }) => {
|
|
10
9
|
return (
|
|
11
|
-
<header
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
variant: 'styles.Header'
|
|
15
|
-
}}
|
|
16
|
-
>
|
|
17
|
-
<div
|
|
18
|
-
sx={{
|
|
19
|
-
...(styles ? styles : {})
|
|
20
|
-
}}
|
|
21
|
-
>
|
|
22
|
-
{children}
|
|
23
|
-
</div>
|
|
24
|
-
</header>
|
|
10
|
+
<Box as='header' role='banner' sx={{ variant: 'styles.Header' }}>
|
|
11
|
+
<Box sx={{ ...(styles ? styles : {}) }}>{children}</Box>
|
|
12
|
+
</Box>
|
|
25
13
|
)
|
|
26
14
|
}
|
|
27
15
|
|
package/src/lazy-load.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
/** @jsx jsx */
|
|
2
|
-
import { jsx } from 'theme-ui'
|
|
3
1
|
import { useState, useEffect } from 'react'
|
|
2
|
+
import { Box } from '@theme-ui/components'
|
|
4
3
|
import { useInView } from 'react-intersection-observer'
|
|
5
4
|
|
|
6
5
|
const DefaultPlaceholder = ({ height = '100%', width = '100%' }) => (
|
|
7
|
-
<
|
|
6
|
+
<Box
|
|
8
7
|
data-testid='default-placeholder'
|
|
9
8
|
sx={{
|
|
10
9
|
minHeight: '1px',
|
|
@@ -14,28 +13,48 @@ const DefaultPlaceholder = ({ height = '100%', width = '100%' }) => (
|
|
|
14
13
|
}}
|
|
15
14
|
>
|
|
16
15
|
{' '}
|
|
17
|
-
</
|
|
16
|
+
</Box>
|
|
18
17
|
)
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
20
|
* Lazy Loader
|
|
22
21
|
*
|
|
23
22
|
* Hides a component until it's been visible in the viewport.
|
|
23
|
+
*
|
|
24
|
+
* IntersectionObserver does not run on the server; children are never included in SSR HTML.
|
|
25
|
+
* On the client, observation is deferred until after mount so Next.js (and other SSR
|
|
26
|
+
* frameworks) hydrate the placeholder first, then attach the observer — avoiding a
|
|
27
|
+
* first-paint flash of real content when the block is already in view.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} [props.useInViewOptions] -
|
|
30
|
+
* Passed to `useInView` after defaults (`triggerOnce: true`, `threshold: 0`,
|
|
31
|
+
* `initialInView: false`). Use `rootMargin` or `threshold` for stricter visibility.
|
|
32
|
+
* Pass `skip` to disable observation (merged with the internal client-only `skip`).
|
|
24
33
|
*/
|
|
25
|
-
const LazyLoad = ({ children, placeholder = <DefaultPlaceholder
|
|
26
|
-
const [
|
|
34
|
+
const LazyLoad = ({ children, placeholder = <DefaultPlaceholder />, useInViewOptions = {} }) => {
|
|
35
|
+
const [mounted, setMounted] = useState(false)
|
|
36
|
+
const [revealed, setRevealed] = useState(false)
|
|
37
|
+
const { skip: skipFromOptions, ...restInViewOptions } = useInViewOptions
|
|
38
|
+
|
|
27
39
|
const { ref, inView } = useInView({
|
|
28
40
|
triggerOnce: true,
|
|
29
|
-
threshold: 0
|
|
41
|
+
threshold: 0,
|
|
42
|
+
initialInView: false,
|
|
43
|
+
...restInViewOptions,
|
|
44
|
+
skip: !mounted || !!skipFromOptions
|
|
30
45
|
})
|
|
31
46
|
|
|
32
47
|
useEffect(() => {
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
setMounted(true)
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (mounted && inView) {
|
|
53
|
+
setRevealed(true)
|
|
35
54
|
}
|
|
36
|
-
}, [
|
|
55
|
+
}, [mounted, inView])
|
|
37
56
|
|
|
38
|
-
return <
|
|
57
|
+
return <Box ref={ref}>{revealed ? children : placeholder}</Box>
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
export default LazyLoad
|