@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.
Files changed (72) hide show
  1. package/README.md +40 -19
  2. package/package.json +73 -6
  3. package/src/__snapshots__/header.spec.js.snap +8 -8
  4. package/src/__snapshots__/theme.spec.js.snap +39 -20
  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/category-label.js +23 -0
  17. package/src/category-label.spec.js +24 -0
  18. package/src/chevron-icons.js +37 -0
  19. package/src/chronogrove-theme-surface-colors.js +22 -0
  20. package/src/color-mode/browser-sync.js +7 -0
  21. package/src/color-mode/browser-sync.spec.js +7 -0
  22. package/src/color-mode/chronogrove-head-theme.js +22 -0
  23. package/src/color-mode/head-inline.js +40 -5
  24. package/src/color-mode/head-inline.spec.js +29 -0
  25. package/src/color-mode/index.js +3 -0
  26. package/src/color-mode/resolve-theme-colors.js +18 -6
  27. package/src/color-mode/resolve-theme-colors.spec.js +13 -3
  28. package/src/color-mode/spa-navigation.js +14 -0
  29. package/src/color-mode/spa-navigation.spec.js +25 -0
  30. package/src/color-mode/use-document-color-mode-surface.js +52 -0
  31. package/src/color-mode/use-document-color-mode-surface.node.spec.js +12 -0
  32. package/src/color-mode/use-document-color-mode-surface.spec.js +154 -0
  33. package/src/color-toggle-styles.css +10 -0
  34. package/src/color-toggle.js +11 -2
  35. package/src/emotion-cache.node.spec.js +13 -0
  36. package/src/emotion-cache.spec.js +12 -0
  37. package/src/external-link-icon.js +30 -0
  38. package/src/external-link-icon.spec.js +16 -0
  39. package/src/gatsby/build-theme-ui-color-mode-head-components.js +7 -1
  40. package/src/gatsby/index.spec.js +42 -0
  41. package/src/gatsby/on-route-update-color-mode.js +1 -14
  42. package/src/header.js +4 -16
  43. package/src/lazy-load.js +30 -11
  44. package/src/lazy-load.spec.js +9 -5
  45. package/src/metric-badge.js +10 -0
  46. package/src/metric-badge.spec.js +15 -0
  47. package/src/metric-card.js +95 -0
  48. package/src/metric-card.spec.js +60 -0
  49. package/src/muted-card-footer.js +22 -0
  50. package/src/muted-card-footer.spec.js +25 -0
  51. package/src/next/app-shell.js +34 -0
  52. package/src/next/emotion-registry.js +68 -0
  53. package/src/next/emotion-registry.spec.js +99 -0
  54. package/src/next/index.js +4 -0
  55. package/src/next/root-layout-head.js +42 -0
  56. package/src/next/root-layout-head.spec.js +17 -0
  57. package/src/next/theme-ui-color-mode-route-sync.js +32 -0
  58. package/src/page-backdrop.js +42 -0
  59. package/src/page-backdrop.spec.js +41 -0
  60. package/src/pagination-button.js +4 -4
  61. package/src/pagination-button.spec.js +26 -2
  62. package/src/pagination.js +198 -0
  63. package/src/pagination.spec.js +281 -0
  64. package/src/skip-nav/SkipNavLink.js +6 -5
  65. package/src/skip-nav/SkipNavLink.spec.js +11 -0
  66. package/src/status-card.js +18 -0
  67. package/src/status-card.spec.js +38 -0
  68. package/src/theme.js +27 -20
  69. package/src/widget-call-to-action.js +106 -0
  70. package/src/widget-call-to-action.spec.js +115 -0
  71. package/src/widget-section.js +83 -0
  72. package/src/widget-section.spec.js +59 -0
package/README.md CHANGED
@@ -18,28 +18,49 @@ Use **`pnpm publish`** for releases so `workspace:` dependencies in dependents a
18
18
 
19
19
  Prefer deep imports so bundles stay lean:
20
20
 
