@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.
Files changed (70) hide show
  1. package/.turbo/turbo-test$colon$coverage.log +44 -0
  2. package/.turbo/turbo-test.log +168 -0
  3. package/LICENSE +9 -0
  4. package/README.md +41 -0
  5. package/babel.config.cjs +9 -0
  6. package/coverage/clover.xml +131 -0
  7. package/coverage/coverage-final.json +13 -0
  8. package/coverage/lcov-report/base.css +224 -0
  9. package/coverage/lcov-report/block-navigation.js +87 -0
  10. package/coverage/lcov-report/browser-sync.js.html +268 -0
  11. package/coverage/lcov-report/favicon.png +0 -0
  12. package/coverage/lcov-report/index.html +161 -0
  13. package/coverage/lcov-report/prettify.css +1 -0
  14. package/coverage/lcov-report/prettify.js +2 -0
  15. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  16. package/coverage/lcov-report/sorter.js +210 -0
  17. package/coverage/lcov-report/src/button.js.html +160 -0
  18. package/coverage/lcov-report/src/color-mode/browser-sync.js.html +268 -0
  19. package/coverage/lcov-report/src/color-mode/constants.js.html +121 -0
  20. package/coverage/lcov-report/src/color-mode/head-inline.js.html +304 -0
  21. package/coverage/lcov-report/src/color-mode/index.html +176 -0
  22. package/coverage/lcov-report/src/color-mode/index.js.html +124 -0
  23. package/coverage/lcov-report/src/color-mode/normalize.js.html +112 -0
  24. package/coverage/lcov-report/src/color-mode/resolve-theme-colors.js.html +154 -0
  25. package/coverage/lcov-report/src/color-toggle.js.html +142 -0
  26. package/coverage/lcov-report/src/emotion-cache.js.html +151 -0
  27. package/coverage/lcov-report/src/helpers/index.html +116 -0
  28. package/coverage/lcov-report/src/helpers/isDarkMode.js.html +91 -0
  29. package/coverage/lcov-report/src/index.html +161 -0
  30. package/coverage/lcov-report/src/provider.js.html +124 -0
  31. package/coverage/lcov-report/src/skip-nav/SkipNavContent.js.html +133 -0
  32. package/coverage/lcov-report/src/skip-nav/SkipNavLink.js.html +301 -0
  33. package/coverage/lcov-report/src/skip-nav/index.html +131 -0
  34. package/coverage/lcov-report/src/theme.js.html +2143 -0
  35. package/coverage/lcov.info +309 -0
  36. package/jest.config.cjs +32 -0
  37. package/jest.setup.cjs +1 -0
  38. package/package.json +73 -0
  39. package/src/__snapshots__/theme.spec.js.snap +1027 -0
  40. package/src/button.js +25 -0
  41. package/src/button.spec.js +16 -0
  42. package/src/color-mode/browser-sync.js +61 -0
  43. package/src/color-mode/browser-sync.node.spec.js +15 -0
  44. package/src/color-mode/browser-sync.spec.js +137 -0
  45. package/src/color-mode/constants.js +12 -0
  46. package/src/color-mode/head-inline.js +73 -0
  47. package/src/color-mode/head-inline.spec.js +33 -0
  48. package/src/color-mode/index.js +13 -0
  49. package/src/color-mode/normalize.js +9 -0
  50. package/src/color-mode/normalize.spec.js +17 -0
  51. package/src/color-mode/resolve-theme-colors.js +23 -0
  52. package/src/color-mode/resolve-theme-colors.spec.js +39 -0
  53. package/src/color-toggle.js +19 -0
  54. package/src/color-toggle.spec.js +35 -0
  55. package/src/emotion-cache.js +22 -0
  56. package/src/emotion-cache.spec.js +30 -0
  57. package/src/helpers/isDarkMode.js +2 -0
  58. package/src/helpers/isDarkMode.spec.js +9 -0
  59. package/src/index.js +1 -0
  60. package/src/provider.js +13 -0
  61. package/src/provider.spec.js +25 -0
  62. package/src/skip-nav/SkipNavContent.js +16 -0
  63. package/src/skip-nav/SkipNavContent.spec.js +22 -0
  64. package/src/skip-nav/SkipNavLink.js +72 -0
  65. package/src/skip-nav/SkipNavLink.spec.js +56 -0
  66. package/src/skip-nav/index.js +2 -0
  67. package/src/theme.js +686 -0
  68. package/src/theme.spec.js +56 -0
  69. package/test-utils/mock-theme-toggles-react.js +10 -0
  70. 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,9 @@
1
+ export function normalizeThemeUiColorMode(mode) {
2
+ if (mode === 'light') {
3
+ return 'default'
4
+ }
5
+ if (mode === 'dark' || mode === 'default') {
6
+ return mode
7
+ }
8
+ return null
9
+ }
@@ -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
+ })
@@ -0,0 +1,2 @@
1
+ const isDarkMode = colorMode => colorMode === 'dark'
2
+ export default isDarkMode
@@ -0,0 +1,9 @@
1
+ import isDarkMode from './isDarkMode'
2
+
3
+ describe('isDarkMode', () => {
4
+ it('is true only for dark', () => {
5
+ expect(isDarkMode('dark')).toBe(true)
6
+ expect(isDarkMode('default')).toBe(false)
7
+ expect(isDarkMode(undefined)).toBe(false)
8
+ })
9
+ })
package/src/index.js ADDED
@@ -0,0 +1 @@
1
+ export { ChronogroveThemeProvider } from './provider.js'
@@ -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