@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,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import { render, waitFor, fireEvent } from '@testing-library/react'
|
|
7
|
+
import { ThemeUIProvider, useColorMode, useThemeUI } from 'theme-ui'
|
|
8
|
+
|
|
9
|
+
import * as colorUtils from '../color-utils.js'
|
|
10
|
+
|
|
11
|
+
jest.mock('theme-ui', () => ({
|
|
12
|
+
...jest.requireActual('theme-ui'),
|
|
13
|
+
useColorMode: jest.fn(),
|
|
14
|
+
useThemeUI: jest.fn()
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
import chronogroveTheme from '../theme.js'
|
|
18
|
+
import ChronogroveAnimatedPageBackground from './ChronogroveAnimatedPageBackground.js'
|
|
19
|
+
|
|
20
|
+
jest.mock('./ColorBends.js', () => {
|
|
21
|
+
return function MockColorBends() {
|
|
22
|
+
return <div data-testid='mock-color-bends' />
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const realHexToRgba = jest.requireActual('../color-utils.js').hexToRgba
|
|
27
|
+
|
|
28
|
+
function setScrollY(y) {
|
|
29
|
+
Object.defineProperty(window, 'scrollY', { configurable: true, value: y, writable: true })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('ChronogroveAnimatedPageBackground', () => {
|
|
33
|
+
let hexToRgbaSpy
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
window.localStorage.removeItem('theme-ui-color-mode')
|
|
37
|
+
Object.defineProperty(document.documentElement, 'scrollHeight', {
|
|
38
|
+
configurable: true,
|
|
39
|
+
value: 2000
|
|
40
|
+
})
|
|
41
|
+
Object.defineProperty(window, 'innerHeight', { configurable: true, value: 800 })
|
|
42
|
+
setScrollY(0)
|
|
43
|
+
hexToRgbaSpy = jest.spyOn(colorUtils, 'hexToRgba').mockImplementation((hex, alpha) => realHexToRgba(hex, alpha))
|
|
44
|
+
useColorMode.mockReturnValue(['default'])
|
|
45
|
+
useThemeUI.mockReturnValue({ theme: chronogroveTheme })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
hexToRgbaSpy.mockRestore()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
function renderBackground(ui, theme = chronogroveTheme) {
|
|
53
|
+
useThemeUI.mockReturnValue({ theme })
|
|
54
|
+
return render(<ThemeUIProvider theme={theme}>{ui}</ThemeUIProvider>)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
it('renders fixed backdrop and overlay layers', async () => {
|
|
58
|
+
const { container, rerender } = renderBackground(<ChronogroveAnimatedPageBackground />)
|
|
59
|
+
|
|
60
|
+
await waitFor(() => {
|
|
61
|
+
expect(container.querySelectorAll('[aria-hidden="true"]')).toHaveLength(2)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
rerender(
|
|
65
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
66
|
+
<ChronogroveAnimatedPageBackground
|
|
67
|
+
overlayHeight='200px'
|
|
68
|
+
darkOpacity={0.2}
|
|
69
|
+
fadeDistance={500}
|
|
70
|
+
maxParallaxOffset={80}
|
|
71
|
+
/>
|
|
72
|
+
</ThemeUIProvider>
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
await waitFor(() => {
|
|
76
|
+
expect(container.querySelectorAll('[aria-hidden="true"]')).toHaveLength(2)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('shows ColorBends in dark mode', async () => {
|
|
81
|
+
useColorMode.mockReturnValue(['dark'])
|
|
82
|
+
const { getByTestId } = renderBackground(<ChronogroveAnimatedPageBackground />)
|
|
83
|
+
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
expect(getByTestId('mock-color-bends')).toBeInTheDocument()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('uses dark fallback hex when gradient resolves a CSS var in dark mode', async () => {
|
|
90
|
+
useColorMode.mockReturnValue(['dark'])
|
|
91
|
+
const varTheme = {
|
|
92
|
+
rawColors: { background: 'var(--page-bg)' },
|
|
93
|
+
colors: { background: '#ffffff' }
|
|
94
|
+
}
|
|
95
|
+
renderBackground(<ChronogroveAnimatedPageBackground />, varTheme)
|
|
96
|
+
|
|
97
|
+
await waitFor(() => {
|
|
98
|
+
expect(hexToRgbaSpy).toHaveBeenCalledWith('#14141F', 0.6)
|
|
99
|
+
expect(hexToRgbaSpy).toHaveBeenCalledWith('#14141F', 0.2)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('uses light fallback hex when gradient resolves a CSS var in light mode', async () => {
|
|
104
|
+
useColorMode.mockReturnValue(['default'])
|
|
105
|
+
const varTheme = {
|
|
106
|
+
rawColors: { background: 'var(--page-bg)' },
|
|
107
|
+
colors: { background: '#ffffff' }
|
|
108
|
+
}
|
|
109
|
+
renderBackground(<ChronogroveAnimatedPageBackground />, varTheme)
|
|
110
|
+
|
|
111
|
+
await waitFor(() => {
|
|
112
|
+
expect(hexToRgbaSpy).toHaveBeenCalledWith('#fdf8f5', 0.6)
|
|
113
|
+
expect(hexToRgbaSpy).toHaveBeenCalledWith('#fdf8f5', 0.2)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('falls back to surface hex when theme omits background tokens', async () => {
|
|
118
|
+
useColorMode.mockReturnValue(['dark'])
|
|
119
|
+
renderBackground(<ChronogroveAnimatedPageBackground />, {})
|
|
120
|
+
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
expect(hexToRgbaSpy).toHaveBeenCalled()
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('prefers colors.background when rawColors are absent', async () => {
|
|
127
|
+
useColorMode.mockReturnValue(['default'])
|
|
128
|
+
renderBackground(<ChronogroveAnimatedPageBackground />, {
|
|
129
|
+
colors: { background: '#aabbcc' }
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
expect(hexToRgbaSpy).toHaveBeenCalledWith('#aabbcc', 0.6)
|
|
134
|
+
expect(hexToRgbaSpy).toHaveBeenCalledWith('#aabbcc', 0.2)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('prefers rawColors.background over colors.background', async () => {
|
|
139
|
+
useColorMode.mockReturnValue(['default'])
|
|
140
|
+
renderBackground(<ChronogroveAnimatedPageBackground />, {
|
|
141
|
+
rawColors: { background: '#112233' },
|
|
142
|
+
colors: { background: '#eeeeee' }
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
await waitFor(() => {
|
|
146
|
+
expect(hexToRgbaSpy).toHaveBeenCalledWith('#112233', 0.6)
|
|
147
|
+
expect(hexToRgbaSpy).toHaveBeenCalledWith('#112233', 0.2)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('falls through to colors.background when raw background is null', async () => {
|
|
152
|
+
useColorMode.mockReturnValue(['default'])
|
|
153
|
+
renderBackground(<ChronogroveAnimatedPageBackground />, {
|
|
154
|
+
rawColors: { background: null },
|
|
155
|
+
colors: { background: '#ccddee' }
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(hexToRgbaSpy).toHaveBeenCalledWith('#ccddee', 0.6)
|
|
160
|
+
expect(hexToRgbaSpy).toHaveBeenCalledWith('#ccddee', 0.2)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('uses built-in fallbacks when theme is undefined', async () => {
|
|
165
|
+
useColorMode.mockReturnValue(['default'])
|
|
166
|
+
useThemeUI.mockReturnValue({ theme: undefined })
|
|
167
|
+
const { container } = render(
|
|
168
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
169
|
+
<ChronogroveAnimatedPageBackground />
|
|
170
|
+
</ThemeUIProvider>
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
expect(container.querySelectorAll('[aria-hidden="true"]')).toHaveLength(2)
|
|
175
|
+
expect(hexToRgbaSpy).toHaveBeenCalled()
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('updates overlay on scroll', async () => {
|
|
180
|
+
const { container } = renderBackground(<ChronogroveAnimatedPageBackground fadeDistance={100} />)
|
|
181
|
+
|
|
182
|
+
await waitFor(() => {
|
|
183
|
+
expect(container.querySelectorAll('[aria-hidden="true"]')).toHaveLength(2)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
setScrollY(50)
|
|
187
|
+
fireEvent.scroll(window)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
import './color-bends.css'
|
|
5
|
+
|
|
6
|
+
const MAX_COLORS = 8
|
|
7
|
+
|
|
8
|
+
const frag = `
|
|
9
|
+
#define MAX_COLORS ${MAX_COLORS}
|
|
10
|
+
uniform vec2 uCanvas;
|
|
11
|
+
uniform float uTime;
|
|
12
|
+
uniform float uSpeed;
|
|
13
|
+
uniform vec2 uRot;
|
|
14
|
+
uniform int uColorCount;
|
|
15
|
+
uniform vec3 uColors[MAX_COLORS];
|
|
16
|
+
uniform int uTransparent;
|
|
17
|
+
uniform float uScale;
|
|
18
|
+
uniform float uFrequency;
|
|
19
|
+
uniform float uWarpStrength;
|
|
20
|
+
uniform vec2 uPointer; // in NDC [-1,1]
|
|
21
|
+
uniform float uMouseInfluence;
|
|
22
|
+
uniform float uParallax;
|
|
23
|
+
uniform float uNoise;
|
|
24
|
+
varying vec2 vUv;
|
|
25
|
+
|
|
26
|
+
void main() {
|
|
27
|
+
float t = uTime * uSpeed;
|
|
28
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
29
|
+
p += uPointer * uParallax * 0.1;
|
|
30
|
+
vec2 rp = vec2(p.x * uRot.x - p.y * uRot.y, p.x * uRot.y + p.y * uRot.x);
|
|
31
|
+
vec2 q = vec2(rp.x * (uCanvas.x / uCanvas.y), rp.y);
|
|
32
|
+
q /= max(uScale, 0.0001);
|
|
33
|
+
q /= 0.5 + 0.2 * dot(q, q);
|
|
34
|
+
q += 0.2 * cos(t) - 7.56;
|
|
35
|
+
vec2 toward = (uPointer - rp);
|
|
36
|
+
q += toward * uMouseInfluence * 0.2;
|
|
37
|
+
|
|
38
|
+
vec3 col = vec3(0.0);
|
|
39
|
+
float a = 1.0;
|
|
40
|
+
|
|
41
|
+
if (uColorCount > 0) {
|
|
42
|
+
vec2 s = q;
|
|
43
|
+
vec3 sumCol = vec3(0.0);
|
|
44
|
+
float cover = 0.0;
|
|
45
|
+
|
|
46
|
+
for (int i = 0; i < MAX_COLORS; ++i) {
|
|
47
|
+
if (i >= uColorCount) break;
|
|
48
|
+
s -= 0.01;
|
|
49
|
+
vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));
|
|
50
|
+
float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(i)) / 4.0);
|
|
51
|
+
float kBelow = clamp(uWarpStrength, 0.0, 1.0);
|
|
52
|
+
float kMix = pow(kBelow, 0.3); // strong response across 0..1
|
|
53
|
+
float gain = 1.0 + max(uWarpStrength - 1.0, 0.0); // allow >1 to amplify displacement
|
|
54
|
+
vec2 disp = (r - s) * kBelow;
|
|
55
|
+
vec2 warped = s + disp * gain;
|
|
56
|
+
float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(i)) / 4.0);
|
|
57
|
+
float m = mix(m0, m1, kMix);
|
|
58
|
+
float w = 1.0 - exp(-6.0 / exp(6.0 * m));
|
|
59
|
+
sumCol += uColors[i] * w;
|
|
60
|
+
cover = max(cover, w);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
col = clamp(sumCol, 0.0, 1.0);
|
|
64
|
+
a = uTransparent > 0 ? cover : 1.0;
|
|
65
|
+
} else {
|
|
66
|
+
vec2 s = q;
|
|
67
|
+
for (int k = 0; k < 3; ++k) {
|
|
68
|
+
s -= 0.01;
|
|
69
|
+
vec2 r = sin(1.5 * (s.yx * uFrequency) + 2.0 * cos(s * uFrequency));
|
|
70
|
+
float m0 = length(r + sin(5.0 * r.y * uFrequency - 3.0 * t + float(k)) / 4.0);
|
|
71
|
+
float kBelow = clamp(uWarpStrength, 0.0, 1.0);
|
|
72
|
+
float kMix = pow(kBelow, 0.3);
|
|
73
|
+
float gain = 1.0 + max(uWarpStrength - 1.0, 0.0);
|
|
74
|
+
vec2 disp = (r - s) * kBelow;
|
|
75
|
+
vec2 warped = s + disp * gain;
|
|
76
|
+
float m1 = length(warped + sin(5.0 * warped.y * uFrequency - 3.0 * t + float(k)) / 4.0);
|
|
77
|
+
float m = mix(m0, m1, kMix);
|
|
78
|
+
col[k] = 1.0 - exp(-6.0 / exp(6.0 * m));
|
|
79
|
+
}
|
|
80
|
+
a = uTransparent > 0 ? max(max(col.r, col.g), col.b) : 1.0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (uNoise > 0.0001) {
|
|
84
|
+
float n = fract(sin(dot(gl_FragCoord.xy + vec2(uTime), vec2(12.9898, 78.233))) * 43758.5453123);
|
|
85
|
+
col += (n - 0.5) * uNoise;
|
|
86
|
+
col = clamp(col, 0.0, 1.0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
vec3 rgb = (uTransparent > 0) ? col * a : col;
|
|
90
|
+
gl_FragColor = vec4(rgb, a);
|
|
91
|
+
}
|
|
92
|
+
`
|
|
93
|
+
|
|
94
|
+
const vert = `
|
|
95
|
+
varying vec2 vUv;
|
|
96
|
+
void main() {
|
|
97
|
+
vUv = uv;
|
|
98
|
+
gl_Position = vec4(position, 1.0);
|
|
99
|
+
}
|
|
100
|
+
`
|
|
101
|
+
|
|
102
|
+
export default function ColorBends({
|
|
103
|
+
className,
|
|
104
|
+
style,
|
|
105
|
+
rotation = 45,
|
|
106
|
+
speed = 0.2,
|
|
107
|
+
colors = [],
|
|
108
|
+
transparent = true,
|
|
109
|
+
autoRotate = 0,
|
|
110
|
+
scale = 1,
|
|
111
|
+
frequency = 1,
|
|
112
|
+
warpStrength = 1,
|
|
113
|
+
mouseInfluence = 1,
|
|
114
|
+
parallax = 0.5,
|
|
115
|
+
noise = 0.1
|
|
116
|
+
}) {
|
|
117
|
+
const containerRef = useRef(null)
|
|
118
|
+
const rendererRef = useRef(null)
|
|
119
|
+
const rafRef = useRef(null)
|
|
120
|
+
const materialRef = useRef(null)
|
|
121
|
+
const resizeObserverRef = useRef(null)
|
|
122
|
+
const rotationRef = useRef(rotation)
|
|
123
|
+
const autoRotateRef = useRef(autoRotate)
|
|
124
|
+
const pointerTargetRef = useRef(new THREE.Vector2(0, 0))
|
|
125
|
+
const pointerCurrentRef = useRef(new THREE.Vector2(0, 0))
|
|
126
|
+
const pointerSmoothRef = useRef(8)
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const container = containerRef.current
|
|
130
|
+
if (!container) return
|
|
131
|
+
|
|
132
|
+
const scene = new THREE.Scene()
|
|
133
|
+
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
|
|
134
|
+
const geometry = new THREE.PlaneGeometry(2, 2)
|
|
135
|
+
const uColorsArray = Array.from({ length: MAX_COLORS }, () => new THREE.Vector3(0, 0, 0))
|
|
136
|
+
|
|
137
|
+
const material = new THREE.ShaderMaterial({
|
|
138
|
+
vertexShader: vert,
|
|
139
|
+
fragmentShader: frag,
|
|
140
|
+
uniforms: {
|
|
141
|
+
uCanvas: { value: new THREE.Vector2(1, 1) },
|
|
142
|
+
uTime: { value: 0 },
|
|
143
|
+
uSpeed: { value: speed },
|
|
144
|
+
uRot: { value: new THREE.Vector2(1, 0) },
|
|
145
|
+
uColorCount: { value: 0 },
|
|
146
|
+
uColors: { value: uColorsArray },
|
|
147
|
+
uTransparent: { value: transparent ? 1 : 0 },
|
|
148
|
+
uScale: { value: scale },
|
|
149
|
+
uFrequency: { value: frequency },
|
|
150
|
+
uWarpStrength: { value: warpStrength },
|
|
151
|
+
uPointer: { value: new THREE.Vector2(0, 0) },
|
|
152
|
+
uMouseInfluence: { value: mouseInfluence },
|
|
153
|
+
uParallax: { value: parallax },
|
|
154
|
+
uNoise: { value: noise }
|
|
155
|
+
},
|
|
156
|
+
premultipliedAlpha: true,
|
|
157
|
+
transparent: true
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
materialRef.current = material
|
|
161
|
+
|
|
162
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
163
|
+
scene.add(mesh)
|
|
164
|
+
|
|
165
|
+
const renderer = new THREE.WebGLRenderer({
|
|
166
|
+
antialias: false,
|
|
167
|
+
powerPreference: 'high-performance',
|
|
168
|
+
alpha: true
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
rendererRef.current = renderer
|
|
172
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace
|
|
173
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
|
|
174
|
+
renderer.setClearColor(0x000000, transparent ? 0 : 1)
|
|
175
|
+
renderer.domElement.style.width = '100%'
|
|
176
|
+
renderer.domElement.style.height = '100%'
|
|
177
|
+
renderer.domElement.style.display = 'block'
|
|
178
|
+
|
|
179
|
+
container.appendChild(renderer.domElement)
|
|
180
|
+
|
|
181
|
+
const clock = new THREE.Clock()
|
|
182
|
+
|
|
183
|
+
const handleResize = () => {
|
|
184
|
+
const w = container.clientWidth || 1
|
|
185
|
+
const h = container.clientHeight || 1
|
|
186
|
+
renderer.setSize(w, h, false)
|
|
187
|
+
material.uniforms.uCanvas.value.set(w, h)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
handleResize()
|
|
191
|
+
|
|
192
|
+
if ('ResizeObserver' in window) {
|
|
193
|
+
const ro = new ResizeObserver(handleResize)
|
|
194
|
+
ro.observe(container)
|
|
195
|
+
resizeObserverRef.current = ro
|
|
196
|
+
} else {
|
|
197
|
+
window.addEventListener('resize', handleResize)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const loop = () => {
|
|
201
|
+
const dt = clock.getDelta()
|
|
202
|
+
const elapsed = clock.elapsedTime
|
|
203
|
+
|
|
204
|
+
material.uniforms.uTime.value = elapsed
|
|
205
|
+
|
|
206
|
+
const deg = (rotationRef.current % 360) + autoRotateRef.current * elapsed
|
|
207
|
+
const rad = (deg * Math.PI) / 180
|
|
208
|
+
const c = Math.cos(rad)
|
|
209
|
+
const s = Math.sin(rad)
|
|
210
|
+
material.uniforms.uRot.value.set(c, s)
|
|
211
|
+
|
|
212
|
+
const cur = pointerCurrentRef.current
|
|
213
|
+
const tgt = pointerTargetRef.current
|
|
214
|
+
const amt = Math.min(1, dt * pointerSmoothRef.current)
|
|
215
|
+
cur.lerp(tgt, amt)
|
|
216
|
+
material.uniforms.uPointer.value.copy(cur)
|
|
217
|
+
|
|
218
|
+
renderer.render(scene, camera)
|
|
219
|
+
rafRef.current = requestAnimationFrame(loop)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
rafRef.current = requestAnimationFrame(loop)
|
|
223
|
+
|
|
224
|
+
return () => {
|
|
225
|
+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
|
|
226
|
+
if (resizeObserverRef.current) resizeObserverRef.current.disconnect()
|
|
227
|
+
else window.removeEventListener('resize', handleResize)
|
|
228
|
+
geometry.dispose()
|
|
229
|
+
material.dispose()
|
|
230
|
+
renderer.dispose()
|
|
231
|
+
if (renderer.domElement && renderer.domElement.parentElement === container) {
|
|
232
|
+
container.removeChild(renderer.domElement)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}, [frequency, mouseInfluence, noise, parallax, scale, speed, transparent, warpStrength])
|
|
236
|
+
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
const material = materialRef.current
|
|
239
|
+
const renderer = rendererRef.current
|
|
240
|
+
if (!material) return
|
|
241
|
+
|
|
242
|
+
rotationRef.current = rotation
|
|
243
|
+
autoRotateRef.current = autoRotate
|
|
244
|
+
material.uniforms.uSpeed.value = speed
|
|
245
|
+
material.uniforms.uScale.value = scale
|
|
246
|
+
material.uniforms.uFrequency.value = frequency
|
|
247
|
+
material.uniforms.uWarpStrength.value = warpStrength
|
|
248
|
+
material.uniforms.uMouseInfluence.value = mouseInfluence
|
|
249
|
+
material.uniforms.uParallax.value = parallax
|
|
250
|
+
material.uniforms.uNoise.value = noise
|
|
251
|
+
|
|
252
|
+
const toVec3 = hex => {
|
|
253
|
+
const h = hex.replace('#', '').trim()
|
|
254
|
+
const v =
|
|
255
|
+
h.length === 3
|
|
256
|
+
? [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)]
|
|
257
|
+
: [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]
|
|
258
|
+
return new THREE.Vector3(v[0] / 255, v[1] / 255, v[2] / 255)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const arr = (colors || [])
|
|
262
|
+
.filter(color => typeof color === 'string')
|
|
263
|
+
.slice(0, MAX_COLORS)
|
|
264
|
+
.map(toVec3)
|
|
265
|
+
|
|
266
|
+
for (let i = 0; i < MAX_COLORS; i++) {
|
|
267
|
+
const vec = material.uniforms.uColors.value[i]
|
|
268
|
+
if (i < arr.length) vec.copy(arr[i])
|
|
269
|
+
else vec.set(0, 0, 0)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
material.uniforms.uColorCount.value = arr.length
|
|
273
|
+
material.uniforms.uTransparent.value = transparent ? 1 : 0
|
|
274
|
+
if (renderer) renderer.setClearColor(0x000000, transparent ? 0 : 1)
|
|
275
|
+
}, [
|
|
276
|
+
rotation,
|
|
277
|
+
autoRotate,
|
|
278
|
+
speed,
|
|
279
|
+
scale,
|
|
280
|
+
frequency,
|
|
281
|
+
warpStrength,
|
|
282
|
+
mouseInfluence,
|
|
283
|
+
parallax,
|
|
284
|
+
noise,
|
|
285
|
+
colors,
|
|
286
|
+
transparent
|
|
287
|
+
])
|
|
288
|
+
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
const material = materialRef.current
|
|
291
|
+
const container = containerRef.current
|
|
292
|
+
if (!material || !container) return
|
|
293
|
+
|
|
294
|
+
const handlePointerMove = e => {
|
|
295
|
+
const rect = container.getBoundingClientRect()
|
|
296
|
+
const x = ((e.clientX - rect.left) / (rect.width || 1)) * 2 - 1
|
|
297
|
+
const y = -(((e.clientY - rect.top) / (rect.height || 1)) * 2 - 1)
|
|
298
|
+
pointerTargetRef.current.set(x, y)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
container.addEventListener('pointermove', handlePointerMove)
|
|
302
|
+
|
|
303
|
+
return () => {
|
|
304
|
+
container.removeEventListener('pointermove', handlePointerMove)
|
|
305
|
+
}
|
|
306
|
+
}, [])
|
|
307
|
+
|
|
308
|
+
return <div ref={containerRef} className={`color-bends-container ${className || ''}`} style={style} />
|
|
309
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
jest.mock('./ColorBends.js', () => {
|
|
6
|
+
return function MockColorBends() {
|
|
7
|
+
return null
|
|
8
|
+
}
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
import DefaultBackground, { ChronogroveAnimatedPageBackground } from './index.js'
|
|
12
|
+
|
|
13
|
+
describe('animated-page-background barrel', () => {
|
|
14
|
+
it('re-exports the animated background as default and named export', () => {
|
|
15
|
+
expect(ChronogroveAnimatedPageBackground).toEqual(expect.any(Function))
|
|
16
|
+
expect(DefaultBackground).toBe(ChronogroveAnimatedPageBackground)
|
|
17
|
+
})
|
|
18
|
+
})
|
package/src/button.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box } from '@theme-ui/components'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Styled label for post categories. Pass display text as `children` (site-specific mapping stays in the app).
|
|
6
|
+
*/
|
|
7
|
+
const CategoryLabel = ({ children, sx: sxProp = {}, ...props }) => (
|
|
8
|
+
<Box
|
|
9
|
+
sx={{
|
|
10
|
+
display: 'inline-block',
|
|
11
|
+
fontSize: [0],
|
|
12
|
+
fontFamily: 'heading',
|
|
13
|
+
color: 'primary',
|
|
14
|
+
letterSpacing: '0.05em',
|
|
15
|
+
...sxProp
|
|
16
|
+
}}
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</Box>
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export default CategoryLabel
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { ThemeUIProvider } from 'theme-ui'
|
|
3
|
+
|
|
4
|
+
import CategoryLabel from './category-label.js'
|
|
5
|
+
|
|
6
|
+
const theme = { colors: { primary: '#422EA3' } }
|
|
7
|
+
|
|
8
|
+
const renderWithTheme = ui => render(<ThemeUIProvider theme={theme}>{ui}</ThemeUIProvider>)
|
|
9
|
+
|
|
10
|
+
describe('CategoryLabel', () => {
|
|
11
|
+
it('renders children', () => {
|
|
12
|
+
renderWithTheme(<CategoryLabel>Travel</CategoryLabel>)
|
|
13
|
+
expect(screen.getByText('Travel')).toBeInTheDocument()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('merges sx', () => {
|
|
17
|
+
renderWithTheme(
|
|
18
|
+
<CategoryLabel sx={{ color: 'red' }} data-testid='cat'>
|
|
19
|
+
X
|
|
20
|
+
</CategoryLabel>
|
|
21
|
+
)
|
|
22
|
+
expect(screen.getByTestId('cat')).toBeInTheDocument()
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
/** Chevron left (inline SVG, no icon font). */
|
|
4
|
+
export const ChevronLeftIcon = props => (
|
|
5
|
+
<svg
|
|
6
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
7
|
+
viewBox='0 0 320 512'
|
|
8
|
+
width='1em'
|
|
9
|
+
height='1em'
|
|
10
|
+
aria-hidden='true'
|
|
11
|
+
focusable='false'
|
|
12
|
+
{...props}
|
|
13
|
+
>
|
|
14
|
+
<path
|
|
15
|
+
fill='currentColor'
|
|
16
|
+
d='M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z'
|
|
17
|
+
/>
|
|
18
|
+
</svg>
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
/** Chevron right (inline SVG, no icon font). */
|
|
22
|
+
export const ChevronRightIcon = props => (
|
|
23
|
+
<svg
|
|
24
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
25
|
+
viewBox='0 0 320 512'
|
|
26
|
+
width='1em'
|
|
27
|
+
height='1em'
|
|
28
|
+
aria-hidden='true'
|
|
29
|
+
focusable='false'
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
<path
|
|
33
|
+
fill='currentColor'
|
|
34
|
+
d='M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z'
|
|
35
|
+
/>
|
|
36
|
+
</svg>
|
|
37
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical string colors for page surface + frosted panels (default + dark mode).
|
|
3
|
+
*
|
|
4
|
+
* Imported by `theme.js` (full Theme UI merge) and `color-mode/chronogrove-head-theme.js`
|
|
5
|
+
* (RSC-safe head theme) so SSR inline CSS and the client Theme UI theme stay aligned.
|
|
6
|
+
*
|
|
7
|
+
* This module must stay free of `theme-ui` / `@theme-ui/presets` / React so it can load in
|
|
8
|
+
* Next.js Server Components.
|
|
9
|
+
*/
|
|
10
|
+
export const chronogroveThemeSurfaceColorsLight = {
|
|
11
|
+
background: '#fdf8f5',
|
|
12
|
+
text: '#111',
|
|
13
|
+
textMuted: '#333',
|
|
14
|
+
'panel-background': 'rgba(255, 255, 255, 0.45)'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const chronogroveThemeSurfaceColorsDark = {
|
|
18
|
+
background: '#14141F',
|
|
19
|
+
text: '#fff',
|
|
20
|
+
textMuted: '#d8d8d8',
|
|
21
|
+
'panel-background': 'rgba(20, 20, 31, 0.45)'
|
|
22
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { chronogroveHeadTheme } from './chronogrove-head-theme.js'
|
|
1
2
|
import { THEME_UI_COLOR_MODE_STORAGE_KEY } from './constants.js'
|
|
2
3
|
import { normalizeThemeUiColorMode } from './normalize.js'
|
|
4
|
+
import { resolveChronogroveSurfaceColors } from './resolve-theme-colors.js'
|
|
5
|
+
|
|
6
|
+
const SURFACE = resolveChronogroveSurfaceColors(chronogroveHeadTheme)
|
|
3
7
|
|
|
4
8
|
export function resolveThemeUiColorMode(storageKey = THEME_UI_COLOR_MODE_STORAGE_KEY) {
|
|
5
9
|
let mode
|
|
@@ -49,6 +53,9 @@ export function syncThemeUiColorMode(storageKey = THEME_UI_COLOR_MODE_STORAGE_KE
|
|
|
49
53
|
.forEach(className => htmlElement.classList.remove(className))
|
|
50
54
|
htmlElement.classList.add(`theme-ui-${mode}`)
|
|
51
55
|
htmlElement.setAttribute('data-theme-ui-color-mode', mode)
|
|
56
|
+
// Match `buildHtmlBackgroundInlineScript`: inline background must follow toggles; otherwise the
|
|
57
|
+
// initial head script’s color sticks and wins over Theme UI’s `--theme-ui-colors-background`.
|
|
58
|
+
htmlElement.style.backgroundColor = mode === 'dark' ? SURFACE.darkBackgroundHex : SURFACE.defaultBackgroundHex
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
export function scheduleThemeUiColorModeSync(storageKey = THEME_UI_COLOR_MODE_STORAGE_KEY) {
|
|
@@ -86,6 +86,13 @@ describe('syncThemeUiColorMode', () => {
|
|
|
86
86
|
syncThemeUiColorMode()
|
|
87
87
|
expect(document.documentElement.classList.contains('theme-ui-dark')).toBe(true)
|
|
88
88
|
expect(document.documentElement.getAttribute('data-theme-ui-color-mode')).toBe('dark')
|
|
89
|
+
expect(document.documentElement.style.backgroundColor).toBe('rgb(20, 20, 31)')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('sets html inline background for default mode', () => {
|
|
93
|
+
window.localStorage.setItem('theme-ui-color-mode', 'default')
|
|
94
|
+
syncThemeUiColorMode()
|
|
95
|
+
expect(document.documentElement.style.backgroundColor).toBe('rgb(253, 248, 245)')
|
|
89
96
|
})
|
|
90
97
|
|
|
91
98
|
it('strips prior theme-ui-* classes before applying', () => {
|