@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.
Files changed (58) hide show
  1. package/README.md +42 -19
  2. package/package.json +42 -6
  3. package/src/__snapshots__/header.spec.js.snap +8 -8
  4. package/src/__snapshots__/theme.spec.js.snap +14 -15
  5. package/src/action-button.js +6 -6
  6. package/src/action-button.spec.js +14 -2
  7. package/src/action-card-layout.js +13 -0
  8. package/src/action-card-layout.spec.js +13 -0
  9. package/src/animated-page-background/ChronogroveAnimatedPageBackground.js +153 -0
  10. package/src/animated-page-background/ChronogroveAnimatedPageBackground.spec.js +189 -0
  11. package/src/animated-page-background/ColorBends.js +309 -0
  12. package/src/animated-page-background/color-bends.css +13 -0
  13. package/src/animated-page-background/index.js +2 -0
  14. package/src/animated-page-background/index.spec.js +18 -0
  15. package/src/button.js +4 -3
  16. package/src/chronogrove-theme-surface-colors.js +22 -0
  17. package/src/color-mode/browser-sync.js +7 -0
  18. package/src/color-mode/browser-sync.spec.js +7 -0
  19. package/src/color-mode/chronogrove-head-theme.js +22 -0
  20. package/src/color-mode/head-inline.js +40 -5
  21. package/src/color-mode/head-inline.spec.js +29 -0
  22. package/src/color-mode/index.js +3 -0
  23. package/src/color-mode/resolve-theme-colors.js +18 -6
  24. package/src/color-mode/resolve-theme-colors.spec.js +13 -3
  25. package/src/color-mode/spa-navigation.js +14 -0
  26. package/src/color-mode/spa-navigation.spec.js +25 -0
  27. package/src/color-mode/use-document-color-mode-surface.js +52 -0
  28. package/src/color-mode/use-document-color-mode-surface.node.spec.js +12 -0
  29. package/src/color-mode/use-document-color-mode-surface.spec.js +154 -0
  30. package/src/color-toggle-styles.css +10 -0
  31. package/src/color-toggle.js +12 -3
  32. package/src/emotion-cache.node.spec.js +13 -0
  33. package/src/emotion-cache.spec.js +12 -0
  34. package/src/gatsby/build-theme-ui-color-mode-head-components.js +7 -1
  35. package/src/gatsby/index.spec.js +42 -0
  36. package/src/gatsby/on-route-update-color-mode.js +1 -14
  37. package/src/header.js +4 -16
  38. package/src/lazy-load.js +30 -11
  39. package/src/lazy-load.spec.js +9 -5
  40. package/src/next/app-shell.js +34 -0
  41. package/src/next/emotion-registry.js +68 -0
  42. package/src/next/emotion-registry.spec.js +99 -0
  43. package/src/next/index.js +4 -0
  44. package/src/next/root-layout-head.js +42 -0
  45. package/src/next/root-layout-head.spec.js +17 -0
  46. package/src/next/theme-ui-color-mode-route-sync.js +32 -0
  47. package/src/page-backdrop.js +42 -0
  48. package/src/page-backdrop.spec.js +41 -0
  49. package/src/pagination-button.js +4 -4
  50. package/src/pagination-button.spec.js +26 -2
  51. package/src/skip-nav/SkipNavLink.js +7 -6
  52. package/src/skip-nav/SkipNavLink.spec.js +11 -0
  53. package/src/theme.js +12 -15
  54. package/babel.config.cjs +0 -9
  55. package/jest.config.cjs +0 -33
  56. package/jest.setup.cjs +0 -1
  57. package/test-utils/mock-theme-toggles-react.js +0 -10
  58. package/turbo.json +0 -12
@@ -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
- expect(screen.getByTestId('content')).toBeInTheDocument()
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
- expect(screen.getByTestId('content')).toBeInTheDocument()
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
+ })
@@ -1,6 +1,6 @@
1
- /** @jsx jsx */
2
1
  import React from 'react'
3
- import { jsx, useThemeUI } from 'theme-ui'
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
- </button>
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
- /** @jsx jsx */
2
- import { jsx, useThemeUI } from 'theme-ui'
3
- import { forwardRef } from 'react'
4
- import isDarkMode from '@chronogrove/ui/is-dark-mode'
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
- <Comp
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
- </Comp>
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
- background: 'var(--theme-ui-colors-panel-background)',
78
- color: 'var(--theme-ui-colors-panel-text)',
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: 'var(--theme-ui-colors-panel-background)',
94
+ backgroundColor: 'panel-background',
90
95
  boxShadow: 'none',
91
- color: 'var(--theme-ui-colors-panel-text)',
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
- background: '#fdf8f5',
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
- background: '#14141F',
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')