@chronogrove/ui 0.79.0 → 0.81.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 +40 -19
- package/package.json +73 -6
- package/src/__snapshots__/header.spec.js.snap +8 -8
- package/src/__snapshots__/theme.spec.js.snap +39 -20
- 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/category-label.js +23 -0
- package/src/category-label.spec.js +24 -0
- package/src/chevron-icons.js +37 -0
- 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 +11 -2
- package/src/emotion-cache.node.spec.js +13 -0
- package/src/emotion-cache.spec.js +12 -0
- package/src/external-link-icon.js +30 -0
- package/src/external-link-icon.spec.js +16 -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/metric-badge.js +10 -0
- package/src/metric-badge.spec.js +15 -0
- package/src/metric-card.js +95 -0
- package/src/metric-card.spec.js +60 -0
- package/src/muted-card-footer.js +22 -0
- package/src/muted-card-footer.spec.js +25 -0
- 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/pagination.js +198 -0
- package/src/pagination.spec.js +281 -0
- package/src/skip-nav/SkipNavLink.js +6 -5
- package/src/skip-nav/SkipNavLink.spec.js +11 -0
- package/src/status-card.js +18 -0
- package/src/status-card.spec.js +38 -0
- package/src/theme.js +27 -20
- package/src/widget-call-to-action.js +106 -0
- package/src/widget-call-to-action.spec.js +115 -0
- package/src/widget-section.js +83 -0
- package/src/widget-section.spec.js +59 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chronogroveThemeSurfaceColorsDark,
|
|
3
|
+
chronogroveThemeSurfaceColorsLight
|
|
4
|
+
} from '../chronogrove-theme-surface-colors.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Minimal Theme UI–shaped theme object (only `colors`) for RSC / SSR `<head>` scripts.
|
|
8
|
+
* No imports from `theme-ui` or the full `@chronogrove/ui/theme` module — those pull in React
|
|
9
|
+
* `createContext` and cannot load in Next.js Server Components.
|
|
10
|
+
*
|
|
11
|
+
* Surface literals come from `chronogrove-theme-surface-colors.js`, same as `theme.js`.
|
|
12
|
+
*/
|
|
13
|
+
export const chronogroveHeadTheme = {
|
|
14
|
+
colors: {
|
|
15
|
+
...chronogroveThemeSurfaceColorsLight,
|
|
16
|
+
modes: {
|
|
17
|
+
dark: {
|
|
18
|
+
...chronogroveThemeSurfaceColorsDark
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chronogroveThemeSurfaceColorsDark,
|
|
3
|
+
chronogroveThemeSurfaceColorsLight
|
|
4
|
+
} from '../chronogrove-theme-surface-colors.js'
|
|
5
|
+
|
|
1
6
|
import { THEME_UI_COLOR_MODE_STORAGE_KEY } from './constants.js'
|
|
2
7
|
|
|
3
8
|
function q(str) {
|
|
@@ -60,13 +65,43 @@ export function buildHtmlBackgroundInlineScript({
|
|
|
60
65
|
`
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
const surfaceLight = chronogroveThemeSurfaceColorsLight
|
|
69
|
+
const surfaceDark = chronogroveThemeSurfaceColorsDark
|
|
70
|
+
|
|
63
71
|
export function buildThemeUiColorModeFallbackCss({
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
defaultBackgroundHex = surfaceLight.background,
|
|
73
|
+
darkBackgroundHex = surfaceDark.background,
|
|
74
|
+
defaultTextHex = surfaceLight.text,
|
|
75
|
+
defaultTextMutedHex = surfaceLight.textMuted,
|
|
76
|
+
darkTextHex = surfaceDark.text,
|
|
77
|
+
darkTextMutedHex = surfaceDark.textMuted,
|
|
78
|
+
defaultPanelBackground = surfaceLight['panel-background'],
|
|
79
|
+
darkPanelBackground = surfaceDark['panel-background'],
|
|
80
|
+
defaultPanelText = surfaceLight.text,
|
|
81
|
+
darkPanelText = surfaceDark.text
|
|
82
|
+
} = {}) {
|
|
83
|
+
/**
|
|
84
|
+
* Base `:root` (light) must not depend on `data-theme-ui-color-mode`. In App Router SSR / first
|
|
85
|
+
* paint, that attribute is not set until the inline no-flash script runs—rules that only target
|
|
86
|
+
* `:root[data-theme-ui-color-mode="default"]` would leave `--theme-ui-colors-panel-background`
|
|
87
|
+
* (and glass panels using `bg: 'panel-background'`) unset until hydration.
|
|
88
|
+
*/
|
|
69
89
|
return `
|
|
90
|
+
:root {
|
|
91
|
+
--theme-ui-colors-background: ${defaultBackgroundHex} !important;
|
|
92
|
+
--theme-ui-colors-panel-background: ${defaultPanelBackground} !important;
|
|
93
|
+
--theme-ui-colors-panel-text: ${defaultPanelText} !important;
|
|
94
|
+
--theme-ui-colors-text: ${defaultTextHex};
|
|
95
|
+
--theme-ui-colors-text-muted: ${defaultTextMutedHex};
|
|
96
|
+
}
|
|
97
|
+
:root[data-theme-ui-color-mode="dark"],
|
|
98
|
+
html.theme-ui-dark {
|
|
99
|
+
--theme-ui-colors-background: ${darkBackgroundHex} !important;
|
|
100
|
+
--theme-ui-colors-panel-background: ${darkPanelBackground} !important;
|
|
101
|
+
--theme-ui-colors-panel-text: ${darkPanelText} !important;
|
|
102
|
+
--theme-ui-colors-text: ${darkTextHex};
|
|
103
|
+
--theme-ui-colors-text-muted: ${darkTextMutedHex};
|
|
104
|
+
}
|
|
70
105
|
: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
106
|
: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
107
|
`.trim()
|
|
@@ -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 { scheduleThemeUiColorModeSync } from './color-mode/browser-sync.js'
|
|
4
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
|
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Small external-link glyph (inline SVG). Use for “opens in new window” affordances.
|
|
5
|
+
*/
|
|
6
|
+
export const ExternalLinkIcon = props => (
|
|
7
|
+
<svg
|
|
8
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
9
|
+
viewBox='0 0 512 512'
|
|
10
|
+
width='0.75em'
|
|
11
|
+
height='0.75em'
|
|
12
|
+
aria-hidden='true'
|
|
13
|
+
focusable='false'
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
<path
|
|
17
|
+
fill='currentColor'
|
|
18
|
+
d='M432 320h-32a16 16 0 0 0-16 16v112H64V128h144a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16H48a48 48 0 0 0-48 48v352a48 48 0 0 0 48 48h352a48 48 0 0 0 48-48V336a16 16 0 0 0-16-16zM488 0h-168c-13.3 0-24 10.7-24 24v8c0 13.3 10.7 24 24 24h69.2L207 279.6a24.06 24.06 0 0 0 0 34l10.2 10.2a24.06 24.06 0 0 0 34 0L425 128.8V200c0 13.3 10.7 24 24 24h8c13.3 0 24-10.7 24-24V56c0-30.9-25.1-56-56-56z'
|
|
19
|
+
/>
|
|
20
|
+
</svg>
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
/** Same icon wrapped in `<span>` for drop-in use where a text sibling is expected. */
|
|
24
|
+
export default function ViewExternalLinkIcon() {
|
|
25
|
+
return (
|
|
26
|
+
<span>
|
|
27
|
+
<ExternalLinkIcon />
|
|
28
|
+
</span>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
|
|
3
|
+
import ViewExternalLinkIcon, { ExternalLinkIcon } from './external-link-icon.js'
|
|
4
|
+
|
|
5
|
+
describe('ExternalLinkIcon', () => {
|
|
6
|
+
it('renders inline svg', () => {
|
|
7
|
+
const { container } = render(<ExternalLinkIcon data-testid='ico' />)
|
|
8
|
+
expect(container.querySelector('svg')).toBeInTheDocument()
|
|
9
|
+
expect(screen.getByTestId('ico')).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('ViewExternalLinkIcon wraps icon in span', () => {
|
|
13
|
+
const { container } = render(<ViewExternalLinkIcon />)
|
|
14
|
+
expect(container.querySelector('span svg')).toBeInTheDocument()
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -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 [
|