@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
|
@@ -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,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', () => {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chronogroveThemeSurfaceColorsDark,
|
|
3
|
+
chronogroveThemeSurfaceColorsLight
|
|
4
|
+
} from '../chronogrove-theme-surface-colors.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Minimal Theme UI–shaped theme object (only `colors`) for RSC / SSR `<head>` scripts.
|
|
8
|
+
* No imports from `theme-ui` or the full `@chronogrove/ui/theme` module — those pull in React
|
|
9
|
+
* `createContext` and cannot load in Next.js Server Components.
|
|
10
|
+
*
|
|
11
|
+
* Surface literals come from `chronogrove-theme-surface-colors.js`, same as `theme.js`.
|
|
12
|
+
*/
|
|
13
|
+
export const chronogroveHeadTheme = {
|
|
14
|
+
colors: {
|
|
15
|
+
...chronogroveThemeSurfaceColorsLight,
|
|
16
|
+
modes: {
|
|
17
|
+
dark: {
|
|
18
|
+
...chronogroveThemeSurfaceColorsDark
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chronogroveThemeSurfaceColorsDark,
|
|
3
|
+
chronogroveThemeSurfaceColorsLight
|
|
4
|
+
} from '../chronogrove-theme-surface-colors.js'
|
|
5
|
+
|
|
1
6
|
import { THEME_UI_COLOR_MODE_STORAGE_KEY } from './constants.js'
|
|
2
7
|
|
|
3
8
|
function q(str) {
|
|
@@ -60,13 +65,43 @@ export function buildHtmlBackgroundInlineScript({
|
|
|
60
65
|
`
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
const surfaceLight = chronogroveThemeSurfaceColorsLight
|
|
69
|
+
const surfaceDark = chronogroveThemeSurfaceColorsDark
|
|
70
|
+
|
|
63
71
|
export function buildThemeUiColorModeFallbackCss({
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
defaultBackgroundHex = surfaceLight.background,
|
|
73
|
+
darkBackgroundHex = surfaceDark.background,
|
|
74
|
+
defaultTextHex = surfaceLight.text,
|
|
75
|
+
defaultTextMutedHex = surfaceLight.textMuted,
|
|
76
|
+
darkTextHex = surfaceDark.text,
|
|
77
|
+
darkTextMutedHex = surfaceDark.textMuted,
|
|
78
|
+
defaultPanelBackground = surfaceLight['panel-background'],
|
|
79
|
+
darkPanelBackground = surfaceDark['panel-background'],
|
|
80
|
+
defaultPanelText = surfaceLight.text,
|
|
81
|
+
darkPanelText = surfaceDark.text
|
|
82
|
+
} = {}) {
|
|
83
|
+
/**
|
|
84
|
+
* Base `:root` (light) must not depend on `data-theme-ui-color-mode`. In App Router SSR / first
|
|
85
|
+
* paint, that attribute is not set until the inline no-flash script runs—rules that only target
|
|
86
|
+
* `:root[data-theme-ui-color-mode="default"]` would leave `--theme-ui-colors-panel-background`
|
|
87
|
+
* (and glass panels using `bg: 'panel-background'`) unset until hydration.
|
|
88
|
+
*/
|
|
69
89
|
return `
|
|
90
|
+
:root {
|
|
91
|
+
--theme-ui-colors-background: ${defaultBackgroundHex} !important;
|
|
92
|
+
--theme-ui-colors-panel-background: ${defaultPanelBackground} !important;
|
|
93
|
+
--theme-ui-colors-panel-text: ${defaultPanelText} !important;
|
|
94
|
+
--theme-ui-colors-text: ${defaultTextHex};
|
|
95
|
+
--theme-ui-colors-text-muted: ${defaultTextMutedHex};
|
|
96
|
+
}
|
|
97
|
+
:root[data-theme-ui-color-mode="dark"],
|
|
98
|
+
html.theme-ui-dark {
|
|
99
|
+
--theme-ui-colors-background: ${darkBackgroundHex} !important;
|
|
100
|
+
--theme-ui-colors-panel-background: ${darkPanelBackground} !important;
|
|
101
|
+
--theme-ui-colors-panel-text: ${darkPanelText} !important;
|
|
102
|
+
--theme-ui-colors-text: ${darkTextHex};
|
|
103
|
+
--theme-ui-colors-text-muted: ${darkTextMutedHex};
|
|
104
|
+
}
|
|
70
105
|
:root[data-theme-ui-color-mode="default"], :root[data-theme-ui-color-mode="default"] * { --theme-ui-colors-text: ${defaultTextHex} !important; --theme-ui-colors-text-muted: ${defaultTextMutedHex} !important; }
|
|
71
106
|
:root[data-theme-ui-color-mode="dark"], :root[data-theme-ui-color-mode="dark"] * { --theme-ui-colors-text: ${darkTextHex} !important; --theme-ui-colors-text-muted: ${darkTextMutedHex} !important; }
|
|
72
107
|
`.trim()
|