21
- | Import path | Contents |
22
- | ----------------------------------- | -------------------------------------------------------------------------------------------------- |
23
- | `@chronogrove/ui` | `ChronogroveThemeProvider` |
24
- | `@chronogrove/ui/theme` | Default Theme UI theme object + named exports |
25
- | `@chronogrove/ui/provider` | `ChronogroveThemeProvider` |
26
- | `@chronogrove/ui/color-mode` | Storage key, reconcile event, SSR inline builders, `resolveChronogroveSurfaceColors`, browser sync |
27
- | `@chronogrove/ui/emotion-cache` | `createChronogroveEmotionCache`, `getChronogroveEmotionCache` |
28
- | `@chronogrove/ui/button` | Theme UI `components` button |
29
- | `@chronogrove/ui/color-toggle` | Theme UI + `@theme-toggles/react` toggle |
30
- | `@chronogrove/ui/skip-nav` | `SkipNavLink`, `SkipNavContent` |
31
- | `@chronogrove/ui/is-dark-mode` | `colorMode === 'dark'` helper |
32
- | `@chronogrove/ui/color-utils` | `hexToRgb`, `hexToRgba`, `BUTTON_PRIMARY_COLORS` |
33
- | `@chronogrove/ui/action-button` | Outline CTA as `<button>` or `<a>` |
34
- | `@chronogrove/ui/pagination-button` | Compact paginator control |
35
- | `@chronogrove/ui/lazy-load` | Defer children until in viewport (`react-intersection-observer`) |
36
- | `@chronogrove/ui/header` | Masthead shell (`variant: styles.Header`) |
37
- | `@chronogrove/ui/page-header` | Blog-style `h1` heading (`p-name`) |
38
- | `@chronogrove/ui/gatsby` | Color-mode Gatsby SSR/browser helpers |
21
+ | Import path | Contents |
22
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
23
+ | `@chronogrove/ui` | `ChronogroveThemeProvider` |
24
+ | `@chronogrove/ui/theme` | Default Theme UI theme object + named exports |
25
+ | `@chronogrove/ui/provider` | `ChronogroveThemeProvider` |
26
+ | `@chronogrove/ui/color-mode` | Storage key, reconcile event, SSR inline builders, `chronogroveHeadTheme` (RSC-safe), `resolveChronogroveSurfaceColors`, `useDocumentColorModeSurface`, browser sync, `reconcileThemeUiColorModeOnNavigation` |
27
+ | `@chronogrove/ui/animated-page-background` | **`ChronogroveAnimatedPageBackground`** — same stack as the Gatsby home: fixed `z-index: 0`, light = solid theme background, dark = **three.js** Color Bends + scroll-linked gradient overlay and parallax (`three` is a dependency of this package). **Not** the same as `page-backdrop`. |
28
+ | `@chronogrove/ui/color-bends` | **`ColorBends`** lower-level three.js gradient background used inside `ChronogroveAnimatedPageBackground`. Prefer the full **`animated-page-background`** or **`@chronogrove/ui/next`** shell unless you need the raw component. |
29
+ | `@chronogrove/ui/page-backdrop` | **`ChronogrovePageBackdrop`** lightweight alternative: fixed `z-index: 0` fill without WebGL (CSS gradients in dark mode). Use when you cannot ship `three`. |
30
+ | `@chronogrove/ui/next` | **Next.js App Router helpers:** `ChronogroveNextRootLayoutHead` (RSC `<head>` injections), `ChronogroveNextEmotionRegistry`, `ChronogroveNextAppShell` (theme + three.js background + surface sync + soft-nav reconcile), `ChronogroveNextThemeUiColorModeRouteSync` (standalone). Requires `next` (peer, optional for the rest of the package). |
31
+ | `@chronogrove/ui/action-card-layout` | **`actionCardPinnedLayoutSx`** — layout `sx` for `Card variant="actionCard"` (matches GitHub pinned cards). |
32
+ | `@chronogrove/ui/emotion-cache` | `createChronogroveEmotionCache`, `getChronogroveEmotionCache` |
33
+ | `@chronogrove/ui/button` | Theme UI `components` button |
34
+ | `@chronogrove/ui/color-toggle` | Theme UI + `@theme-toggles/react` `Expand` |
35
+ | `@chronogrove/ui/color-toggle-styles` | CSS for the toggle (`Expand.css` + sizing); `@import` once in global CSS (see `examples/chronogrove-next/app/globals.css`) |
36
+ | `@chronogrove/ui/skip-nav` | `SkipNavLink`, `SkipNavContent` |
37
+ | `@chronogrove/ui/is-dark-mode` | `colorMode === 'dark'` helper |
38
+ | `@chronogrove/ui/color-utils` | `hexToRgb`, `hexToRgba`, `BUTTON_PRIMARY_COLORS` |
39
+ | `@chronogrove/ui/action-button` | Outline CTA as `<button>` or `<a>` |
40
+ | `@chronogrove/ui/pagination-button` | Compact paginator control |
41
+ | `@chronogrove/ui/lazy-load` | Defer children until in viewport (`react-intersection-observer`) |
42
+ | `@chronogrove/ui/header` | Masthead shell (`variant: styles.Header`) |
43
+ | `@chronogrove/ui/page-header` | Blog-style `h1` heading (`p-name`) |
44
+ | `@chronogrove/ui/gatsby` | Color-mode Gatsby SSR/browser helpers |
39
45
 
40
46
  ## Next.js (App Router)
