@chronogrove/ui 0.82.1 → 0.83.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 +1 -1
- package/package.json +9 -1
- package/src/__snapshots__/image-thumbnails.spec.js.snap +61 -0
- package/src/__snapshots__/thumbnail-strip.spec.js.snap +61 -0
- package/src/color-mode/constants.js +6 -0
- package/src/color-mode/cross-domain-color-mode-client-config.js +33 -0
- package/src/color-mode/cross-domain-color-mode-client-config.spec.js +45 -0
- package/src/color-mode/cross-domain-color-mode-cookie-node.spec.js +28 -0
- package/src/color-mode/cross-domain-color-mode-cookie-set-http.spec.js +42 -0
- package/src/color-mode/cross-domain-color-mode-cookie-set-https.spec.js +50 -0
- package/src/color-mode/cross-domain-color-mode-cookie-set-localhost.spec.js +15 -0
- package/src/color-mode/cross-domain-color-mode-cookie.js +91 -0
- package/src/color-mode/cross-domain-color-mode-cookie.spec.js +120 -0
- package/src/color-mode/head-inline-resolution.spec.js +111 -0
- package/src/color-mode/head-inline.js +104 -17
- package/src/color-mode/head-inline.spec.js +68 -1
- package/src/color-mode/index.js +17 -2
- package/src/color-mode/registrable-domain.js +30 -0
- package/src/color-mode/registrable-domain.spec.js +28 -0
- package/src/color-toggle.js +3 -0
- package/src/gatsby/build-theme-ui-color-mode-head-components.js +5 -4
- package/src/gatsby/index.spec.js +11 -1
- package/src/image-thumbnails.js +89 -0
- package/src/image-thumbnails.spec.js +95 -0
- package/src/next/app-shell.js +12 -2
- package/src/next/root-layout-head.js +6 -3
- package/src/thumbnail-strip.js +72 -0
- package/src/thumbnail-strip.spec.js +83 -0
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ Prefer deep imports so bundles stay lean:
|
|
|
44
44
|
| `@chronogrove/ui/widget-header` | Widget section title row (optional Font Awesome icon, aside slot, optional metrics) |
|
|
45
45
|
| `@chronogrove/ui/gatsby` | Color-mode Gatsby SSR/browser helpers |
|
|
46
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
|
|
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`**, **`thumbnail-strip`**, **`image-thumbnails`** (`optimizeSrc` for CDN resizing; Gatsby passes a Cloudinary helper). The authoritative list is **`package.json`** → **`exports`**.
|
|
48
48
|
|
|
49
49
|
## Next.js (App Router)
|
|
50
50
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chronogrove/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.83.0",
|
|
4
4
|
"description": "Chronogrove Theme UI theme, color mode helpers, and shared UI primitives",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -138,6 +138,14 @@
|
|
|
138
138
|
"import": "./src/action-card-layout.js",
|
|
139
139
|
"default": "./src/action-card-layout.js"
|
|
140
140
|
},
|
|
141
|
+
"./thumbnail-strip": {
|
|
142
|
+
"import": "./src/thumbnail-strip.js",
|
|
143
|
+
"default": "./src/thumbnail-strip.js"
|
|
144
|
+
},
|
|
145
|
+
"./image-thumbnails": {
|
|
146
|
+
"import": "./src/image-thumbnails.js",
|
|
147
|
+
"default": "./src/image-thumbnails.js"
|
|
148
|
+
},
|
|
141
149
|
"./gatsby": {
|
|
142
150
|
"import": "./src/gatsby/index.js",
|
|
143
151
|
"default": "./src/gatsby/index.js"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
|
2
|
+
|
|
3
|
+
exports[`ImageThumbnails matches snapshot with custom maxImages 1`] = `
|
|
4
|
+
<DocumentFragment>
|
|
5
|
+
<div
|
|
6
|
+
class="css-1ud2rd2"
|
|
7
|
+
>
|
|
8
|
+
<div
|
|
9
|
+
class="css-1jg6seg"
|
|
10
|
+
>
|
|
11
|
+
<div
|
|
12
|
+
class="css-m0n9e0"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
<div
|
|
16
|
+
class="css-1da34oq"
|
|
17
|
+
>
|
|
18
|
+
<div
|
|
19
|
+
class="css-1yk8ryr"
|
|
20
|
+
/>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</DocumentFragment>
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
exports[`ImageThumbnails matches snapshot with default props 1`] = `
|
|
27
|
+
<DocumentFragment>
|
|
28
|
+
<div
|
|
29
|
+
class="css-1ud2rd2"
|
|
30
|
+
>
|
|
31
|
+
<div
|
|
32
|
+
class="css-1jg6seg"
|
|
33
|
+
>
|
|
34
|
+
<div
|
|
35
|
+
class="css-m0n9e0"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
<div
|
|
39
|
+
class="css-1da34oq"
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
class="css-1yk8ryr"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
<div
|
|
46
|
+
class="css-1jg6seg"
|
|
47
|
+
>
|
|
48
|
+
<div
|
|
49
|
+
class="css-1udw8eu"
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
<div
|
|
53
|
+
class="css-1da34oq"
|
|
54
|
+
>
|
|
55
|
+
<div
|
|
56
|
+
class="css-dvh21o"
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</DocumentFragment>
|
|
61
|
+
`;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
|
2
|
+
|
|
3
|
+
exports[`ThumbnailStrip matches snapshot with custom maxImages and size 1`] = `
|
|
4
|
+
<DocumentFragment>
|
|
5
|
+
<div
|
|
6
|
+
class="css-dpp4jp"
|
|
7
|
+
>
|
|
8
|
+
<div
|
|
9
|
+
class="css-1p4ud4e"
|
|
10
|
+
>
|
|
11
|
+
<div
|
|
12
|
+
class="css-m0n9e0"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
<div
|
|
16
|
+
class="css-zlkqv4"
|
|
17
|
+
>
|
|
18
|
+
<div
|
|
19
|
+
class="css-1yk8ryr"
|
|
20
|
+
/>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</DocumentFragment>
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
exports[`ThumbnailStrip matches snapshot with default props 1`] = `
|
|
27
|
+
<DocumentFragment>
|
|
28
|
+
<div
|
|
29
|
+
class="css-1httq4l"
|
|
30
|
+
>
|
|
31
|
+
<div
|
|
32
|
+
class="css-1jl8pr3"
|
|
33
|
+
>
|
|
34
|
+
<div
|
|
35
|
+
class="css-m0n9e0"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
<div
|
|
39
|
+
class="css-f02gwb"
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
class="css-1yk8ryr"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
<div
|
|
46
|
+
class="css-44fsif"
|
|
47
|
+
>
|
|
48
|
+
<div
|
|
49
|
+
class="css-1udw8eu"
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
<div
|
|
53
|
+
class="css-ladzy1"
|
|
54
|
+
>
|
|
55
|
+
<div
|
|
56
|
+
class="css-dvh21o"
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</DocumentFragment>
|
|
61
|
+
`;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/** Theme UI localStorage key (aligned with theme-ui/color-mode). */
|
|
2
2
|
export const THEME_UI_COLOR_MODE_STORAGE_KEY = 'theme-ui-color-mode'
|
|
3
3
|
|
|
4
|
+
/** Default cookie name when `cookieName` is omitted from cross-domain color mode options. */
|
|
5
|
+
export const CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME = 'chronogrove-theme-ui-color-mode'
|
|
6
|
+
|
|
7
|
+
/** ~400 days; browser cookie eviction policies may apply earlier. */
|
|
8
|
+
export const CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_MAX_AGE_SEC = 60 * 60 * 24 * 400
|
|
9
|
+
|
|
4
10
|
/** Dispatched on route changes so hosts can reconcile React color-mode context with `localStorage`. */
|
|
5
11
|
export const RECONCILE_COLOR_MODE_EVENT = 'theme-ui-reconcile-color-mode'
|
|
6
12
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME } from './constants.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser-only config for {@link setChronogroveCrossDomainColorModeCookie} (must match the
|
|
5
|
+
* `crossDomainColorMode` passed into head inline scripts at build time). Set via
|
|
6
|
+
* {@link setChronogroveCrossDomainColorModeClientConfig} from Gatsby `onClientEntry`, Next
|
|
7
|
+
* `ChronogroveNextAppShell`, or your own bootstrap.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let clientConfig = /** @type {{ registrableDomain?: string, cookieName?: string } | null} */ (null)
|
|
11
|
+
|
|
12
|
+
export function setChronogroveCrossDomainColorModeClientConfig(next) {
|
|
13
|
+
if (next == null) {
|
|
14
|
+
clientConfig = null
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
if (typeof next !== 'object') {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
const { registrableDomain, cookieName } = next
|
|
21
|
+
if (!registrableDomain || typeof registrableDomain !== 'string' || !registrableDomain.trim()) {
|
|
22
|
+
clientConfig = null
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
clientConfig = {
|
|
26
|
+
registrableDomain: registrableDomain.trim(),
|
|
27
|
+
cookieName: cookieName ?? CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getChronogroveCrossDomainColorModeClientConfig() {
|
|
32
|
+
return clientConfig
|
|
33
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME } from './constants.js'
|
|
6
|
+
import {
|
|
7
|
+
getChronogroveCrossDomainColorModeClientConfig,
|
|
8
|
+
setChronogroveCrossDomainColorModeClientConfig
|
|
9
|
+
} from './cross-domain-color-mode-client-config.js'
|
|
10
|
+
|
|
11
|
+
describe('cross-domain-color-mode-client-config', () => {
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
setChronogroveCrossDomainColorModeClientConfig(null)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('stores registrableDomain and optional cookieName', () => {
|
|
17
|
+
setChronogroveCrossDomainColorModeClientConfig({
|
|
18
|
+
registrableDomain: 'example.com',
|
|
19
|
+
cookieName: 'my-mode'
|
|
20
|
+
})
|
|
21
|
+
expect(getChronogroveCrossDomainColorModeClientConfig()).toEqual({
|
|
22
|
+
registrableDomain: 'example.com',
|
|
23
|
+
cookieName: 'my-mode'
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('uses default cookie name when cookieName is omitted', () => {
|
|
28
|
+
setChronogroveCrossDomainColorModeClientConfig({ registrableDomain: 'example.com' })
|
|
29
|
+
expect(getChronogroveCrossDomainColorModeClientConfig()?.cookieName).toBe(
|
|
30
|
+
CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('clears config for null, invalid, or empty registrableDomain', () => {
|
|
35
|
+
setChronogroveCrossDomainColorModeClientConfig({ registrableDomain: 'example.com' })
|
|
36
|
+
setChronogroveCrossDomainColorModeClientConfig(null)
|
|
37
|
+
expect(getChronogroveCrossDomainColorModeClientConfig()).toBe(null)
|
|
38
|
+
|
|
39
|
+
setChronogroveCrossDomainColorModeClientConfig({ registrableDomain: ' ' })
|
|
40
|
+
expect(getChronogroveCrossDomainColorModeClientConfig()).toBe(null)
|
|
41
|
+
|
|
42
|
+
setChronogroveCrossDomainColorModeClientConfig('oops')
|
|
43
|
+
expect(getChronogroveCrossDomainColorModeClientConfig()).toBe(null)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getChronogroveCrossDomainColorModeFromCookie,
|
|
7
|
+
getHostnameForChronogroveCrossDomainCookie,
|
|
8
|
+
setChronogroveCrossDomainColorModeCookie,
|
|
9
|
+
shouldUseSecureChronogroveCrossDomainCookie
|
|
10
|
+
} from './cross-domain-color-mode-cookie.js'
|
|
11
|
+
|
|
12
|
+
describe('cross-domain color mode cookie (Node / SSR)', () => {
|
|
13
|
+
it('setChronogroveCrossDomainColorModeCookie returns without throwing when document is undefined', () => {
|
|
14
|
+
expect(() => setChronogroveCrossDomainColorModeCookie('dark')).not.toThrow()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('getChronogroveCrossDomainColorModeFromCookie uses empty string when document is undefined', () => {
|
|
18
|
+
expect(getChronogroveCrossDomainColorModeFromCookie()).toBe(null)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('getHostnameForChronogroveCrossDomainCookie() returns empty when global window is absent', () => {
|
|
22
|
+
expect(getHostnameForChronogroveCrossDomainCookie()).toBe('')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('shouldUseSecureChronogroveCrossDomainCookie() is false when global window is absent', () => {
|
|
26
|
+
expect(shouldUseSecureChronogroveCrossDomainCookie()).toBe(false)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
* @jest-environment-options {"url":"http://app.example.com/"}
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME } from './constants.js'
|
|
7
|
+
import { setChronogroveCrossDomainColorModeCookie } from './cross-domain-color-mode-cookie.js'
|
|
8
|
+
|
|
9
|
+
const exampleDomain = { registrableDomain: 'example.com' }
|
|
10
|
+
|
|
11
|
+
describe('setChronogroveCrossDomainColorModeCookie (http, cross-domain enabled)', () => {
|
|
12
|
+
let lastAssignment = ''
|
|
13
|
+
let originalDescriptor
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
lastAssignment = ''
|
|
17
|
+
originalDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie')
|
|
18
|
+
Object.defineProperty(document, 'cookie', {
|
|
19
|
+
configurable: true,
|
|
20
|
+
get() {
|
|
21
|
+
if (!lastAssignment) return ''
|
|
22
|
+
return lastAssignment.split(';')[0].trim()
|
|
23
|
+
},
|
|
24
|
+
set(value) {
|
|
25
|
+
lastAssignment = value
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
if (originalDescriptor) {
|
|
32
|
+
Object.defineProperty(Document.prototype, 'cookie', originalDescriptor)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('omits Secure when not on HTTPS', () => {
|
|
37
|
+
expect(window.location.protocol).toBe('http:')
|
|
38
|
+
setChronogroveCrossDomainColorModeCookie('default', exampleDomain)
|
|
39
|
+
expect(lastAssignment).toContain(`${CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME}=default`)
|
|
40
|
+
expect(lastAssignment).not.toContain('Secure')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
* @jest-environment-options {"url":"https://www.example.com/path"}
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME } from './constants.js'
|
|
7
|
+
import { setChronogroveCrossDomainColorModeCookie } from './cross-domain-color-mode-cookie.js'
|
|
8
|
+
|
|
9
|
+
const exampleDomain = { registrableDomain: 'example.com' }
|
|
10
|
+
|
|
11
|
+
describe('setChronogroveCrossDomainColorModeCookie (https, cross-domain enabled)', () => {
|
|
12
|
+
let lastAssignment = ''
|
|
13
|
+
let originalDescriptor
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
lastAssignment = ''
|
|
17
|
+
originalDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie')
|
|
18
|
+
Object.defineProperty(document, 'cookie', {
|
|
19
|
+
configurable: true,
|
|
20
|
+
get() {
|
|
21
|
+
if (!lastAssignment) return ''
|
|
22
|
+
return lastAssignment.split(';')[0].trim()
|
|
23
|
+
},
|
|
24
|
+
set(value) {
|
|
25
|
+
lastAssignment = value
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
if (originalDescriptor) {
|
|
32
|
+
Object.defineProperty(Document.prototype, 'cookie', originalDescriptor)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('sets Domain, SameSite, Max-Age, and Secure on HTTPS (assignment string)', () => {
|
|
37
|
+
expect(window.location.hostname).toBe('www.example.com')
|
|
38
|
+
setChronogroveCrossDomainColorModeCookie('dark', exampleDomain)
|
|
39
|
+
expect(lastAssignment).toContain(`${CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME}=dark`)
|
|
40
|
+
expect(lastAssignment).toContain('Domain=.example.com')
|
|
41
|
+
expect(lastAssignment).toContain('SameSite=Lax')
|
|
42
|
+
expect(lastAssignment).toContain('Max-Age=')
|
|
43
|
+
expect(lastAssignment).toContain('Secure')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('ignores invalid mode values', () => {
|
|
47
|
+
setChronogroveCrossDomainColorModeCookie('not-a-mode', exampleDomain)
|
|
48
|
+
expect(lastAssignment).toBe('')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
* @jest-environment-options {"url":"http://localhost:3000/"}
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { setChronogroveCrossDomainColorModeCookie } from './cross-domain-color-mode-cookie.js'
|
|
7
|
+
|
|
8
|
+
describe('setChronogroveCrossDomainColorModeCookie (localhost)', () => {
|
|
9
|
+
it('does not set when hostname is not under the registrable domain', () => {
|
|
10
|
+
expect(window.location.hostname).toBe('localhost')
|
|
11
|
+
document.cookie = ''
|
|
12
|
+
setChronogroveCrossDomainColorModeCookie('dark', { registrableDomain: 'example.com' })
|
|
13
|
+
expect(document.cookie).toBe('')
|
|
14
|
+
})
|
|
15
|
+
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_MAX_AGE_SEC,
|
|
3
|
+
CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME
|
|
4
|
+
} from './constants.js'
|
|
5
|
+
import { normalizeThemeUiColorMode } from './normalize.js'
|
|
6
|
+
import { isHostnameUnderRegistrableDomain, validateRegistrableDomain } from './registrable-domain.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read `window.location.hostname` when `location` exists (for tests, pass a stub `globalWindow`).
|
|
10
|
+
* Call with **no arguments** to use the real `window`; passing `undefined` explicitly is treated as
|
|
11
|
+
* “no window” (tests only).
|
|
12
|
+
*/
|
|
13
|
+
export function getHostnameForChronogroveCrossDomainCookie(globalWindow) {
|
|
14
|
+
const win = arguments.length === 0 ? (typeof window !== 'undefined' ? window : undefined) : globalWindow
|
|
15
|
+
if (win == null || !win.location) {
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
18
|
+
return win.location.hostname || ''
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Whether to append `Secure` to the cross-domain cookie (HTTPS only). */
|
|
22
|
+
export function shouldUseSecureChronogroveCrossDomainCookie(globalWindow) {
|
|
23
|
+
const win = arguments.length === 0 ? (typeof window !== 'undefined' ? window : undefined) : globalWindow
|
|
24
|
+
return Boolean(win && win.location && win.location.protocol === 'https:')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Read a single cookie value from a `document.cookie`-style string (for tests and SSR-safe parsing).
|
|
29
|
+
*/
|
|
30
|
+
export function parseChronogroveColorModeCookie(
|
|
31
|
+
cookieString,
|
|
32
|
+
cookieName = CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME
|
|
33
|
+
) {
|
|
34
|
+
if (!cookieString) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
const parts = `; ${cookieString}`.split(`; ${cookieName}=`)
|
|
38
|
+
if (parts.length < 2) {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
const raw = parts.pop().split(';').shift()
|
|
42
|
+
if (raw == null || raw === '') {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
let decoded
|
|
46
|
+
try {
|
|
47
|
+
decoded = decodeURIComponent(raw.trim())
|
|
48
|
+
} catch {
|
|
49
|
+
decoded = raw.trim()
|
|
50
|
+
}
|
|
51
|
+
return normalizeThemeUiColorMode(decoded)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getChronogroveCrossDomainColorModeFromCookie(
|
|
55
|
+
cookieString = typeof document !== 'undefined' ? document.cookie : ''
|
|
56
|
+
) {
|
|
57
|
+
return parseChronogroveColorModeCookie(cookieString)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Persist color mode for other subdomains of `options.registrableDomain`. No-op without `document`,
|
|
62
|
+
* without a valid `registrableDomain`, or when the current host is not under that domain.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} mode
|
|
65
|
+
* @param {{ registrableDomain: string, cookieName?: string } | null | undefined} [options]
|
|
66
|
+
*/
|
|
67
|
+
export function setChronogroveCrossDomainColorModeCookie(mode, options) {
|
|
68
|
+
if (typeof document === 'undefined') {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
const normalized = normalizeThemeUiColorMode(mode)
|
|
72
|
+
if (!normalized) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const registrableDomain = options && validateRegistrableDomain(options.registrableDomain)
|
|
76
|
+
if (!registrableDomain) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
const hostname = getHostnameForChronogroveCrossDomainCookie()
|
|
80
|
+
if (!isHostnameUnderRegistrableDomain(hostname, registrableDomain)) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const cookieName = options.cookieName ?? CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME
|
|
85
|
+
const value = encodeURIComponent(normalized)
|
|
86
|
+
let cookie = `${cookieName}=${value}; Path=/; Max-Age=${CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_MAX_AGE_SEC}; SameSite=Lax; Domain=.${registrableDomain}`
|
|
87
|
+
if (shouldUseSecureChronogroveCrossDomainCookie()) {
|
|
88
|
+
cookie += '; Secure'
|
|
89
|
+
}
|
|
90
|
+
document.cookie = cookie
|
|
91
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME } from './constants.js'
|
|
6
|
+
import {
|
|
7
|
+
getChronogroveCrossDomainColorModeFromCookie,
|
|
8
|
+
getHostnameForChronogroveCrossDomainCookie,
|
|
9
|
+
parseChronogroveColorModeCookie,
|
|
10
|
+
setChronogroveCrossDomainColorModeCookie,
|
|
11
|
+
shouldUseSecureChronogroveCrossDomainCookie
|
|
12
|
+
} from './cross-domain-color-mode-cookie.js'
|
|
13
|
+
|
|
14
|
+
describe('getHostnameForChronogroveCrossDomainCookie', () => {
|
|
15
|
+
it('returns empty string when window is missing or has no location', () => {
|
|
16
|
+
expect(getHostnameForChronogroveCrossDomainCookie(undefined)).toBe('')
|
|
17
|
+
expect(getHostnameForChronogroveCrossDomainCookie(null)).toBe('')
|
|
18
|
+
expect(getHostnameForChronogroveCrossDomainCookie({})).toBe('')
|
|
19
|
+
expect(getHostnameForChronogroveCrossDomainCookie({ location: null })).toBe('')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('returns hostname or empty when location exists', () => {
|
|
23
|
+
expect(getHostnameForChronogroveCrossDomainCookie({ location: { hostname: 'www.example.com' } })).toBe(
|
|
24
|
+
'www.example.com'
|
|
25
|
+
)
|
|
26
|
+
expect(getHostnameForChronogroveCrossDomainCookie({ location: { hostname: '' } })).toBe('')
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('shouldUseSecureChronogroveCrossDomainCookie', () => {
|
|
31
|
+
it('is true only for https:', () => {
|
|
32
|
+
expect(shouldUseSecureChronogroveCrossDomainCookie(undefined)).toBe(false)
|
|
33
|
+
expect(shouldUseSecureChronogroveCrossDomainCookie({})).toBe(false)
|
|
34
|
+
expect(shouldUseSecureChronogroveCrossDomainCookie({ location: { protocol: 'http:' } })).toBe(false)
|
|
35
|
+
expect(shouldUseSecureChronogroveCrossDomainCookie({ location: { protocol: 'https:' } })).toBe(true)
|
|
36
|
+
expect(shouldUseSecureChronogroveCrossDomainCookie({ location: {} })).toBe(false)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('setChronogroveCrossDomainColorModeCookie', () => {
|
|
41
|
+
it('no-ops without options.registrableDomain', () => {
|
|
42
|
+
document.cookie = ''
|
|
43
|
+
setChronogroveCrossDomainColorModeCookie('dark')
|
|
44
|
+
setChronogroveCrossDomainColorModeCookie('dark', {})
|
|
45
|
+
setChronogroveCrossDomainColorModeCookie('dark', { registrableDomain: ' ' })
|
|
46
|
+
expect(document.cookie).toBe('')
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('parseChronogroveColorModeCookie', () => {
|
|
51
|
+
it('returns normalized mode when cookie is present', () => {
|
|
52
|
+
expect(
|
|
53
|
+
parseChronogroveColorModeCookie(
|
|
54
|
+
`${CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME}=dark`,
|
|
55
|
+
CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME
|
|
56
|
+
)
|
|
57
|
+
).toBe('dark')
|
|
58
|
+
expect(
|
|
59
|
+
parseChronogroveColorModeCookie(
|
|
60
|
+
`other=1; ${CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME}=default`,
|
|
61
|
+
CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME
|
|
62
|
+
)
|
|
63
|
+
).toBe('default')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('maps light to default', () => {
|
|
67
|
+
expect(
|
|
68
|
+
parseChronogroveColorModeCookie(
|
|
69
|
+
`${CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME}=light`,
|
|
70
|
+
CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME
|
|
71
|
+
)
|
|
72
|
+
).toBe('default')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('returns null for missing or invalid values', () => {
|
|
76
|
+
expect(parseChronogroveColorModeCookie('', CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME)).toBe(null)
|
|
77
|
+
expect(parseChronogroveColorModeCookie('other=1', CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME)).toBe(null)
|
|
78
|
+
expect(
|
|
79
|
+
parseChronogroveColorModeCookie(
|
|
80
|
+
`${CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME}=`,
|
|
81
|
+
CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME
|
|
82
|
+
)
|
|
83
|
+
).toBe(null)
|
|
84
|
+
expect(
|
|
85
|
+
parseChronogroveColorModeCookie(
|
|
86
|
+
`${CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME}=nope`,
|
|
87
|
+
CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME
|
|
88
|
+
)
|
|
89
|
+
).toBe(null)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('falls back to raw when decodeURIComponent throws', () => {
|
|
93
|
+
expect(
|
|
94
|
+
parseChronogroveColorModeCookie(
|
|
95
|
+
`${CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME}=%`,
|
|
96
|
+
CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME
|
|
97
|
+
)
|
|
98
|
+
).toBe(null)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('decodes URI-encoded values', () => {
|
|
102
|
+
expect(
|
|
103
|
+
parseChronogroveColorModeCookie(
|
|
104
|
+
`${CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME}=dark`,
|
|
105
|
+
CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME
|
|
106
|
+
)
|
|
107
|
+
).toBe('dark')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('getChronogroveCrossDomainColorModeFromCookie', () => {
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
document.cookie = `${CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME}=; Path=/; Max-Age=0`
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('reads from document.cookie when called with no arguments', () => {
|
|
117
|
+
document.cookie = `${CHRONOGROVE_CROSS_DOMAIN_COLOR_MODE_COOKIE_NAME}=dark`
|
|
118
|
+
expect(getChronogroveCrossDomainColorModeFromCookie()).toBe('dark')
|
|
119
|
+
})
|
|
120
|
+
})
|