@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
package/src/lazy-load.spec.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
3
3
|
import '@testing-library/jest-dom'
|
|
4
4
|
import LazyLoad from './lazy-load.js'
|
|
5
5
|
|
|
@@ -31,7 +31,7 @@ describe('LazyLoad', () => {
|
|
|
31
31
|
expect(screen.queryByTestId('content')).not.toBeInTheDocument()
|
|
32
32
|
})
|
|
33
33
|
|
|
34
|
-
it('renders the children when visible', () => {
|
|
34
|
+
it('renders the children when visible', async () => {
|
|
35
35
|
mockInView = true
|
|
36
36
|
|
|
37
37
|
render(
|
|
@@ -40,7 +40,9 @@ describe('LazyLoad', () => {
|
|
|
40
40
|
</LazyLoad>
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(screen.getByTestId('content')).toBeInTheDocument()
|
|
45
|
+
})
|
|
44
46
|
})
|
|
45
47
|
|
|
46
48
|
it('renders the custom placeholder when provided', () => {
|
|
@@ -54,7 +56,7 @@ describe('LazyLoad', () => {
|
|
|
54
56
|
expect(screen.queryByTestId('content')).not.toBeInTheDocument()
|
|
55
57
|
})
|
|
56
58
|
|
|
57
|
-
it('does not re-render children if already visible', () => {
|
|
59
|
+
it('does not re-render children if already visible', async () => {
|
|
58
60
|
mockInView = true
|
|
59
61
|
|
|
60
62
|
const { rerender } = render(
|
|
@@ -63,7 +65,9 @@ describe('LazyLoad', () => {
|
|
|
63
65
|
</LazyLoad>
|
|
64
66
|
)
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
await waitFor(() => {
|
|
69
|
+
expect(screen.getByTestId('content')).toBeInTheDocument()
|
|
70
|
+
})
|
|
67
71
|
|
|
68
72
|
mockInView = false
|
|
69
73
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Box } from '@theme-ui/components'
|
|
4
|
+
|
|
5
|
+
import { ChronogroveAnimatedPageBackground } from '../animated-page-background/index.js'
|
|
6
|
+
import { useDocumentColorModeSurface } from '../color-mode/index.js'
|
|
7
|
+
import { ChronogroveThemeProvider } from '../provider.js'
|
|
8
|
+
import chronogroveTheme from '../theme.js'
|
|
9
|
+
|
|
10
|
+
import { ChronogroveNextThemeUiColorModeRouteSync } from './theme-ui-color-mode-route-sync.js'
|
|
11
|
+
|
|
12
|
+
/** Mirrors Gatsby `RootWrapper`: sync html class, data attribute, and surface bg from the live theme. */
|
|
13
|
+
function DocumentColorModeSurface() {
|
|
14
|
+
useDocumentColorModeSurface()
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default Next.js App Router shell: Theme UI provider, three.js Color Bends background (same as
|
|
20
|
+
* Gatsby home), document surface sync, and soft-navigation color-mode reconcile. Wrap with
|
|
21
|
+
* {@link ChronogroveNextEmotionRegistry} in `layout.jsx` outside this component.
|
|
22
|
+
*/
|
|
23
|
+
export function ChronogroveNextAppShell({ children, theme = chronogroveTheme }) {
|
|
24
|
+
return (
|
|
25
|
+
<ChronogroveThemeProvider theme={theme}>
|
|
26
|
+
<ChronogroveAnimatedPageBackground />
|
|
27
|
+
<DocumentColorModeSurface />
|
|
28
|
+
<Box sx={{ position: 'relative', zIndex: 1, bg: 'transparent', color: 'text' }}>
|
|
29
|
+
<ChronogroveNextThemeUiColorModeRouteSync />
|
|
30
|
+
{children}
|
|
31
|
+
</Box>
|
|
32
|
+
</ChronogroveThemeProvider>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { CacheProvider } from '@emotion/react'
|
|
5
|
+
import createCache from '@emotion/cache'
|
|
6
|
+
import { useServerInsertedHTML } from 'next/navigation'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Emotion cache for Next.js App Router: streaming SSR via `useServerInsertedHTML`, key `css`
|
|
10
|
+
* to match Theme UI / Chronogrove.
|
|
11
|
+
*
|
|
12
|
+
* Intercepts `cache.insert` and flushes only *new* rule names on each `useServerInsertedHTML`
|
|
13
|
+
* invocation. Do **not** use `Object.keys(cache.inserted)` (or similar) in the hook body — that
|
|
14
|
+
* re-emits every rule on every chunk during streaming SSR. Here, `flush()` drains a per-request
|
|
15
|
+
* queue so each rule is serialized once; `cache.inserted` stays as Emotion’s dedupe store.
|
|
16
|
+
*
|
|
17
|
+
* Next.js **pushes** a new callback from every render (`serverInsertedHTMLCallbacks.push`); all
|
|
18
|
+
* closures call the same `flush`, so only the first callback in a flush pass drains pending names.
|
|
19
|
+
*
|
|
20
|
+
* @see https://github.com/emotion-js/emotion/issues/2928
|
|
21
|
+
*
|
|
22
|
+
* @see https://nextjs.org/docs/app/building-your-application/styling/css-in-js
|
|
23
|
+
*/
|
|
24
|
+
export function ChronogroveNextEmotionRegistry({ children }) {
|
|
25
|
+
const [{ cache, flush }] = React.useState(() => {
|
|
26
|
+
const c = createCache({ key: 'css' })
|
|
27
|
+
c.compat = true
|
|
28
|
+
const prevInsert = c.insert
|
|
29
|
+
let pendingNames = []
|
|
30
|
+
c.insert = (...args) => {
|
|
31
|
+
const serialized = args[1]
|
|
32
|
+
if (serialized != null && c.inserted[serialized.name] === undefined) {
|
|
33
|
+
pendingNames.push(serialized.name)
|
|
34
|
+
}
|
|
35
|
+
return prevInsert(...args)
|
|
36
|
+
}
|
|
37
|
+
const flush = () => {
|
|
38
|
+
const names = pendingNames
|
|
39
|
+
pendingNames = []
|
|
40
|
+
return names
|
|
41
|
+
}
|
|
42
|
+
return { cache: c, flush }
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
useServerInsertedHTML(() => {
|
|
46
|
+
const names = flush()
|
|
47
|
+
if (names.length === 0) {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
let styles = ''
|
|
51
|
+
for (const name of names) {
|
|
52
|
+
const inserted = cache.inserted[name]
|
|
53
|
+
// `inserted` can be `true` for Global/keyframes when rules are already in a sheet (Next.js + Emotion pattern).
|
|
54
|
+
if (typeof inserted === 'string') {
|
|
55
|
+
styles += inserted
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return (
|
|
59
|
+
<style
|
|
60
|
+
key={cache.key}
|
|
61
|
+
data-emotion={`${cache.key} ${names.join(' ')}`}
|
|
62
|
+
dangerouslySetInnerHTML={{ __html: styles }}
|
|
63
|
+
/>
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return <CacheProvider value={cache}>{children}</CacheProvider>
|
|
68
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
jest.mock('next/navigation', () => ({
|
|
6
|
+
useServerInsertedHTML: jest.fn()
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import { render } from '@testing-library/react'
|
|
11
|
+
import { Global, css } from '@emotion/react'
|
|
12
|
+
import { useServerInsertedHTML } from 'next/navigation'
|
|
13
|
+
|
|
14
|
+
import { ChronogroveNextEmotionRegistry } from './emotion-registry.js'
|
|
15
|
+
|
|
16
|
+
/** `className={css({...})}` does not always hit `cache.insert` in Jest/jsdom; `Global` does. */
|
|
17
|
+
function EmotionChild({ color }) {
|
|
18
|
+
return (
|
|
19
|
+
<Global
|
|
20
|
+
styles={css`
|
|
21
|
+
body {
|
|
22
|
+
color: ${color};
|
|
23
|
+
}
|
|
24
|
+
`}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('ChronogroveNextEmotionRegistry', () => {
|
|
30
|
+
let flushCallbacks
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
flushCallbacks = []
|
|
34
|
+
useServerInsertedHTML.mockImplementation(cb => {
|
|
35
|
+
flushCallbacks.push(cb)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('flushes only new insertions per useServerInsertedHTML call (avoids duplicate style tags when streaming)', () => {
|
|
40
|
+
const { rerender } = render(
|
|
41
|
+
<ChronogroveNextEmotionRegistry>
|
|
42
|
+
<EmotionChild color='tomato' />
|
|
43
|
+
</ChronogroveNextEmotionRegistry>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const first = flushCallbacks[flushCallbacks.length - 1]()
|
|
47
|
+
expect(first).not.toBeNull()
|
|
48
|
+
// `cache.inserted[id]` can be `true` for Global in jsdom; must not stringify to invalid CSS.
|
|
49
|
+
expect(first.props.dangerouslySetInnerHTML.__html).not.toMatch(/^true+$/)
|
|
50
|
+
|
|
51
|
+
expect(flushCallbacks[flushCallbacks.length - 1]()).toBeNull()
|
|
52
|
+
|
|
53
|
+
rerender(
|
|
54
|
+
<ChronogroveNextEmotionRegistry>
|
|
55
|
+
<EmotionChild color='steelblue' />
|
|
56
|
+
</ChronogroveNextEmotionRegistry>
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const afterNewRules = flushCallbacks[flushCallbacks.length - 1]()
|
|
60
|
+
expect(afterNewRules).not.toBeNull()
|
|
61
|
+
expect(flushCallbacks[flushCallbacks.length - 1]()).toBeNull()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Next.js registers `useServerInsertedHTML` once per render (`push` onto an array). All handlers
|
|
66
|
+
* share one `flush`; only the first invocation per drain should emit — not N copies of the full
|
|
67
|
+
* `cache.inserted` map.
|
|
68
|
+
*/
|
|
69
|
+
it('drains pending styles once when multiple handlers run (matches Next.js callback stacking)', () => {
|
|
70
|
+
const { rerender } = render(
|
|
71
|
+
<ChronogroveNextEmotionRegistry>
|
|
72
|
+
<EmotionChild color='tomato' />
|
|
73
|
+
</ChronogroveNextEmotionRegistry>
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
expect(flushCallbacks).toHaveLength(1)
|
|
77
|
+
|
|
78
|
+
rerender(
|
|
79
|
+
<ChronogroveNextEmotionRegistry>
|
|
80
|
+
<EmotionChild color='tomato' />
|
|
81
|
+
</ChronogroveNextEmotionRegistry>
|
|
82
|
+
)
|
|
83
|
+
rerender(
|
|
84
|
+
<ChronogroveNextEmotionRegistry>
|
|
85
|
+
<EmotionChild color='tomato' />
|
|
86
|
+
</ChronogroveNextEmotionRegistry>
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
expect(flushCallbacks).toHaveLength(3)
|
|
90
|
+
|
|
91
|
+
const html0 = flushCallbacks[0]()
|
|
92
|
+
const html1 = flushCallbacks[1]()
|
|
93
|
+
const html2 = flushCallbacks[2]()
|
|
94
|
+
|
|
95
|
+
expect(html0).not.toBeNull()
|
|
96
|
+
expect(html1).toBeNull()
|
|
97
|
+
expect(html2).toBeNull()
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ChronogroveNextAppShell } from './app-shell.js'
|
|
2
|
+
export { ChronogroveNextEmotionRegistry } from './emotion-registry.js'
|
|
3
|
+
export { ChronogroveNextRootLayoutHead } from './root-layout-head.js'
|
|
4
|
+
export { ChronogroveNextThemeUiColorModeRouteSync } from './theme-ui-color-mode-route-sync.js'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chronogroveHeadTheme,
|
|
3
|
+
resolveChronogroveSurfaceColors,
|
|
4
|
+
buildThemeUiNoFlashInlineScript,
|
|
5
|
+
buildHtmlBackgroundInlineScript,
|
|
6
|
+
buildThemeUiColorModeFallbackCss
|
|
7
|
+
} from '../color-mode/index.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Server Component fragment for the root `<head>`: Emotion insertion point, Theme UI no-flash
|
|
11
|
+
* script, HTML background script, and fallback CSS (same composition as
|
|
12
|
+
* `buildThemeUiColorModeHeadComponents` for Gatsby).
|
|
13
|
+
*/
|
|
14
|
+
export function ChronogroveNextRootLayoutHead() {
|
|
15
|
+
const surface = resolveChronogroveSurfaceColors(chronogroveHeadTheme)
|
|
16
|
+
const colorModeScript = buildThemeUiNoFlashInlineScript()
|
|
17
|
+
const htmlBackgroundScript = buildHtmlBackgroundInlineScript({
|
|
18
|
+
defaultBackgroundHex: surface.defaultBackgroundHex,
|
|
19
|
+
darkBackgroundHex: surface.darkBackgroundHex
|
|
20
|
+
})
|
|
21
|
+
const colorModeFallbackCSS = buildThemeUiColorModeFallbackCss({
|
|
22
|
+
defaultBackgroundHex: surface.defaultBackgroundHex,
|
|
23
|
+
darkBackgroundHex: surface.darkBackgroundHex,
|
|
24
|
+
defaultTextHex: surface.defaultTextHex,
|
|
25
|
+
defaultTextMutedHex: surface.defaultTextMutedHex,
|
|
26
|
+
darkTextHex: surface.darkTextHex,
|
|
27
|
+
darkTextMutedHex: surface.darkTextMutedHex,
|
|
28
|
+
defaultPanelBackground: surface.defaultPanelBackground,
|
|
29
|
+
darkPanelBackground: surface.darkPanelBackground,
|
|
30
|
+
defaultPanelText: surface.defaultPanelText,
|
|
31
|
+
darkPanelText: surface.darkPanelText
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<meta name='emotion-insertion-point' content='' />
|
|
37
|
+
<script dangerouslySetInnerHTML={{ __html: colorModeScript }} />
|
|
38
|
+
<script dangerouslySetInnerHTML={{ __html: htmlBackgroundScript }} />
|
|
39
|
+
<style dangerouslySetInnerHTML={{ __html: colorModeFallbackCSS }} />
|
|
40
|
+
</>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { render } from '@testing-library/react'
|
|
6
|
+
|
|
7
|
+
import { ChronogroveNextRootLayoutHead } from './root-layout-head.js'
|
|
8
|
+
|
|
9
|
+
describe('ChronogroveNextRootLayoutHead', () => {
|
|
10
|
+
it('emits scripts and fallback CSS (meta belongs in document <head> in real App Router layouts)', () => {
|
|
11
|
+
const { container } = render(<ChronogroveNextRootLayoutHead />)
|
|
12
|
+
// RTL renders into a div; React omits invalid <meta> there — in Next, `<head><ChronogroveNextRootLayoutHead /></head>` keeps the meta.
|
|
13
|
+
expect(container.querySelectorAll('script')).toHaveLength(2)
|
|
14
|
+
expect(container.querySelector('style')).toBeTruthy()
|
|
15
|
+
expect(container.textContent).toMatch(/theme-ui-color-mode/)
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react'
|
|
4
|
+
import { usePathname } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
import { reconcileThemeUiColorModeOnNavigation } from '../color-mode/index.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Keeps Theme UI color mode aligned after Next.js **client-side navigations** (same role as Gatsby
|
|
10
|
+
* `onRouteUpdate`). Do **not** run on initial mount: `reconcileThemeUiColorModeOnNavigation` syncs the
|
|
11
|
+
* DOM from `localStorage`, while Theme UI applies a toggle by updating React state first and writing
|
|
12
|
+
* `localStorage` in a `useEffect` — an eager reconcile can re-read stale `localStorage` and force
|
|
13
|
+
* the page back to the previous mode (e.g. stuck in dark).
|
|
14
|
+
*/
|
|
15
|
+
export function ChronogroveNextThemeUiColorModeRouteSync() {
|
|
16
|
+
const pathname = usePathname()
|
|
17
|
+
const previousPathname = useRef(null)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (previousPathname.current === null) {
|
|
21
|
+
previousPathname.current = pathname
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
if (previousPathname.current === pathname) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
previousPathname.current = pathname
|
|
28
|
+
reconcileThemeUiColorModeOnNavigation()
|
|
29
|
+
}, [pathname])
|
|
30
|
+
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Box } from '@theme-ui/components'
|
|
4
|
+
import { useColorMode } from 'theme-ui'
|
|
5
|
+
|
|
6
|
+
import isDarkMode from './helpers/isDarkMode.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fixed viewport layer behind page content (`z-index: 0`), matching the role of
|
|
10
|
+
* `AnimatedPageBackground` on the Gatsby home: a real surface under `z-index: 1` UI so
|
|
11
|
+
* frosted panels (`backdrop-filter` + `panel-background`) have something to blur and tint
|
|
12
|
+
* against. The full site uses WebGL Color Bends there; this package ships a **lightweight**
|
|
13
|
+
* CSS gradient treatment only (no three.js).
|
|
14
|
+
*/
|
|
15
|
+
export function ChronogrovePageBackdrop() {
|
|
16
|
+
const [colorMode] = useColorMode()
|
|
17
|
+
const dark = isDarkMode(colorMode)
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Box
|
|
21
|
+
aria-hidden
|
|
22
|
+
sx={{
|
|
23
|
+
position: 'fixed',
|
|
24
|
+
inset: 0,
|
|
25
|
+
width: '100vw',
|
|
26
|
+
minHeight: '100vh',
|
|
27
|
+
zIndex: 0,
|
|
28
|
+
pointerEvents: 'none',
|
|
29
|
+
bg: 'background',
|
|
30
|
+
// Dark: subtle primary/secondary glows — same intent as Color Bends, fraction of the cost.
|
|
31
|
+
backgroundImage: dark
|
|
32
|
+
? `
|
|
33
|
+
radial-gradient(ellipse 120% 85% at 50% -15%, rgba(74, 158, 255, 0.16) 0%, transparent 52%),
|
|
34
|
+
radial-gradient(ellipse 90% 70% at 95% 85%, rgba(113, 30, 155, 0.12) 0%, transparent 48%),
|
|
35
|
+
radial-gradient(ellipse 70% 50% at 10% 60%, rgba(128, 0, 128, 0.08) 0%, transparent 45%)
|
|
36
|
+
`
|
|
37
|
+
: 'none',
|
|
38
|
+
transition: 'background-image 0.3s ease'
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { render } from '@testing-library/react'
|
|
6
|
+
import { ThemeUIProvider } from 'theme-ui'
|
|
7
|
+
|
|
8
|
+
import chronogroveTheme from './theme.js'
|
|
9
|
+
import { ChronogrovePageBackdrop } from './page-backdrop.js'
|
|
10
|
+
|
|
11
|
+
describe('ChronogrovePageBackdrop', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
window.localStorage.removeItem('theme-ui-color-mode')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('renders a fixed backdrop layer', () => {
|
|
17
|
+
const { container } = render(
|
|
18
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
19
|
+
<ChronogrovePageBackdrop />
|
|
20
|
+
</ThemeUIProvider>
|
|
21
|
+
)
|
|
22
|
+
const layer = container.querySelector('[aria-hidden="true"]')
|
|
23
|
+
expect(layer).toBeTruthy()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('adds gradient overlays in dark mode', () => {
|
|
27
|
+
window.localStorage.setItem('theme-ui-color-mode', 'dark')
|
|
28
|
+
const { container } = render(
|
|
29
|
+
<ThemeUIProvider
|
|
30
|
+
theme={{
|
|
31
|
+
...chronogroveTheme,
|
|
32
|
+
config: { useLocalStorage: true, useColorSchemeMediaQuery: false }
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<ChronogrovePageBackdrop />
|
|
36
|
+
</ThemeUIProvider>
|
|
37
|
+
)
|
|
38
|
+
const layer = container.querySelector('[aria-hidden="true"]')
|
|
39
|
+
expect(window.getComputedStyle(layer).backgroundImage).toMatch(/radial-gradient/)
|
|
40
|
+
})
|
|
41
|
+
})
|
package/src/pagination-button.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/** @jsx jsx */
|
|
2
1
|
import React from 'react'
|
|
3
|
-
import {
|
|
2
|
+
import { Box } from '@theme-ui/components'
|
|
3
|
+
import { useThemeUI } from 'theme-ui'
|
|
4
4
|
import isDarkMode from './helpers/isDarkMode.js'
|
|
5
5
|
import { hexToRgb } from './color-utils.js'
|
|
6
6
|
|
|
@@ -97,9 +97,9 @@ const PaginationButton = ({
|
|
|
97
97
|
)
|
|
98
98
|
|
|
99
99
|
return (
|
|
100
|
-
<button type='button' onClick={onClick} disabled={disabled} sx={baseStyles} {...props}>
|
|
100
|
+
<Box as='button' type='button' onClick={onClick} disabled={disabled} sx={baseStyles} {...props}>
|
|
101
101
|
{content}
|
|
102
|
-
</
|
|
102
|
+
</Box>
|
|
103
103
|
)
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** @jsx jsx */
|
|
2
|
-
import { jsx } from 'theme-ui'
|
|
3
1
|
import { render, screen, fireEvent } from '@testing-library/react'
|
|
4
2
|
import { ThemeUIProvider } from 'theme-ui'
|
|
5
3
|
|
|
@@ -96,6 +94,20 @@ describe('PaginationButton', () => {
|
|
|
96
94
|
})
|
|
97
95
|
})
|
|
98
96
|
|
|
97
|
+
it('uses dark secondary palette when color mode is dark', () => {
|
|
98
|
+
mockUseThemeUI.mockReturnValueOnce({
|
|
99
|
+
colorMode: 'dark',
|
|
100
|
+
theme: {
|
|
101
|
+
colors: {
|
|
102
|
+
primary: BUTTON_PRIMARY_COLORS.light,
|
|
103
|
+
primaryRgb: '66, 46, 163'
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
renderWithProviders(<PaginationButton variant='secondary'>2</PaginationButton>)
|
|
108
|
+
expect(screen.getByRole('button', { name: /2/i })).toHaveStyle({ color: '#888' })
|
|
109
|
+
})
|
|
110
|
+
|
|
99
111
|
it('applies small size styles', () => {
|
|
100
112
|
renderWithProviders(<PaginationButton size='small'>1</PaginationButton>)
|
|
101
113
|
|
|
@@ -118,6 +130,18 @@ describe('PaginationButton', () => {
|
|
|
118
130
|
})
|
|
119
131
|
})
|
|
120
132
|
|
|
133
|
+
it('falls back to medium size when size is invalid', () => {
|
|
134
|
+
renderWithProviders(<PaginationButton size='invalid'>1</PaginationButton>)
|
|
135
|
+
|
|
136
|
+
const button = screen.getByRole('button', { name: /1/i })
|
|
137
|
+
expect(button).toHaveStyle({
|
|
138
|
+
fontSize: '11px',
|
|
139
|
+
minWidth: '28px',
|
|
140
|
+
height: '28px',
|
|
141
|
+
padding: '6px 10px'
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
121
145
|
it('renders with icon', () => {
|
|
122
146
|
const TestIcon = () => <span data-testid='test-icon'>←</span>
|
|
123
147
|
renderWithProviders(<PaginationButton icon={<TestIcon />}>Prev</PaginationButton>)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import isDarkMode from '
|
|
1
|
+
import React, { forwardRef } from 'react'
|
|
2
|
+
import { Box } from '@theme-ui/components'
|
|
3
|
+
import { useThemeUI } from 'theme-ui'
|
|
4
|
+
import isDarkMode from '../helpers/isDarkMode.js'
|
|
5
5
|
|
|
6
6
|
const SkipNavLink = forwardRef(function SkipNavLink(
|
|
7
7
|
{ as: Comp = 'a', children = 'Skip to content', contentId = 'skip-nav-content', ...props },
|
|
@@ -13,7 +13,8 @@ const SkipNavLink = forwardRef(function SkipNavLink(
|
|
|
13
13
|
const primaryRgb = theme?.colors?.primaryRgb ?? (darkModeActive ? '74, 158, 255' : '66, 46, 163')
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
|
-
<
|
|
16
|
+
<Box
|
|
17
|
+
as={Comp}
|
|
17
18
|
{...props}
|
|
18
19
|
ref={forwardedRef}
|
|
19
20
|
href={`#${contentId}`}
|
|
@@ -63,7 +64,7 @@ const SkipNavLink = forwardRef(function SkipNavLink(
|
|
|
63
64
|
}}
|
|
64
65
|
>
|
|
65
66
|
{children}
|
|
66
|
-
</
|
|
67
|
+
</Box>
|
|
67
68
|
)
|
|
68
69
|
})
|
|
69
70
|
|
|
@@ -30,6 +30,17 @@ describe('SkipNavLink', () => {
|
|
|
30
30
|
expect(link).toHaveAttribute('data-skip-nav-link', '')
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
+
it('respects custom contentId for the hash target', () => {
|
|
34
|
+
renderLink(<SkipNavLink contentId='main'>Jump</SkipNavLink>)
|
|
35
|
+
expect(screen.getByRole('link', { name: /jump/i })).toHaveAttribute('href', '#main')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('uses default label and skip target when props are omitted', () => {
|
|
39
|
+
renderLink(<SkipNavLink />)
|
|
40
|
+
const link = screen.getByRole('link', { name: /skip to content/i })
|
|
41
|
+
expect(link).toHaveAttribute('href', '#skip-nav-content')
|
|
42
|
+
})
|
|
43
|
+
|
|
33
44
|
it('uses fallback colors when theme omits primary tokens', () => {
|
|
34
45
|
render(
|
|
35
46
|
<ThemeUIProvider theme={{ colors: {} }}>
|
package/src/theme.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { tailwind } from '@theme-ui/presets'
|
|
2
2
|
import { merge } from 'theme-ui'
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
chronogroveThemeSurfaceColorsDark,
|
|
6
|
+
chronogroveThemeSurfaceColorsLight
|
|
7
|
+
} from './chronogrove-theme-surface-colors.js'
|
|
8
|
+
|
|
4
9
|
const fonts = {
|
|
5
10
|
sans: '-apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, Ubuntu, roboto, noto, segoe ui, arial, sans-serif',
|
|
6
11
|
serif:
|
|
@@ -74,8 +79,8 @@ export const backdropBlurLight = {
|
|
|
74
79
|
|
|
75
80
|
export const card = {
|
|
76
81
|
borderRadius: 'card',
|
|
77
|
-
|
|
78
|
-
color: '
|
|
82
|
+
bg: 'panel-background',
|
|
83
|
+
color: 'text',
|
|
79
84
|
boxShadow: 'default',
|
|
80
85
|
flexGrow: 1,
|
|
81
86
|
padding: 3,
|
|
@@ -86,9 +91,9 @@ export const card = {
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
export const metricCard = {
|
|
89
|
-
backgroundColor: '
|
|
94
|
+
backgroundColor: 'panel-background',
|
|
90
95
|
boxShadow: 'none',
|
|
91
|
-
color: '
|
|
96
|
+
color: 'text',
|
|
92
97
|
span: {
|
|
93
98
|
fontFamily: 'heading',
|
|
94
99
|
fontWeight: 'bold',
|
|
@@ -100,8 +105,6 @@ export const PostCard = {
|
|
|
100
105
|
...card,
|
|
101
106
|
...floatOnHover,
|
|
102
107
|
...glassmorhismPanel,
|
|
103
|
-
backgroundColor: 'var(--theme-ui-colors-panel-background)',
|
|
104
|
-
color: 'var(--theme-ui-colors-panel-text)',
|
|
105
108
|
display: 'flex',
|
|
106
109
|
height: '100%',
|
|
107
110
|
flexDirection: 'column',
|
|
@@ -369,20 +372,16 @@ export default merge(tailwind, {
|
|
|
369
372
|
|
|
370
373
|
colors: {
|
|
371
374
|
accent: 'deeppink',
|
|
372
|
-
|
|
373
|
-
'panel-background': 'rgba(255, 255, 255, 0.45)',
|
|
375
|
+
...chronogroveThemeSurfaceColorsLight,
|
|
374
376
|
'panel-divider': () => '1px solid rgba(255, 229, 224, 0.17)',
|
|
375
377
|
'panel-highlight': theme => theme.colors.gray[1],
|
|
376
378
|
modes: {
|
|
377
379
|
dark: {
|
|
378
|
-
|
|
379
|
-
'panel-background': 'rgba(20, 20, 31, 0.45)',
|
|
380
|
+
...chronogroveThemeSurfaceColorsDark,
|
|
380
381
|
'panel-divider': theme => `1px solid ${theme.colors.gray[8]}`,
|
|
381
382
|
'panel-highlight': theme => theme.colors.gray[8],
|
|
382
383
|
primary: '#4a9eff',
|
|
383
384
|
primaryRgb: '74, 158, 255',
|
|
384
|
-
text: '#fff',
|
|
385
|
-
textMuted: '#d8d8d8',
|
|
386
385
|
tableText: '#fff',
|
|
387
386
|
tableBackground: 'rgba(30, 30, 47, 0.45)',
|
|
388
387
|
tableHeaderBackground: 'rgba(30, 37, 48, 0.8)',
|
|
@@ -400,9 +399,7 @@ export default merge(tailwind, {
|
|
|
400
399
|
tableHeaderBackground: '#f4f4f9',
|
|
401
400
|
tableRowBackground: 'transparent',
|
|
402
401
|
tableRowAlternateBackground: '#fafafa',
|
|
403
|
-
tableBorder: 'muted'
|
|
404
|
-
text: '#111',
|
|
405
|
-
textMuted: '#333'
|
|
402
|
+
tableBorder: 'muted'
|
|
406
403
|
},
|
|
407
404
|
|
|
408
405
|
fonts: {
|
package/babel.config.cjs
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Jest transforms @emotion and JSX; mirror theme with classic runtime.
|
|
3
|
-
*/
|
|
4
|
-
module.exports = {
|
|
5
|
-
presets: [
|
|
6
|
-
[require.resolve('@babel/preset-env'), { targets: { node: 'current' } }],
|
|
7
|
-
[require.resolve('@babel/preset-react'), { runtime: 'classic', useBuiltIns: true }]
|
|
8
|
-
]
|
|
9
|
-
}
|
package/jest.config.cjs
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/** @type {import('jest').Config} */
|
|
2
|
-
module.exports = {
|
|
3
|
-
testEnvironment: 'jsdom',
|
|
4
|
-
clearMocks: true,
|
|
5
|
-
setupFilesAfterEnv: ['<rootDir>/jest.setup.cjs'],
|
|
6
|
-
testMatch: ['**/?(*.)+(spec|test).js'],
|
|
7
|
-
transform: {
|
|
8
|
-
'^.+\\.jsx?$': 'babel-jest'
|
|
9
|
-
},
|
|
10
|
-
transformIgnorePatterns: ['node_modules/(?!theme-ui)'],
|
|
11
|
-
collectCoverageFrom: ['src/**/*.js', '!src/**/*.spec.js'],
|
|
12
|
-
coverageDirectory: 'coverage',
|
|
13
|
-
coveragePathIgnorePatterns: [
|
|
14
|
-
'/node_modules/',
|
|
15
|
-
'src/index.js',
|
|
16
|
-
'src/skip-nav/index.js',
|
|
17
|
-
'src/color-mode/index.js',
|
|
18
|
-
'src/gatsby/index.js',
|
|
19
|
-
'src/theme.js'
|
|
20
|
-
],
|
|
21
|
-
moduleNameMapper: {
|
|
22
|
-
'^@chronogrove/ui/is-dark-mode$': '<rootDir>/src/helpers/isDarkMode.js',
|
|
23
|
-
'^@theme-toggles/react$': '<rootDir>/test-utils/mock-theme-toggles-react.js'
|
|
24
|
-
},
|
|
25
|
-
coverageThreshold: {
|
|
26
|
-
global: {
|
|
27
|
-
statements: 95,
|
|
28
|
-
branches: 90,
|
|
29
|
-
functions: 95,
|
|
30
|
-
lines: 95
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
package/jest.setup.cjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
require('@testing-library/jest-dom')
|