41
47
 
42
- Anything using `useColorMode`, `useThemeUI`, or the provider must run in a **client** component (`'use client'`). Stack **Emotion** `CacheProvider` (with the `meta[name="emotion-insertion-point"]` pattern) **outside** `ChronogroveThemeProvider`. Detailed integration is tracked as follow-up work in the repo roadmap.
48
+ **Reference app:** [`examples/chronogrove-next`](../../examples/chronogrove-next) (`chronogrove-next`). Run `pnpm --filter chronogrove-next dev` from the repo root.
49
+
50
+ **Prefer `@chronogrove/ui/next`** for the standard wiring: `ChronogroveNextRootLayoutHead` in `<head>`, `ChronogroveNextEmotionRegistry` wrapping the body tree, and `ChronogroveNextAppShell` (or compose the lower-level exports yourself).
51
+
52
+ **Server vs client**
53
+
54
+ - **Server `layout`:** Keep the root layout a Server Component. **Do not** import `@chronogrove/ui/theme` here—it loads `theme-ui`’s `merge` and triggers React `createContext`, which Next.js disallows in RSC. Use **`ChronogroveNextRootLayoutHead`** from `@chronogrove/ui/next`, or manually pass **`chronogroveHeadTheme`** from `@chronogrove/ui/color-mode` to `resolveChronogroveSurfaceColors` and emit **in order**: `<meta name="emotion-insertion-point" content="" />`, then the Theme UI no-flash script, HTML background script, and fallback CSS (same composition as [`buildThemeUiColorModeHeadComponents`](./src/gatsby/build-theme-ui-color-mode-head-components.js) in `@chronogrove/ui/gatsby`).
55
+ - **Client Emotion registry + theme:** Use **`ChronogroveNextEmotionRegistry`** from `@chronogrove/ui/next`, or wrap the app in Emotion’s `CacheProvider` with a **per-request** cache with `key: 'css'` and Next’s `useServerInsertedHTML` ([Next.js: CSS-in-JS](https://nextjs.org/docs/app/building-your-application/styling/css-in-js)). **Do not** rely on `getChronogroveEmotionCache()` for SSR—it is a browser-oriented singleton. Import the **full** theme from `@chronogrove/ui/theme` only inside client components. Anything using `useColorMode`, `useThemeUI`, or `ChronogroveThemeProvider` must be `'use client'`. **Order:** `CacheProvider` (registry) **outside** `ChronogroveThemeProvider` **inside** `<body>`.
56
+ - **Document surface (match Gatsby `RootWrapper`):** **`ChronogroveNextAppShell`** calls **`useDocumentColorModeSurface`** from `@chronogrove/ui/color-mode` once. It syncs `document.documentElement`’s `theme-ui-*` class, `data-theme-ui-color-mode`, and inline page background from the **resolved** Theme UI theme (`rawColors` / `colors.background`). Without it, Emotion can win the cascade over head fallback CSS and panel tokens (`bg: 'panel-background'`) may not update in dark mode after hydration.
57
+ - **Animated page background (match Gatsby home):** **`ChronogroveNextAppShell`** includes **`ChronogroveAnimatedPageBackground`** (three.js Color Bends). For a **CSS-only** backdrop without `three`, use **`ChronogrovePageBackdrop`** from `@chronogrove/ui/page-backdrop` instead and compose your own shell. Content should sit in a **`position: relative; z-index: 1`** wrapper.
58
+ - **Soft navigations:** **`ChronogroveNextAppShell`** includes **`ChronogroveNextThemeUiColorModeRouteSync`**, which calls `reconcileThemeUiColorModeOnNavigation` after pathname changes (not on initial mount). Running reconcile on mount can fight `useColorMode` toggles. Gatsby’s equivalent is `onRouteUpdateThemeUiColorMode` (no initial `onRouteUpdate`).
59
+ - **Hydration:** Set `suppressHydrationWarning` on `<html>` and `<body>`. The inline no-flash / background scripts run before React hydrates and update `<html>` (`theme-ui-*` classes, `data-theme-ui-color-mode`, background), so the DOM no longer matches the server-rendered markup—React would warn without this flag (similar to [next-themes](https://github.com/pacocoursey/next-themes) on Next.js App Router).
60
+
61
+ **Imports:** Prefer `@chronogrove/ui/color-mode` for head builders and SPA reconcile. Reserve `@chronogrove/ui/gatsby` for Gatsby hooks (`onPreRenderHTML`, `onRenderBody` helpers).
62
+
63
+ **JSX + bundlers:** Primitives such as [`button`](./src/button.js) use [`Box`](https://theme-ui.com/components/box) from `@theme-ui/components` with an `as` prop for native elements, so `sx` works with Gatsby’s classic JSX runtime and Next’s SWC without a Theme UI file pragma. Jest uses [`babel.config.cjs`](./babel.config.cjs) (automatic JSX).
43
64
 
44
65
  ## Changelog
45
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chronogrove/ui",
3
- "version": "0.79.0",
3
+ "version": "0.81.0",
4
4
  "description": "Chronogrove Theme UI theme, color mode helpers, and shared UI primitives",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,7 +13,9 @@
13
13
  "url": "https://github.com/chrisvogt/gatsby-theme-chronogrove.git",
14
14
  "directory": "packages/ui"
15
15
  },
16
- "sideEffects": false,
16
+ "sideEffects": [
17
+ "**/*.css"
18
+ ],
17
19
  "exports": {
18
20
  ".": {
19
21
  "import": "./src/index.js",
@@ -35,6 +37,7 @@
35
37
  "import": "./src/color-toggle.js",
36
38
  "default": "./src/color-toggle.js"
37
39
  },
40
+ "./color-toggle-styles": "./src/color-toggle-styles.css",
38
41
  "./skip-nav": {
39
42
  "import": "./src/skip-nav/index.js",
40
43
  "default": "./src/skip-nav/index.js"
@@ -63,10 +66,50 @@
63
66
  "import": "./src/pagination-button.js",
64
67
  "default": "./src/pagination-button.js"
65
68
  },
69
+ "./pagination": {
70
+ "import": "./src/pagination.js",
71
+ "default": "./src/pagination.js"
72
+ },
73
+ "./category-label": {
74
+ "import": "./src/category-label.js",
75
+ "default": "./src/category-label.js"
76
+ },
77
+ "./external-link-icon": {
78
+ "import": "./src/external-link-icon.js",
79
+ "default": "./src/external-link-icon.js"
80
+ },
81
+ "./metric-badge": {
82
+ "import": "./src/metric-badge.js",
83
+ "default": "./src/metric-badge.js"
84
+ },
85
+ "./metric-card": {
86
+ "import": "./src/metric-card.js",
87
+ "default": "./src/metric-card.js"
88
+ },
89
+ "./muted-card-footer": {
90
+ "import": "./src/muted-card-footer.js",
91
+ "default": "./src/muted-card-footer.js"
92
+ },
93
+ "./status-card": {
94
+ "import": "./src/status-card.js",
95
+ "default": "./src/status-card.js"
96
+ },
97
+ "./widget-section": {
98
+ "import": "./src/widget-section.js",
99
+ "default": "./src/widget-section.js"
100
+ },
101
+ "./widget-call-to-action": {
102
+ "import": "./src/widget-call-to-action.js",
103
+ "default": "./src/widget-call-to-action.js"
104
+ },
66
105
  "./lazy-load": {
67
106
  "import": "./src/lazy-load.js",
68
107
  "default": "./src/lazy-load.js"
69
108
  },
109
+ "./next": {
110
+ "import": "./src/next/index.js",
111
+ "default": "./src/next/index.js"
112
+ },
70
113
  "./header": {
71
114
  "import": "./src/header.js",
72
115
  "default": "./src/header.js"
@@ -75,15 +118,37 @@
75
118
  "import": "./src/page-header.js",
76
119
  "default": "./src/page-header.js"
77
120
  },
121
+ "./page-backdrop": {
122
+ "import": "./src/page-backdrop.js",
123
+ "default": "./src/page-backdrop.js"
124
+ },
125
+ "./animated-page-background": {
126
+ "import": "./src/animated-page-background/index.js",
127
+ "default": "./src/animated-page-background/index.js"
128
+ },
129
+ "./color-bends": {
130
+ "import": "./src/animated-page-background/ColorBends.js",
131
+ "default": "./src/animated-page-background/ColorBends.js"
132
+ },
133
+ "./action-card-layout": {
134
+ "import": "./src/action-card-layout.js",
135
+ "default": "./src/action-card-layout.js"
136
+ },
78
137
  "./gatsby": {
79
138
  "import": "./src/gatsby/index.js",
80
139
  "default": "./src/gatsby/index.js"
81
140
  }
82
141
  },
83
142
  "peerDependencies": {
143
+ "next": "^14.0.0 || ^15.0.0",
84
144
  "react": "^18.0.0 || ^19.0.0",
85
145
  "react-dom": "^18.0.0 || ^19.0.0"
86
146
  },
147
+ "peerDependenciesMeta": {
148
+ "next": {
149
+ "optional": true
150
+ }
151
+ },
87
152
  "dependencies": {
88
153
  "@emotion/cache": "^11.14.0",
89
154
  "@emotion/react": "^11.14.0",
@@ -91,17 +156,19 @@
91
156
  "@theme-ui/components": "^0.17.4",
92
157
  "@theme-ui/presets": "^0.17.4",
93
158
  "react-intersection-observer": "^10.0.3",
94
- "theme-ui": "^0.17.4"
159
+ "theme-ui": "^0.17.4",
160
+ "three": "^0.183.2"
95
161
  },
96
162
  "devDependencies": {
97
- "@babel/core": "^7.28.5",
98
- "@babel/preset-env": "^7.28.5",
163
+ "@babel/core": "^7.29.0",
164
+ "@babel/preset-env": "^7.29.2",
99
165
  "@babel/preset-react": "^7.28.5",
100
166
  "@testing-library/jest-dom": "^6.9.1",
101
167
  "@testing-library/react": "^16.3.2",
102
- "babel-jest": "^30.0.5",
168
+ "babel-jest": "^30.3.0",
103
169
  "jest": "^30.3.0",
104
170
  "jest-environment-jsdom": "^30.3.0",
171
+ "next": "^15.1.0",
105
172
  "react": "^19.2.5",
106
173
  "react-dom": "^19.2.5"
107
174
  },
@@ -3,11 +3,11 @@
3
3
  exports[`Header renders with children 1`] = `
4
4
  <DocumentFragment>
5
5
  <header
6
- class="css-1u8qly9"
6
+ class="css-1yz7e9k"
7
7
  role="banner"
8
8
  >
9
9
  <div
10
- class="css-1u8qly9"
10
+ class="css-1yz7e9k"
11
11
  >
12
12
  <h1>
13
13
  Test Header
@@ -20,11 +20,11 @@ exports[`Header renders with children 1`] = `
20
20
  exports[`Header renders with custom styles 1`] = `
21
21
  <DocumentFragment>
22
22
  <header
23
- class="css-1u8qly9"
23
+ class="css-1yz7e9k"
24
24
  role="banner"
25
25
  >
26
26
  <div
27
- class="css-10n8us"
27
+ class="css-1g5wp3y"
28
28
  >
29
29
  <h1>
30
30
  Test Header with Styles
@@ -37,11 +37,11 @@ exports[`Header renders with custom styles 1`] = `
37
37
  exports[`Header renders with empty styles object 1`] = `
38
38
  <DocumentFragment>
39
39
  <header
40
- class="css-1u8qly9"
40
+ class="css-1yz7e9k"
41
41
  role="banner"
42
42
  >
43
43
  <div
44
- class="css-1u8qly9"
44
+ class="css-1yz7e9k"
45
45
  >
46
46
  <h1>
47
47
  Test Header with Empty Styles
@@ -54,11 +54,11 @@ exports[`Header renders with empty styles object 1`] = `
54
54
  exports[`Header renders without styles prop 1`] = `
55
55
  <DocumentFragment>
56
56
  <header
57
- class="css-1u8qly9"
57
+ class="css-1yz7e9k"
58
58
  role="banner"
59
59
  >
60
60
  <div
61
- class="css-1u8qly9"
61
+ class="css-1yz7e9k"
62
62
  >
63
63
  <h1>
64
64
  Test Header without Styles
@@ -130,12 +130,11 @@ exports[`Theme Configuration a snapshot of the configuration matches the snapsho
130
130
  },
131
131
  "WebkitBackdropFilter": "blur(10px)",
132
132
  "backdropFilter": "blur(10px)",
133
- "background": "var(--theme-ui-colors-panel-background)",
134
- "backgroundColor": "var(--theme-ui-colors-panel-background)",
133
+ "bg": "panel-background",
135
134
  "border": "1px solid rgba(255, 255, 255, 0.15)",
136
135
  "borderRadius": "10px",
137
136
  "boxShadow": "0 4px 6px rgba(0, 0, 0, 0.1)",
138
- "color": "var(--theme-ui-colors-panel-text)",
137
+ "color": "text",
139
138
  "display": "flex",
140
139
  "flexDirection": "column",
141
140
  "flexGrow": 1,
@@ -160,8 +159,8 @@ exports[`Theme Configuration a snapshot of the configuration matches the snapsho
160
159
  "UserProfileDark": {
161
160
  "WebkitBackdropFilter": "blur(12px) saturate(150%)",
162
161
  "backdropFilter": "blur(12px) saturate(150%)",
163
- "background": "var(--theme-ui-colors-panel-background)",
164
162
  "backgroundColor": "none",
163
+ "bg": "panel-background",
165
164
  "borderBottom": "none",
166
165
  "borderRadius": "card",
167
166
  "boxShadow": "none",
@@ -184,12 +183,12 @@ exports[`Theme Configuration a snapshot of the configuration matches the snapsho
184
183
  ":hover": "pointer",
185
184
  },
186
185
  "backdropFilter": "blur(10px)",
187
- "background": "var(--theme-ui-colors-panel-background)",
186
+ "bg": "panel-background",
188
187
  "border": "1px solid rgba(255, 255, 255, 0.15)",
189
188
  "borderLeft": [Function],
190
189
  "borderRadius": "10px",
191
190
  "boxShadow": "0 4px 6px rgba(0, 0, 0, 0.1)",
192
- "color": "var(--theme-ui-colors-panel-text)",
191
+ "color": "text",
193
192
  "flexGrow": 1,
194
193
  "fontSize": [
195
194
  1,
@@ -285,12 +284,12 @@ exports[`Theme Configuration a snapshot of the configuration matches the snapsho
285
284
  },
286
285
  "WebkitBackdropFilter": "blur(10px)",
287
286
  "backdropFilter": "blur(10px)",
288
- "background": "var(--theme-ui-colors-panel-background)",
287
+ "bg": "panel-background",
289
288
  "border": "1px solid rgba(255, 255, 255, 0.15)",
290
289
  "borderLeft": [Function],
291
290
  "borderRadius": "10px",
292
291
  "boxShadow": "0 4px 6px rgba(0, 0, 0, 0.1)",
293
- "color": "var(--theme-ui-colors-panel-text)",
292
+ "color": "text",
294
293
  "flexGrow": 1,
295
294
  "fontSize": [
296
295
  1,
@@ -302,33 +301,48 @@ exports[`Theme Configuration a snapshot of the configuration matches the snapsho
302
301
  "metricCard": {
303
302
  "WebkitBackdropFilter": "blur(12px) saturate(150%)",
304
303
  "backdropFilter": "blur(12px) saturate(150%)",
305
- "background": "var(--theme-ui-colors-panel-background)",
306
- "backgroundColor": "var(--theme-ui-colors-panel-background)",
304
+ "backgroundColor": "panel-background",
305
+ "bg": "panel-background",
306
+ "border": "1px solid",
307
+ "borderColor": "panel-divider",
307
308
  "borderRadius": "card",
308
309
  "boxShadow": "none",
309
- "color": "var(--theme-ui-colors-panel-text)",
310
+ "color": "text",
311
+ "flexGrow": 1,
312
+ "fontSize": [
313
+ 1,
314
+ 2,
315
+ ],
316
+ "padding": 3,
317
+ "textDecoration": "none",
318
+ },
319
+ "metricCardDark": {
320
+ "WebkitBackdropFilter": "blur(12px) saturate(150%)",
321
+ "backdropFilter": "blur(12px) saturate(150%)",
322
+ "backgroundColor": "#1e2530",
323
+ "bg": "panel-background",
324
+ "border": "1px solid",
325
+ "borderColor": "panel-divider",
326
+ "borderRadius": "card",
327
+ "boxShadow": "none",
328
+ "color": "text",
310
329
  "flexGrow": 1,
311
330
  "fontSize": [
312
331
  1,
313
332
  2,
314
333
  ],
315
334
  "padding": 3,
316
- "span": {
317
- "fontFamily": "heading",
318
- "fontWeight": "bold",
319
- "padding": 2,
320
- },
321
335
  "textDecoration": "none",
322
336
  },
323
337
  "presentationalCard": {
324
338
  "WebkitBackdropFilter": "blur(10px)",
325
339
  "backdropFilter": "blur(10px)",
326
- "background": "var(--theme-ui-colors-panel-background)",
340
+ "bg": "panel-background",
327
341
  "border": "1px solid rgba(255, 255, 255, 0.15)",
328
342
  "borderLeft": [Function],
329
343
  "borderRadius": "10px",
330
344
  "boxShadow": "0 4px 6px rgba(0, 0, 0, 0.1)",
331
- "color": "var(--theme-ui-colors-panel-text)",
345
+ "color": "text",
332
346
  "flexGrow": 1,
333
347
  "fontSize": [
334
348
  1,
@@ -340,10 +354,10 @@ exports[`Theme Configuration a snapshot of the configuration matches the snapsho
340
354
  "primary": {
341
355
  "WebkitBackdropFilter": "blur(12px) saturate(150%)",
342
356
  "backdropFilter": "blur(12px) saturate(150%)",
343
- "background": "var(--theme-ui-colors-panel-background)",
357
+ "bg": "panel-background",
344
358
  "borderRadius": "card",
345
359
  "boxShadow": "default",
346
- "color": "var(--theme-ui-colors-panel-text)",
360
+ "color": "text",
347
361
  "flexGrow": 1,
348
362
  "fontSize": [
349
363
  1,
@@ -806,6 +820,11 @@ exports[`Theme Configuration a snapshot of the configuration matches the snapsho
806
820
  "right",
807
821
  ],
808
822
  },
823
+ "mutedCardFooter": {
824
+ "display": "flex",
825
+ "justifyContent": "space-between",
826
+ "mt": 2,
827
+ },
809
828
  "outlined": {
810
829
  "border": "4px solid #efefef",
811
830
  },
@@ -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
 
@@ -67,16 +67,16 @@ const ActionButton = ({ children, href, onClick, variant = 'primary', size = 'me
67
67
 
68
68
  if (href) {
69
69
  return (
70
- <a href={href} sx={baseStyles} {...props}>
70
+ <Box as='a' href={href} sx={baseStyles} {...props}>
71
71
  {content}
72
- </a>
72
+ </Box>
73
73
  )
74
74
  }
75
75
 
76
76
  return (
77
- <button type='button' onClick={onClick} sx={baseStyles} {...props}>
77
+ <Box as='button' type='button' onClick={onClick} sx={baseStyles} {...props}>
78
78
  {content}
79
- </button>
79
+ </Box>
80
80
  )
81
81
  }
82
82
 
@@ -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
 
@@ -84,6 +82,20 @@ describe('ActionButton', () => {
84
82
  })
85
83
  })
86
84
 
85
+ it('uses dark secondary palette when color mode is dark', () => {
86
+ mockUseThemeUI.mockReturnValueOnce({
87
+ colorMode: 'dark',
88
+ theme: {
89
+ colors: {
90
+ primary: BUTTON_PRIMARY_COLORS.light,
91
+ primaryRgb: '66, 46, 163'
92
+ }
93
+ }
94
+ })
95
+ renderWithProviders(<ActionButton variant='secondary'>Secondary Dark</ActionButton>)
96
+ expect(screen.getByRole('button', { name: /secondary dark/i })).toHaveStyle({ color: '#888' })
97
+ })
98
+
87
99
  it('applies small size styles', () => {
88
100
  renderWithProviders(<ActionButton size='small'>Small Button</ActionButton>)
89
101
 
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Layout `sx` aligned with GitHub pinned cards — pair with `Card variant="actionCard"`.
3
+ * @see theme/src/components/widgets/github/pinned-item-card.js
4
+ */
5
+ export const actionCardPinnedLayoutSx = {
6
+ height: '100%',
7
+ display: 'flex',
8
+ flexDirection: 'column',
9
+ transition: 'transform 0.2s ease-in-out',
10
+ '&:hover': {
11
+ transform: 'translateY(-4px)'
12
+ }
13
+ }
@@ -0,0 +1,13 @@
1
+ import { actionCardPinnedLayoutSx } from './action-card-layout.js'
2
+
3
+ describe('actionCardPinnedLayoutSx', () => {
4
+ it('exports a Theme UI sx object for action-card hover layout', () => {
5
+ expect(actionCardPinnedLayoutSx).toMatchObject({
6
+ height: '100%',
7
+ display: 'flex',
8
+ flexDirection: 'column',
9
+ transition: 'transform 0.2s ease-in-out'
10
+ })
11
+ expect(actionCardPinnedLayoutSx['&:hover']).toEqual({ transform: 'translateY(-4px)' })
12
+ })
13
+ })
@@ -0,0 +1,153 @@
1
+ 'use client'
2
+
3
+ import React, { useMemo, useEffect, useState } from 'react'
4
+ import { Box } from '@theme-ui/components'
5
+ import { useColorMode, useThemeUI } from 'theme-ui'
6
+
7
+ import ColorBends from './ColorBends.js'
8
+ import {
9
+ chronogroveThemeSurfaceColorsDark,
10
+ chronogroveThemeSurfaceColorsLight
11
+ } from '../chronogrove-theme-surface-colors.js'
12
+ import { hexToRgba } from '../color-utils.js'
13
+
14
+ // Based on Starry Banner SVG colors: purple #800080 and gold #FFD700
15
+ const COLOR_BENDS_COLORS = ['#800080', '#6B2F6B', '#FFD700', '#A855A8']
16
+ const COLOR_BENDS_STYLE = { width: '100%', height: '100%' }
17
+
18
+ /**
19
+ * Fixed viewport background: solid in light mode, Color Bends (three.js) in dark mode,
20
+ * plus scroll-linked gradient overlay and parallax — same behavior as the Gatsby home.
21
+ */
22
+ export default function ChronogroveAnimatedPageBackground({
23
+ overlayHeight = 'min(112.5vh, 1500px)',
24
+ darkOpacity = 0.12,
25
+ fadeDistance = 700,
26
+ maxParallaxOffset = 150
27
+ }) {
28
+ const [colorMode] = useColorMode()
29
+ const { theme } = useThemeUI()
30
+ const isDark = colorMode === 'dark'
31
+ const [overlayOpacity, setOverlayOpacity] = useState(1)
32
+ const [parallaxOffset, setParallaxOffset] = useState(0)
33
+ const [maxScrollDistance, setMaxScrollDistance] = useState(1)
34
+ const [mounted, setMounted] = useState(false)
35
+
36
+ const bgColorRaw =
37
+ theme?.rawColors?.background ||
38
+ theme?.colors?.background ||
39
+ (isDark ? chronogroveThemeSurfaceColorsDark.background : chronogroveThemeSurfaceColorsLight.background)
40
+
41
+ useEffect(() => {
42
+ setMounted(true)
43
+ }, [])
44
+
45
+ useEffect(() => {
46
+ const updateMaxScroll = () => {
47
+ const maxScroll = Math.max(1, document.documentElement.scrollHeight - window.innerHeight)
48
+ setMaxScrollDistance(maxScroll)
49
+ }
50
+
51
+ updateMaxScroll()
52
+
53
+ window.addEventListener('resize', updateMaxScroll, { passive: true })
54
+ return () => window.removeEventListener('resize', updateMaxScroll)
55
+ }, [])
56
+
57
+ useEffect(() => {
58
+ const handleScroll = () => {
59
+ const scrollY = window.scrollY
60
+ const opacity = Math.max(0, 1 - scrollY / fadeDistance)
61
+ setOverlayOpacity(opacity)
62
+
63
+ const scrollProgress = Math.min(scrollY / maxScrollDistance, 1)
64
+ const offset = scrollProgress * maxParallaxOffset
65
+ setParallaxOffset(offset)
66
+ }
67
+
68
+ handleScroll()
69
+
70
+ window.addEventListener('scroll', handleScroll, { passive: true })
71
+ return () => window.removeEventListener('scroll', handleScroll)
72
+ }, [fadeDistance, maxParallaxOffset, maxScrollDistance])
73
+
74
+ const backgroundAnimation = useMemo(
75
+ () =>
76
+ isDark ? (
77
+ <ColorBends
78
+ colors={COLOR_BENDS_COLORS}
79
+ rotation={30}
80
+ speed={0.1}
81
+ scale={1}
82
+ frequency={1}
83
+ warpStrength={1}
84
+ mouseInfluence={1}
85
+ parallax={1}
86
+ noise={0.1}
87
+ transparent
88
+ style={COLOR_BENDS_STYLE}
89
+ />
90
+ ) : null,
91
+ [isDark]
92
+ )
93
+
94
+ if (!mounted) {
95
+ return null
96
+ }
97
+
98
+ const toRgba = (color, alpha) => {
99
+ if (typeof color === 'string' && color.startsWith('var(')) {
100
+ const fallbackHex = isDark
101
+ ? chronogroveThemeSurfaceColorsDark.background
102
+ : chronogroveThemeSurfaceColorsLight.background
103
+ return hexToRgba(fallbackHex, alpha)
104
+ }
105
+ return hexToRgba(color, alpha)
106
+ }
107
+
108
+ const gradientOverlay = `linear-gradient(to bottom, ${bgColorRaw} 0%, ${bgColorRaw} 30%, ${toRgba(bgColorRaw, 0.6)} 65%, ${toRgba(bgColorRaw, 0.2)} 85%, transparent 100%)`
109
+
110
+ return (
111
+ <>
112
+ <Box
113
+ key={`bg-${colorMode}`}
114
+ aria-hidden='true'
115
+ sx={{
116
+ position: 'fixed',
117
+ top: 0,
118
+ left: 0,
119
+ right: 0,
120
+ width: '100vw',
121
+ height: `calc(100vh + ${maxParallaxOffset}px)`,
122
+ zIndex: 0,
123
+ overflow: 'hidden',
124
+ opacity: isDark ? darkOpacity : 1,
125
+ pointerEvents: 'none',
126
+ backgroundColor: bgColorRaw,
127
+ transform: `translateY(-${parallaxOffset}px)`,
128
+ willChange: 'transform'
129
+ }}
130
+ >
131
+ {backgroundAnimation}
132
+ </Box>
133
+
134
+ <Box
135
+ key={`overlay-${colorMode}`}
136
+ aria-hidden='true'
137
+ sx={{
138
+ position: 'absolute',
139
+ top: 0,
140
+ left: 0,
141
+ right: 0,
142
+ width: '100%',
143
+ height: overlayHeight,
144
+ zIndex: 0,
145
+ pointerEvents: 'none',
146
+ opacity: overlayOpacity,
147
+ transition: 'opacity 0.1s ease-out',
148
+ background: gradientOverlay
149
+ }}
150
+ />
151
+ </>
152
+ )
153
+ }