@chronogrove/ui 0.81.0 → 0.82.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 +8 -3
- package/package.json +10 -4
- package/src/__snapshots__/profile-metrics-badge.spec.js.snap +20 -0
- package/src/__snapshots__/widget-header.spec.js.snap +33 -0
- package/src/metric-card.js +4 -3
- package/src/metric-card.spec.js +9 -0
- package/src/profile-metrics-badge.js +32 -0
- package/src/profile-metrics-badge.spec.js +89 -0
- package/src/widget-header.js +93 -0
- package/src/widget-header.spec.js +76 -0
- package/src/page-backdrop.js +0 -42
- package/src/page-backdrop.spec.js +0 -41
package/README.md
CHANGED
|
@@ -24,9 +24,8 @@ Prefer deep imports so bundles stay lean:
|
|
|
24
24
|
| `@chronogrove/ui/theme` | Default Theme UI theme object + named exports |
|
|
25
25
|
| `@chronogrove/ui/provider` | `ChronogroveThemeProvider` |
|
|
26
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).
|
|
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). |
|
|
28
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
29
|
| `@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
30
|
| `@chronogrove/ui/action-card-layout` | **`actionCardPinnedLayoutSx`** — layout `sx` for `Card variant="actionCard"` (matches GitHub pinned cards). |
|
|
32
31
|
| `@chronogrove/ui/emotion-cache` | `createChronogroveEmotionCache`, `getChronogroveEmotionCache` |
|
|
@@ -41,8 +40,12 @@ Prefer deep imports so bundles stay lean:
|
|
|
41
40
|
| `@chronogrove/ui/lazy-load` | Defer children until in viewport (`react-intersection-observer`) |
|
|
42
41
|
| `@chronogrove/ui/header` | Masthead shell (`variant: styles.Header`) |
|
|
43
42
|
| `@chronogrove/ui/page-header` | Blog-style `h1` heading (`p-name`) |
|
|
43
|
+
| `@chronogrove/ui/profile-metrics-badge` | Metrics row (`Badge variant="metrics"`) for dashboard widget headers |
|
|
44
|
+
| `@chronogrove/ui/widget-header` | Widget section title row (optional Font Awesome icon, aside slot, optional metrics) |
|
|
44
45
|
| `@chronogrove/ui/gatsby` | Color-mode Gatsby SSR/browser helpers |
|
|
45
46
|
|
|
47
|
+
**Additional subpaths:** The table lists the most common entry points. Also published: **`pagination`** (full bar; composes **`pagination-button`**), **`category-label`**, **`metric-badge`**, **`metric-card`**, **`muted-card-footer`**, **`status-card`**, **`widget-section`**, **`widget-call-to-action`**, **`external-link-icon`**. The authoritative list is **`package.json`** → **`exports`**.
|
|
48
|
+
|
|
46
49
|
## Next.js (App Router)
|
|
47
50
|
|
|
48
51
|
**Reference app:** [`examples/chronogrove-next`](../../examples/chronogrove-next) (`chronogrove-next`). Run `pnpm --filter chronogrove-next dev` from the repo root.
|
|
@@ -54,7 +57,9 @@ Prefer deep imports so bundles stay lean:
|
|
|
54
57
|
- **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
58
|
- **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
59
|
- **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).
|
|
60
|
+
- **Animated page background (match Gatsby home):** **`ChronogroveNextAppShell`** includes **`ChronogroveAnimatedPageBackground`** (three.js Color Bends). Content should sit in a **`position: relative; z-index: 1`** wrapper above the fixed background layer.
|
|
61
|
+
- **`WidgetHeader` + icons:** This package depends on **`@fortawesome/fontawesome-svg-core`** and **`@fortawesome/react-fontawesome`**. Icon **definitions** (e.g. `faGithub`) come from a Font Awesome kit you add to **your** app—typically **`@fortawesome/free-brands-svg-icons`** for brands—the same pattern as **`gatsby-theme-chronogrove`** widgets.
|
|
62
|
+
- **`MetricCard` loading state:** Pass **`loading`** or **`showPlaceholder`** (equivalent) for the built-in pulse placeholder (or **`loadingSlot`** to override).
|
|
58
63
|
- **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
64
|
- **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
65
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chronogrove/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.0",
|
|
4
4
|
"description": "Chronogrove Theme UI theme, color mode helpers, and shared UI primitives",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -102,6 +102,10 @@
|
|
|
102
102
|
"import": "./src/widget-call-to-action.js",
|
|
103
103
|
"default": "./src/widget-call-to-action.js"
|
|
104
104
|
},
|
|
105
|
+
"./widget-header": {
|
|
106
|
+
"import": "./src/widget-header.js",
|
|
107
|
+
"default": "./src/widget-header.js"
|
|
108
|
+
},
|
|
105
109
|
"./lazy-load": {
|
|
106
110
|
"import": "./src/lazy-load.js",
|
|
107
111
|
"default": "./src/lazy-load.js"
|
|
@@ -118,9 +122,9 @@
|
|
|
118
122
|
"import": "./src/page-header.js",
|
|
119
123
|
"default": "./src/page-header.js"
|
|
120
124
|
},
|
|
121
|
-
"./
|
|
122
|
-
"import": "./src/
|
|
123
|
-
"default": "./src/
|
|
125
|
+
"./profile-metrics-badge": {
|
|
126
|
+
"import": "./src/profile-metrics-badge.js",
|
|
127
|
+
"default": "./src/profile-metrics-badge.js"
|
|
124
128
|
},
|
|
125
129
|
"./animated-page-background": {
|
|
126
130
|
"import": "./src/animated-page-background/index.js",
|
|
@@ -152,6 +156,8 @@
|
|
|
152
156
|
"dependencies": {
|
|
153
157
|
"@emotion/cache": "^11.14.0",
|
|
154
158
|
"@emotion/react": "^11.14.0",
|
|
159
|
+
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
|
160
|
+
"@fortawesome/react-fontawesome": "^3.3.0",
|
|
155
161
|
"@theme-toggles/react": "^4.1.0",
|
|
156
162
|
"@theme-ui/components": "^0.17.4",
|
|
157
163
|
"@theme-ui/presets": "^0.17.4",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
|
2
|
+
|
|
3
|
+
exports[`ProfileMetricsBadge matches the snapshot with metrics 1`] = `
|
|
4
|
+
<DocumentFragment>
|
|
5
|
+
<div
|
|
6
|
+
class="css-1182k7c"
|
|
7
|
+
>
|
|
8
|
+
<div
|
|
9
|
+
class="css-1njnxko"
|
|
10
|
+
>
|
|
11
|
+
10 Followers
|
|
12
|
+
</div>
|
|
13
|
+
<div
|
|
14
|
+
class="css-137c6l"
|
|
15
|
+
>
|
|
16
|
+
20 Following
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</DocumentFragment>
|
|
20
|
+
`;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
|
2
|
+
|
|
3
|
+
exports[`WidgetHeader matches the snapshot 1`] = `
|
|
4
|
+
<DocumentFragment>
|
|
5
|
+
<header
|
|
6
|
+
class="css-7pzsne"
|
|
7
|
+
>
|
|
8
|
+
<div
|
|
9
|
+
class="css-s076pz"
|
|
10
|
+
>
|
|
11
|
+
<h2
|
|
12
|
+
class="css-16f4ihe"
|
|
13
|
+
>
|
|
14
|
+
<span
|
|
15
|
+
class="css-2s41mq"
|
|
16
|
+
>
|
|
17
|
+
<span
|
|
18
|
+
aria-hidden="true"
|
|
19
|
+
data-icon="spotify"
|
|
20
|
+
data-testid="fa-icon"
|
|
21
|
+
/>
|
|
22
|
+
</span>
|
|
23
|
+
Neat & Interesting Widget
|
|
24
|
+
</h2>
|
|
25
|
+
<div
|
|
26
|
+
class="sidebar-content"
|
|
27
|
+
>
|
|
28
|
+
Sidebar
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</header>
|
|
32
|
+
</DocumentFragment>
|
|
33
|
+
`;
|
package/src/metric-card.js
CHANGED
|
@@ -6,13 +6,14 @@ import isDarkMode from './helpers/isDarkMode.js'
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Metric summary card (e.g. Goodreads profile metrics). Uses `metricCard` / `metricCardDark` card variants.
|
|
9
|
-
* When `loading` is true, renders `loadingSlot` or a lightweight pulse placeholder (no react-placeholder dependency).
|
|
9
|
+
* When `loading` or `showPlaceholder` is true, renders `loadingSlot` or a lightweight pulse placeholder (no react-placeholder dependency).
|
|
10
10
|
*/
|
|
11
|
-
const MetricCard = ({ title, value, loading = false, loadingSlot, sx, ...props }) => {
|
|
11
|
+
const MetricCard = ({ title, value, loading = false, showPlaceholder, loadingSlot, sx, ...props }) => {
|
|
12
12
|
const { colorMode } = useThemeUI()
|
|
13
13
|
const variant = isDarkMode(colorMode) ? 'metricCardDark' : 'metricCard'
|
|
14
|
+
const isLoading = Boolean(loading || showPlaceholder)
|
|
14
15
|
|
|
15
|
-
const body =
|
|
16
|
+
const body = isLoading ? (
|
|
16
17
|
(loadingSlot ?? (
|
|
17
18
|
<Box
|
|
18
19
|
aria-busy='true'
|
package/src/metric-card.spec.js
CHANGED
|
@@ -39,6 +39,15 @@ describe('MetricCard', () => {
|
|
|
39
39
|
expect(screen.getByRole('status')).toBeInTheDocument()
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
+
it('treats showPlaceholder as loading', () => {
|
|
43
|
+
render(
|
|
44
|
+
<ThemeUIProvider theme={theme}>
|
|
45
|
+
<MetricCard title='F' value='1' showPlaceholder />
|
|
46
|
+
</ThemeUIProvider>
|
|
47
|
+
)
|
|
48
|
+
expect(screen.getByRole('status')).toBeInTheDocument()
|
|
49
|
+
})
|
|
50
|
+
|
|
42
51
|
it('uses metricCardDark when dark', () => {
|
|
43
52
|
useThemeUI.mockReturnValue({ colorMode: 'dark' })
|
|
44
53
|
render(
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Badge, Box } from '@theme-ui/components'
|
|
3
|
+
|
|
4
|
+
const ProfileMetricsBadge = ({ compact = false, isLoading, metrics = [] }) => {
|
|
5
|
+
let metricsToShow
|
|
6
|
+
if (isLoading) {
|
|
7
|
+
metricsToShow = [{}, {}]
|
|
8
|
+
} else if (Array.isArray(metrics)) {
|
|
9
|
+
metricsToShow = metrics
|
|
10
|
+
} else {
|
|
11
|
+
metricsToShow = []
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Box
|
|
16
|
+
sx={{
|
|
17
|
+
fontFamily: 'heading',
|
|
18
|
+
...(compact ? { mt: 0, pb: 0, pt: 0 } : { mt: 2, pb: 4, pt: 1 }),
|
|
19
|
+
display: 'flex',
|
|
20
|
+
justifyContent: ['center', 'unset']
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
{metricsToShow.map(({ displayName, id, value }, idx) => (
|
|
24
|
+
<Badge key={id || idx} variant='metrics' ml={idx !== 0 && 2}>
|
|
25
|
+
{value} {displayName}
|
|
26
|
+
</Badge>
|
|
27
|
+
))}
|
|
28
|
+
</Box>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default ProfileMetricsBadge
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { ThemeUIProvider } from 'theme-ui'
|
|
3
|
+
|
|
4
|
+
import chronogroveTheme from './theme.js'
|
|
5
|
+
import ProfileMetricsBadge from './profile-metrics-badge.js'
|
|
6
|
+
|
|
7
|
+
describe('ProfileMetricsBadge', () => {
|
|
8
|
+
it('uses default props when none are passed', () => {
|
|
9
|
+
const { container } = render(
|
|
10
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
11
|
+
<ProfileMetricsBadge />
|
|
12
|
+
</ThemeUIProvider>
|
|
13
|
+
)
|
|
14
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('matches the snapshot with metrics', () => {
|
|
18
|
+
const metrics = [
|
|
19
|
+
{ displayName: 'Followers', id: 'followers', value: 10 },
|
|
20
|
+
{ displayName: 'Following', id: 'following', value: 20 }
|
|
21
|
+
]
|
|
22
|
+
const { asFragment } = render(
|
|
23
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
24
|
+
<ProfileMetricsBadge metrics={metrics} />
|
|
25
|
+
</ThemeUIProvider>
|
|
26
|
+
)
|
|
27
|
+
expect(asFragment()).toMatchSnapshot()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('renders metrics with value and displayName', () => {
|
|
31
|
+
const metrics = [{ displayName: 'Friends', id: 'friends', value: 5 }]
|
|
32
|
+
render(
|
|
33
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
34
|
+
<ProfileMetricsBadge metrics={metrics} />
|
|
35
|
+
</ThemeUIProvider>
|
|
36
|
+
)
|
|
37
|
+
expect(screen.getByText('5 Friends')).toBeInTheDocument()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('uses compact spacing when compact is true', () => {
|
|
41
|
+
const metrics = [{ displayName: 'Count', id: 'count', value: 1 }]
|
|
42
|
+
const { container } = render(
|
|
43
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
44
|
+
<ProfileMetricsBadge compact metrics={metrics} />
|
|
45
|
+
</ThemeUIProvider>
|
|
46
|
+
)
|
|
47
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
48
|
+
expect(screen.getByText('1 Count')).toBeInTheDocument()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('shows placeholder badges when isLoading', () => {
|
|
52
|
+
const { container } = render(
|
|
53
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
54
|
+
<ProfileMetricsBadge isLoading metrics={[]} />
|
|
55
|
+
</ThemeUIProvider>
|
|
56
|
+
)
|
|
57
|
+
const wrapper = container.querySelector('[class*="css-"]')
|
|
58
|
+
expect(wrapper).toBeInTheDocument()
|
|
59
|
+
expect(wrapper.children).toHaveLength(2)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('handles non-array metrics with default empty array', () => {
|
|
63
|
+
const { container } = render(
|
|
64
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
65
|
+
<ProfileMetricsBadge metrics={null} />
|
|
66
|
+
</ThemeUIProvider>
|
|
67
|
+
)
|
|
68
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('treats non-array metrics as empty when not loading', () => {
|
|
72
|
+
const { container } = render(
|
|
73
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
74
|
+
<ProfileMetricsBadge metrics='nope' />
|
|
75
|
+
</ThemeUIProvider>
|
|
76
|
+
)
|
|
77
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
78
|
+
expect(container.textContent).toBe('')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('renders nothing when metrics is an empty array', () => {
|
|
82
|
+
const { container } = render(
|
|
83
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
84
|
+
<ProfileMetricsBadge metrics={[]} />
|
|
85
|
+
</ThemeUIProvider>
|
|
86
|
+
)
|
|
87
|
+
expect(container.textContent).toBe('')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
3
|
+
import { Box, Heading } from '@theme-ui/components'
|
|
4
|
+
|
|
5
|
+
import ProfileMetricsBadge from './profile-metrics-badge.js'
|
|
6
|
+
|
|
7
|
+
const baseHeaderStyles = {
|
|
8
|
+
display: 'flex',
|
|
9
|
+
flexDirection: ['column', 'row'],
|
|
10
|
+
alignItems: ['center', 'baseline'],
|
|
11
|
+
justifyContent: ['center', 'space-between'],
|
|
12
|
+
gap: [2, 3],
|
|
13
|
+
flexWrap: 'wrap',
|
|
14
|
+
pb: 2,
|
|
15
|
+
// Soft separator: visible but not harsh (gray[4] in theme scale; fallback ~12% black)
|
|
16
|
+
borderBottom: '1px solid',
|
|
17
|
+
borderColor: theme => theme?.colors?.gray?.[4] ?? 'rgba(0,0,0,0.12)'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Groups headline + CTA so they sit together on the left; metrics stay on the right.
|
|
22
|
+
* Always row so headline + CTA stay on one line at every breakpoint (including mobile).
|
|
23
|
+
*/
|
|
24
|
+
const titleGroupStyles = {
|
|
25
|
+
display: 'flex',
|
|
26
|
+
flexDirection: 'row',
|
|
27
|
+
alignItems: 'baseline',
|
|
28
|
+
gap: [1, 2],
|
|
29
|
+
order: 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Override default heading margin, padding, and line-height so the headline and CTA
|
|
34
|
+
* align on the same baseline at every breakpoint. Theme/global styles often add
|
|
35
|
+
* vertical space to h2; zeroing it and using a tight line-height prevents offset.
|
|
36
|
+
* Baseline alignment for icon + text keeps the headline baseline consistent.
|
|
37
|
+
*/
|
|
38
|
+
const headingStyles = {
|
|
39
|
+
fontSize: [4, 5],
|
|
40
|
+
display: 'flex',
|
|
41
|
+
alignItems: 'baseline',
|
|
42
|
+
m: 0,
|
|
43
|
+
py: 0,
|
|
44
|
+
pt: 0,
|
|
45
|
+
pb: 0,
|
|
46
|
+
lineHeight: 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const metricsStyles = {
|
|
50
|
+
order: 2
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const WidgetHeader = ({ aside, children, icon, metrics, metricsLoading }) => {
|
|
54
|
+
const hasMetrics = (Array.isArray(metrics) && metrics.length > 0) || metricsLoading
|
|
55
|
+
return (
|
|
56
|
+
<Box
|
|
57
|
+
as='header'
|
|
58
|
+
sx={{
|
|
59
|
+
...baseHeaderStyles,
|
|
60
|
+
// Extra margin below header when metrics are present so spacing matches Latest Posts (no metrics)
|
|
61
|
+
mb: hasMetrics ? 4 : 2
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<Box sx={titleGroupStyles}>
|
|
65
|
+
<Heading as='h2' sx={headingStyles}>
|
|
66
|
+
{icon && (
|
|
67
|
+
<Box
|
|
68
|
+
as='span'
|
|
69
|
+
sx={{
|
|
70
|
+
display: 'inline-flex',
|
|
71
|
+
alignItems: 'baseline',
|
|
72
|
+
mr: 2,
|
|
73
|
+
fontSize: 4,
|
|
74
|
+
'& svg': { width: '1em', height: '1em' }
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<FontAwesomeIcon icon={icon} aria-hidden='true' />
|
|
78
|
+
</Box>
|
|
79
|
+
)}
|
|
80
|
+
{children}
|
|
81
|
+
</Heading>
|
|
82
|
+
{aside}
|
|
83
|
+
</Box>
|
|
84
|
+
{((Array.isArray(metrics) && metrics.length > 0) || metricsLoading) && (
|
|
85
|
+
<Box sx={metricsStyles}>
|
|
86
|
+
<ProfileMetricsBadge compact isLoading={metricsLoading} metrics={metrics} />
|
|
87
|
+
</Box>
|
|
88
|
+
)}
|
|
89
|
+
</Box>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default WidgetHeader
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { ThemeUIProvider } from 'theme-ui'
|
|
4
|
+
|
|
5
|
+
import chronogroveTheme from './theme.js'
|
|
6
|
+
import WidgetHeader from './widget-header.js'
|
|
7
|
+
|
|
8
|
+
jest.mock('@fortawesome/react-fontawesome', () => ({
|
|
9
|
+
FontAwesomeIcon: ({ icon }) => <span data-testid='fa-icon' data-icon={icon?.iconName ?? ''} aria-hidden />
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
const aside = <div className='sidebar-content'>Sidebar</div>
|
|
13
|
+
const mockIcon = { iconName: 'spotify', prefix: 'fab' }
|
|
14
|
+
|
|
15
|
+
describe('WidgetHeader', () => {
|
|
16
|
+
it('matches the snapshot', () => {
|
|
17
|
+
const widgetTitle = 'Neat & Interesting Widget'
|
|
18
|
+
const { asFragment } = render(
|
|
19
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
20
|
+
<WidgetHeader aside={aside} icon={mockIcon}>
|
|
21
|
+
{widgetTitle}
|
|
22
|
+
</WidgetHeader>
|
|
23
|
+
</ThemeUIProvider>
|
|
24
|
+
)
|
|
25
|
+
expect(asFragment()).toMatchSnapshot()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('renders metrics row when metrics are provided', () => {
|
|
29
|
+
const metrics = [{ displayName: 'Stars', id: 's', value: 12 }]
|
|
30
|
+
const { getByText } = render(
|
|
31
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
32
|
+
<WidgetHeader metrics={metrics}>Title</WidgetHeader>
|
|
33
|
+
</ThemeUIProvider>
|
|
34
|
+
)
|
|
35
|
+
expect(getByText('12 Stars')).toBeInTheDocument()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('renders loading metrics placeholders when metricsLoading', () => {
|
|
39
|
+
const { container } = render(
|
|
40
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
41
|
+
<WidgetHeader metrics={[]} metricsLoading>
|
|
42
|
+
Title
|
|
43
|
+
</WidgetHeader>
|
|
44
|
+
</ThemeUIProvider>
|
|
45
|
+
)
|
|
46
|
+
const badges = container.querySelectorAll('[class*="css-"]')
|
|
47
|
+
expect(badges.length).toBeGreaterThan(0)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('renders without icon', () => {
|
|
51
|
+
render(
|
|
52
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
53
|
+
<WidgetHeader aside={aside}>No icon</WidgetHeader>
|
|
54
|
+
</ThemeUIProvider>
|
|
55
|
+
)
|
|
56
|
+
expect(screen.getByText('No icon')).toBeInTheDocument()
|
|
57
|
+
expect(screen.queryByTestId('fa-icon')).not.toBeInTheDocument()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('uses borderColor fallback when gray[4] is missing', () => {
|
|
61
|
+
const themeMissingGray4 = {
|
|
62
|
+
...chronogroveTheme,
|
|
63
|
+
colors: {
|
|
64
|
+
...chronogroveTheme.colors,
|
|
65
|
+
gray: { 0: '#111', 1: '#222', 2: '#333', 3: '#444' }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const { container } = render(
|
|
69
|
+
<ThemeUIProvider theme={themeMissingGray4}>
|
|
70
|
+
<WidgetHeader>Border test</WidgetHeader>
|
|
71
|
+
</ThemeUIProvider>
|
|
72
|
+
)
|
|
73
|
+
expect(container.querySelector('header')).toBeTruthy()
|
|
74
|
+
expect(screen.getByText('Border test')).toBeInTheDocument()
|
|
75
|
+
})
|
|
76
|
+
})
|
package/src/page-backdrop.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { Box } from '@theme-ui/components'
|
|
4
|
-
import { useColorMode } from 'theme-ui'
|
|
5
|
-
|
|
6
|
-
import isDarkMode from './helpers/isDarkMode.js'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Fixed viewport layer behind page content (`z-index: 0`), matching the role of
|
|
10
|
-
* `AnimatedPageBackground` on the Gatsby home: a real surface under `z-index: 1` UI so
|
|
11
|
-
* frosted panels (`backdrop-filter` + `panel-background`) have something to blur and tint
|
|
12
|
-
* against. The full site uses WebGL Color Bends there; this package ships a **lightweight**
|
|
13
|
-
* CSS gradient treatment only (no three.js).
|
|
14
|
-
*/
|
|
15
|
-
export function ChronogrovePageBackdrop() {
|
|
16
|
-
const [colorMode] = useColorMode()
|
|
17
|
-
const dark = isDarkMode(colorMode)
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<Box
|
|
21
|
-
aria-hidden
|
|
22
|
-
sx={{
|
|
23
|
-
position: 'fixed',
|
|
24
|
-
inset: 0,
|
|
25
|
-
width: '100vw',
|
|
26
|
-
minHeight: '100vh',
|
|
27
|
-
zIndex: 0,
|
|
28
|
-
pointerEvents: 'none',
|
|
29
|
-
bg: 'background',
|
|
30
|
-
// Dark: subtle primary/secondary glows — same intent as Color Bends, fraction of the cost.
|
|
31
|
-
backgroundImage: dark
|
|
32
|
-
? `
|
|
33
|
-
radial-gradient(ellipse 120% 85% at 50% -15%, rgba(74, 158, 255, 0.16) 0%, transparent 52%),
|
|
34
|
-
radial-gradient(ellipse 90% 70% at 95% 85%, rgba(113, 30, 155, 0.12) 0%, transparent 48%),
|
|
35
|
-
radial-gradient(ellipse 70% 50% at 10% 60%, rgba(128, 0, 128, 0.08) 0%, transparent 45%)
|
|
36
|
-
`
|
|
37
|
-
: 'none',
|
|
38
|
-
transition: 'background-image 0.3s ease'
|
|
39
|
-
}}
|
|
40
|
-
/>
|
|
41
|
-
)
|
|
42
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @jest-environment jsdom
|
|
3
|
-
*/
|
|
4
|
-
import React from 'react'
|
|
5
|
-
import { render } from '@testing-library/react'
|
|
6
|
-
import { ThemeUIProvider } from 'theme-ui'
|
|
7
|
-
|
|
8
|
-
import chronogroveTheme from './theme.js'
|
|
9
|
-
import { ChronogrovePageBackdrop } from './page-backdrop.js'
|
|
10
|
-
|
|
11
|
-
describe('ChronogrovePageBackdrop', () => {
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
window.localStorage.removeItem('theme-ui-color-mode')
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('renders a fixed backdrop layer', () => {
|
|
17
|
-
const { container } = render(
|
|
18
|
-
<ThemeUIProvider theme={chronogroveTheme}>
|
|
19
|
-
<ChronogrovePageBackdrop />
|
|
20
|
-
</ThemeUIProvider>
|
|
21
|
-
)
|
|
22
|
-
const layer = container.querySelector('[aria-hidden="true"]')
|
|
23
|
-
expect(layer).toBeTruthy()
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('adds gradient overlays in dark mode', () => {
|
|
27
|
-
window.localStorage.setItem('theme-ui-color-mode', 'dark')
|
|
28
|
-
const { container } = render(
|
|
29
|
-
<ThemeUIProvider
|
|
30
|
-
theme={{
|
|
31
|
-
...chronogroveTheme,
|
|
32
|
-
config: { useLocalStorage: true, useColorSchemeMediaQuery: false }
|
|
33
|
-
}}
|
|
34
|
-
>
|
|
35
|
-
<ChronogrovePageBackdrop />
|
|
36
|
-
</ThemeUIProvider>
|
|
37
|
-
)
|
|
38
|
-
const layer = container.querySelector('[aria-hidden="true"]')
|
|
39
|
-
expect(window.getComputedStyle(layer).backgroundImage).toMatch(/radial-gradient/)
|
|
40
|
-
})
|
|
41
|
-
})
|