@hagicode/hagilight-core 0.9.1-dev.98.1.e071fc7 → 0.9.2-dev.102.1.daf22c1
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/Footer.astro +19 -1
- package/PromotoBanner.astro +2 -1
- package/README.md +19 -1
- package/dist/build-info-resolver.d.ts +8 -0
- package/dist/build-info-resolver.js +29 -0
- package/dist/build-info.d.ts +19 -0
- package/dist/build-info.js +60 -0
- package/dist/content-images.d.ts +20 -0
- package/dist/content-images.js +205 -12
- package/dist/promoto-banner.d.ts +7 -2
- package/dist/promoto-banner.js +75 -5
- package/package.json +9 -1
package/Footer.astro
CHANGED
|
@@ -3,15 +3,18 @@ import Copyright from './Copyright.astro';
|
|
|
3
3
|
import { siteLinkGaAttributes } from './dist/analytics-events.js';
|
|
4
4
|
import { resolveSiteLinks, type SiteLinksOptions } from './dist/links.js';
|
|
5
5
|
import { resolveRssFooterLinks } from './dist/rss-footer-context.js';
|
|
6
|
+
import { buildInfoFromLocals, buildInfoItems, type BuildInfo } from './dist/build-info.js';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
locale?: string;
|
|
9
10
|
links?: SiteLinksOptions;
|
|
11
|
+
buildInfo?: BuildInfo;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
const { locale: propLocale, links } = Astro.props;
|
|
14
|
+
const { locale: propLocale, links, buildInfo: propBuildInfo } = Astro.props;
|
|
13
15
|
const locale = propLocale ?? Astro.currentLocale ?? 'en-US';
|
|
14
16
|
const footerLinks = resolveRssFooterLinks(Astro.locals, locale, links);
|
|
17
|
+
const buildItems = buildInfoItems(propBuildInfo ?? buildInfoFromLocals(Astro.locals));
|
|
15
18
|
const sitemapOverride = footerLinks.overrides?.sitemap;
|
|
16
19
|
const resolvedLinks = resolveSiteLinks(locale, {
|
|
17
20
|
...footerLinks,
|
|
@@ -96,6 +99,17 @@ const resolvedLinks = resolveSiteLinks(locale, {
|
|
|
96
99
|
<span class="hagilight-footer__attribution"><slot name="attribution" /></span>
|
|
97
100
|
</>
|
|
98
101
|
)}
|
|
102
|
+
{buildItems.map((item) => (
|
|
103
|
+
<>
|
|
104
|
+
{' | '}
|
|
105
|
+
<span
|
|
106
|
+
class="hagilight-footer__build"
|
|
107
|
+
data-build-field={item.field}
|
|
108
|
+
title={item.title}
|
|
109
|
+
data-build-commit={item.title}
|
|
110
|
+
>{item.label}: {item.datetime ? <time datetime={item.datetime}>{item.text}</time> : item.text}</span>
|
|
111
|
+
</>
|
|
112
|
+
))}
|
|
99
113
|
</p>
|
|
100
114
|
</footer>
|
|
101
115
|
|
|
@@ -168,6 +182,10 @@ const resolvedLinks = resolveSiteLinks(locale, {
|
|
|
168
182
|
overflow-wrap: anywhere;
|
|
169
183
|
}
|
|
170
184
|
|
|
185
|
+
.hagilight-footer__build {
|
|
186
|
+
overflow-wrap: anywhere;
|
|
187
|
+
}
|
|
188
|
+
|
|
171
189
|
@media (max-width: 40rem) {
|
|
172
190
|
.hagilight-footer__sections {
|
|
173
191
|
grid-template-columns: 1fr;
|
package/PromotoBanner.astro
CHANGED
|
@@ -98,7 +98,8 @@ const { fallback, locale = Astro.currentLocale ?? 'en' } = Astro.props;
|
|
|
98
98
|
transition: opacity 0.24s ease, visibility 0s linear;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
.hagilight-promoto[data-state='footer-hidden'] .hagilight-promoto__shell
|
|
101
|
+
.hagilight-promoto[data-state='footer-hidden'] .hagilight-promoto__shell,
|
|
102
|
+
.hagilight-promoto[data-state='top-hidden'] .hagilight-promoto__shell {
|
|
102
103
|
opacity: 0;
|
|
103
104
|
visibility: hidden;
|
|
104
105
|
pointer-events: none;
|
package/README.md
CHANGED
|
@@ -35,6 +35,22 @@ import { resolveSiteLinks } from '@hagicode/hagilight-core/links';
|
|
|
35
35
|
const links = resolveSiteLinks('en-US');
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## Build info
|
|
39
|
+
|
|
40
|
+
`@hagicode/hagilight-core/build-info` is browser-safe. It exports the `BuildInfo` type, `normalizeBuildInfo`, `shortCommit`, and `buildInfoItems`, which turn a resolved value into the footer's Branch, Commit, and Built items. `@hagicode/hagilight-core/build-info-resolver` is Node-only: `resolveBuildInfo({ root, env, now })` reads the value for one project. Keep Node-only code out of any module that a browser bundle imports.
|
|
41
|
+
|
|
42
|
+
Each field resolves on its own, in this order:
|
|
43
|
+
|
|
44
|
+
| Field | Order | Fallback |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| Branch | `HAGILIGHT_BUILD_BRANCH`, then `git symbolic-ref --short -q HEAD` | `unknown` |
|
|
47
|
+
| Commit | `HAGILIGHT_BUILD_COMMIT`, then `git rev-parse HEAD` | `unknown` |
|
|
48
|
+
| Built | `now`, in UTC, truncated to whole seconds | none; always set |
|
|
49
|
+
|
|
50
|
+
After trimming, `HAGILIGHT_BUILD_BRANCH` must be nonblank and free of control characters. `HAGILIGHT_BUILD_COMMIT` must be 7 to 64 hexadecimal characters and is stored lowercased. An invalid explicit value falls through to Git. Git runs with a 5-second timeout and its stderr is discarded. A detached HEAD, an unborn branch, a missing `git`, a directory outside any repository, or a timeout yields `unknown` for that field only. The resolver never throws.
|
|
51
|
+
|
|
52
|
+
The footer shows the commit as its first seven characters, with the full hash in the `title` attribute. The core `Footer` renders this line only when it receives a `buildInfo` prop or the `hagilight()` integration from `@hagicode/hagilight/integration` is registered; otherwise it renders no build text.
|
|
53
|
+
|
|
38
54
|
## Content images
|
|
39
55
|
|
|
40
56
|
`@hagicode/hagilight-core/content-images` normalizes the `<img>` elements in Markdown and MDX body content at build time. A body image gets the marker class `hagilight-content-image`, `loading="lazy"`, and `decoding="async"`. Values the author already set are kept, existing classes are kept, and the marker is added once.
|
|
@@ -47,7 +63,7 @@ const links = resolveSiteLinks('en-US');
|
|
|
47
63
|
|
|
48
64
|
Images inside a `<picture>` element are skipped. An MDX `class={expression}` gets no marker because its value is not known at build time. Raw HTML `<img>` tags inside a `.md` file are not normalized; use Markdown image syntax or MDX for body images.
|
|
49
65
|
|
|
50
|
-
Local
|
|
66
|
+
Local body images in `.md` and `.mdx` files that Astro resolves are also wrapped in a `<picture>` element, with an AVIF `<source>` and a WebP `<source>` ahead of the `<img>`. The rule is shared by both file types. An image is wrapped only when all of these hold: it is a relative path (`./` or `../`) that Astro collects as a local file; its extension is not `.svg` or `.gif`, ignoring case, query strings, and fragments; it does not resolve into the project's `public/` directory (`publicDir`), where it keeps its plain `<img>`; the step is enabled; and it is not already inside a `<picture>` or `Picture` the author wrote. A direct import of `rehypeContentImages` or `satteriContentImages` without registration has no `publicDir`, so it skips the `public/` check and wraps every other image that passes. In `.mdx`, the plugin imports Astro's `Picture` component from `astro:assets`, and the inner `<img>` carries the marker class, `loading`, and `decoding`. Site-root, remote, and raw HTML `<img>` images are not wrapped. Astro generates the source URLs through its image service, so AVIF needs a service that can encode it. The default sharp service does. The passthrough no-op service (`astro/assets/services/noop`) does not, so the Astro integrations skip the wrapper there and log one warning. A plugin added by hand does not check the service, and its sources then point at the original bytes. Encoding adds build time, which Astro caches. In `.md`, each `<source>` also carries `src`, `loading`, `decoding`, `width`, and `height` copied from Astro's output; in `.mdx`, `<source>` carries only `srcset` and `type`. Browsers ignore the extra attributes on `source`, but they are not valid HTML there.
|
|
51
67
|
|
|
52
68
|
`hagilight()` from `@hagicode/hagilight-starlight` and `hagilight()` from `@hagicode/hagilight/integration` register the plugin automatically, for the whole site and regardless of integration order. Each one opts out through its `contentImages` option; see those packages' READMEs.
|
|
53
69
|
|
|
@@ -71,6 +87,8 @@ With the unified processor, pass `rehypeContentImages()` in `rehypePlugins` inst
|
|
|
71
87
|
|
|
72
88
|
The shared banner (`PromotoBanner`, `loadActivePromotions`) displays only promotions whose `platforms` array in Index's `/promote.json` includes `web`, in addition to being enabled, inside their activation window, and matched with `/promote_content.json`. This follows the `platforms` flag documented in the Index README. An entry whose `platforms` is missing, empty, or not an array of strings is not displayed, other values such as `hagicode` are ignored, and matching is exact (`Web` does not match). When no promotion remains, the banner shows its site fallback or stays hidden, as before.
|
|
73
89
|
|
|
90
|
+
The banner stays hidden while the page is at the top during a domain's first 30 visits, and while the Footer is in view. Every page load, refresh, and page-to-page navigation counts as a visit; scrolling and hash changes do not. The per-domain counter is stored in the visitor's browser under `hagilight:promoto-banner:visit-count:<hostname>` and is never sent anywhere.
|
|
91
|
+
|
|
74
92
|
A promotion's `image` may also carry `sources`, the alternative image files that Index publishes in the order AVIF, WebP, then the original. The banner renders them as a `<picture>` in that order, with the original as its `<img>`, and skips any entry whose `type` is not an `image/*` media type or whose URL is unsafe. Each `src` is resolved against `https://index.hagicode.com` when the promotion documents load. If the browser fails to load a tier, the banner removes it and the next tier is used. A card without `sources` renders the plain `<img>` as before, and `sources` never affect the dismissal signature, so a dismissed banner stays dismissed when only its image chain changes.
|
|
75
93
|
|
|
76
94
|
## Google Analytics click events
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type BuildInfo } from './build-info.js';
|
|
2
|
+
export interface ResolveBuildInfoOptions {
|
|
3
|
+
/** Site project root. Git runs here, so a site inside a larger repository still finds it. */
|
|
4
|
+
root: string;
|
|
5
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
6
|
+
now?: Date;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveBuildInfo({ root, env, now }: ResolveBuildInfoOptions): BuildInfo;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { normalizeBranch, normalizeCommit, UNKNOWN } from './build-info.js';
|
|
3
|
+
const GIT_TIMEOUT_MS = 5000;
|
|
4
|
+
function readGit(root, args) {
|
|
5
|
+
try {
|
|
6
|
+
return execFileSync('git', args, {
|
|
7
|
+
cwd: root,
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
timeout: GIT_TIMEOUT_MS,
|
|
10
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
11
|
+
windowsHide: true,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function formatBuiltAt(now) {
|
|
19
|
+
return now.toISOString().replace(/\.\d{3}Z$/u, 'Z');
|
|
20
|
+
}
|
|
21
|
+
export function resolveBuildInfo({ root, env = process.env, now = new Date() }) {
|
|
22
|
+
const branch = normalizeBranch(env.HAGILIGHT_BUILD_BRANCH)
|
|
23
|
+
?? normalizeBranch(readGit(root, ['symbolic-ref', '--short', '-q', 'HEAD']))
|
|
24
|
+
?? UNKNOWN;
|
|
25
|
+
const commit = normalizeCommit(env.HAGILIGHT_BUILD_COMMIT)
|
|
26
|
+
?? normalizeCommit(readGit(root, ['rev-parse', 'HEAD']))
|
|
27
|
+
?? UNKNOWN;
|
|
28
|
+
return Object.freeze({ branch, commit, builtAt: formatBuiltAt(now) });
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const UNKNOWN = "unknown";
|
|
2
|
+
export interface BuildInfo {
|
|
3
|
+
readonly branch: string;
|
|
4
|
+
readonly commit: string;
|
|
5
|
+
readonly builtAt: string;
|
|
6
|
+
}
|
|
7
|
+
export interface BuildInfoItem {
|
|
8
|
+
readonly field: 'branch' | 'commit' | 'builtAt';
|
|
9
|
+
readonly label: string;
|
|
10
|
+
readonly text: string;
|
|
11
|
+
readonly title?: string;
|
|
12
|
+
readonly datetime?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function normalizeBranch(value: unknown): string | undefined;
|
|
15
|
+
export declare function normalizeCommit(value: unknown): string | undefined;
|
|
16
|
+
export declare function normalizeBuildInfo(input: unknown): BuildInfo;
|
|
17
|
+
export declare function shortCommit(commit: string): string;
|
|
18
|
+
export declare function buildInfoFromLocals(locals: unknown): BuildInfo | undefined;
|
|
19
|
+
export declare function buildInfoItems(info: BuildInfo | null | undefined): readonly BuildInfoItem[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const UNKNOWN = 'unknown';
|
|
2
|
+
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/u;
|
|
3
|
+
const COMMIT_PATTERN = /^[0-9a-f]{7,64}$/iu;
|
|
4
|
+
const BUILT_AT_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/u;
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
export function normalizeBranch(value) {
|
|
9
|
+
if (typeof value !== 'string')
|
|
10
|
+
return undefined;
|
|
11
|
+
const branch = value.trim();
|
|
12
|
+
return branch && !CONTROL_CHARACTERS.test(branch) ? branch : undefined;
|
|
13
|
+
}
|
|
14
|
+
export function normalizeCommit(value) {
|
|
15
|
+
if (typeof value !== 'string')
|
|
16
|
+
return undefined;
|
|
17
|
+
const commit = value.trim();
|
|
18
|
+
return COMMIT_PATTERN.test(commit) ? commit.toLowerCase() : undefined;
|
|
19
|
+
}
|
|
20
|
+
function normalizeBuiltAt(value) {
|
|
21
|
+
if (typeof value !== 'string' || !BUILT_AT_PATTERN.test(value))
|
|
22
|
+
return undefined;
|
|
23
|
+
return Number.isNaN(Date.parse(value)) ? undefined : value;
|
|
24
|
+
}
|
|
25
|
+
export function normalizeBuildInfo(input) {
|
|
26
|
+
const record = isRecord(input) ? input : {};
|
|
27
|
+
return Object.freeze({
|
|
28
|
+
branch: normalizeBranch(record.branch) ?? UNKNOWN,
|
|
29
|
+
commit: normalizeCommit(record.commit) ?? UNKNOWN,
|
|
30
|
+
builtAt: normalizeBuiltAt(record.builtAt) ?? UNKNOWN,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function shortCommit(commit) {
|
|
34
|
+
return normalizeCommit(commit)?.slice(0, 7) ?? UNKNOWN;
|
|
35
|
+
}
|
|
36
|
+
export function buildInfoFromLocals(locals) {
|
|
37
|
+
if (!isRecord(locals) || locals.hagilightBuild === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
return normalizeBuildInfo(locals.hagilightBuild);
|
|
40
|
+
}
|
|
41
|
+
export function buildInfoItems(info) {
|
|
42
|
+
if (info === undefined || info === null)
|
|
43
|
+
return [];
|
|
44
|
+
const { branch, commit, builtAt } = normalizeBuildInfo(info);
|
|
45
|
+
return [
|
|
46
|
+
{ field: 'branch', label: 'Branch', text: branch },
|
|
47
|
+
{
|
|
48
|
+
field: 'commit',
|
|
49
|
+
label: 'Commit',
|
|
50
|
+
text: shortCommit(commit),
|
|
51
|
+
...(commit === UNKNOWN ? {} : { title: commit }),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
field: 'builtAt',
|
|
55
|
+
label: 'Built',
|
|
56
|
+
text: builtAt,
|
|
57
|
+
...(builtAt === UNKNOWN ? {} : { datetime: builtAt }),
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
}
|
package/dist/content-images.d.ts
CHANGED
|
@@ -36,18 +36,35 @@ export interface ContentImageNode {
|
|
|
36
36
|
properties?: Record<string, unknown>;
|
|
37
37
|
attributes?: MdxAttributeLike[];
|
|
38
38
|
children?: ContentImageNode[];
|
|
39
|
+
/** Source text of an `mdxjsEsm` node. */
|
|
40
|
+
value?: string;
|
|
41
|
+
/** ESTree data of an `mdxjsEsm` node, read by the unified MDX compiler. */
|
|
42
|
+
data?: unknown;
|
|
43
|
+
}
|
|
44
|
+
/** Per-document state for MDX picture output on the Sätteri processor. */
|
|
45
|
+
export interface MdxPictureScope {
|
|
46
|
+
/** Decoded `src` to the identifier its asset import binds. */
|
|
47
|
+
assets: Map<string, string>;
|
|
48
|
+
/** Whether the `Picture` component import has been queued for this document. */
|
|
49
|
+
component: boolean;
|
|
50
|
+
/** Import nodes not yet placed in the document. */
|
|
51
|
+
pending: ContentImageNode[];
|
|
39
52
|
}
|
|
40
53
|
export interface LocalImageData {
|
|
41
54
|
astro?: {
|
|
42
55
|
localImagePaths?: Set<string> | readonly string[];
|
|
43
56
|
};
|
|
57
|
+
/** Sätteri's per-document MDX picture state, created on first use. */
|
|
58
|
+
hagilightContentPicture?: MdxPictureScope;
|
|
44
59
|
}
|
|
45
60
|
export interface SatteriContentImageContext {
|
|
46
61
|
readonly sourceFormat: string;
|
|
62
|
+
readonly fileURL?: URL | undefined;
|
|
47
63
|
readonly data: LocalImageData;
|
|
48
64
|
parent(node: unknown): unknown;
|
|
49
65
|
setProperty(node: unknown, key: string, value: unknown): void;
|
|
50
66
|
replaceNode(node: unknown, newNode: ContentImageNode): void;
|
|
67
|
+
prependChild(node: unknown, childNode: ContentImageNode | ContentImageNode[]): void;
|
|
51
68
|
}
|
|
52
69
|
export interface SatteriContentImageVisitor {
|
|
53
70
|
filter: string[];
|
|
@@ -61,6 +78,8 @@ export interface SatteriContentImagePlugin {
|
|
|
61
78
|
}
|
|
62
79
|
export interface PictureStep {
|
|
63
80
|
enabled: boolean;
|
|
81
|
+
/** Filesystem path of the project's `publicDir`. Images that resolve into it keep their markup. */
|
|
82
|
+
publicDir?: string;
|
|
64
83
|
}
|
|
65
84
|
export interface ContentImageRegistrationInput {
|
|
66
85
|
config: {
|
|
@@ -72,6 +91,7 @@ export interface ContentImageRegistrationInput {
|
|
|
72
91
|
entrypoint?: unknown;
|
|
73
92
|
};
|
|
74
93
|
};
|
|
94
|
+
publicDir?: unknown;
|
|
75
95
|
};
|
|
76
96
|
logger: {
|
|
77
97
|
warn(message: string): void;
|
package/dist/content-images.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
1
3
|
export const CONTENT_IMAGE_CLASS = 'hagilight-content-image';
|
|
2
4
|
function isRecord(value) {
|
|
3
5
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -70,7 +72,7 @@ function isMdxJsx(node) {
|
|
|
70
72
|
}
|
|
71
73
|
function isPicture(node) {
|
|
72
74
|
return (node.type === 'element' && node.tagName === 'picture')
|
|
73
|
-
|| (isMdxJsx(node) && node.name === 'picture');
|
|
75
|
+
|| (isMdxJsx(node) && (node.name === 'picture' || node.name === 'Picture'));
|
|
74
76
|
}
|
|
75
77
|
function upsertMdxAttribute(attributes, name, value) {
|
|
76
78
|
const existing = attributes.find((attribute) => attribute.name === name);
|
|
@@ -109,19 +111,20 @@ function applyUnifiedTree(node, options, inPicture) {
|
|
|
109
111
|
for (const child of node.children ?? [])
|
|
110
112
|
applyUnifiedTree(child, options, childrenInPicture);
|
|
111
113
|
}
|
|
112
|
-
|
|
114
|
+
/** Replaces each `img` outside a picture for which `replace` returns a node. */
|
|
115
|
+
function replaceUnifiedImages(node, inPicture, replace) {
|
|
113
116
|
const children = node.children;
|
|
114
117
|
if (!children)
|
|
115
118
|
return;
|
|
116
119
|
children.forEach((child, index) => {
|
|
117
120
|
if (!inPicture && child.type === 'element' && child.tagName === 'img') {
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
children[index] =
|
|
121
|
+
const replacement = replace(child);
|
|
122
|
+
if (replacement !== undefined) {
|
|
123
|
+
children[index] = replacement;
|
|
121
124
|
return;
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
|
-
|
|
127
|
+
replaceUnifiedImages(child, inPicture || isPicture(child), replace);
|
|
125
128
|
});
|
|
126
129
|
}
|
|
127
130
|
/** Unified rehype plugin for sites whose `markdown.processor` is `unified()`. */
|
|
@@ -130,10 +133,28 @@ export function rehypeContentImages(options = {}, picture = { enabled: true }) {
|
|
|
130
133
|
let pictureCount = 0;
|
|
131
134
|
return (tree, file) => {
|
|
132
135
|
applyUnifiedTree(tree, resolved, false);
|
|
133
|
-
|
|
136
|
+
const format = documentFormat(file?.path);
|
|
137
|
+
if (!picture.enabled || format === undefined)
|
|
134
138
|
return;
|
|
135
139
|
const astro = file?.data?.astro;
|
|
136
|
-
|
|
140
|
+
const documentPath = toFilePath(file?.path);
|
|
141
|
+
const eligibleSrc = (img) => pictureSource(img.properties?.src, astro, picture, documentPath);
|
|
142
|
+
if (format === 'markdown') {
|
|
143
|
+
replaceUnifiedImages(tree, false, (img) => {
|
|
144
|
+
const src = eligibleSrc(img);
|
|
145
|
+
return src === undefined ? undefined : pictureElement(img, src, ++pictureCount);
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const scope = newMdxPictureScope();
|
|
150
|
+
replaceUnifiedImages(tree, false, (img) => {
|
|
151
|
+
const src = eligibleSrc(img);
|
|
152
|
+
if (src === undefined)
|
|
153
|
+
return undefined;
|
|
154
|
+
return pictureComponent(pictureProperties(img.properties ?? {}, resolved), assetIdentifier(scope, src, 'estree'), 'estree');
|
|
155
|
+
});
|
|
156
|
+
if (scope.pending.length > 0)
|
|
157
|
+
tree.children?.unshift(...scope.pending);
|
|
137
158
|
};
|
|
138
159
|
}
|
|
139
160
|
function isInsidePicture(node, ctx) {
|
|
@@ -144,7 +165,12 @@ function isInsidePicture(node, ctx) {
|
|
|
144
165
|
return false;
|
|
145
166
|
}
|
|
146
167
|
const PICTURE_FORMATS = ['avif', 'webp'];
|
|
168
|
+
const PICTURE_FALLBACK_FORMAT = 'webp';
|
|
169
|
+
const PICTURE_COMPONENT = 'HagilightContentPicture';
|
|
170
|
+
const PICTURE_COMPONENT_SOURCE = 'astro:assets';
|
|
147
171
|
const NOOP_IMAGE_SERVICE = 'astro/assets/services/noop';
|
|
172
|
+
/** Extensions the picture step leaves alone: SVG is already vector, and a GIF would lose its animation. */
|
|
173
|
+
const VECTOR_OR_ANIMATED = /\.(?:svg|gif)$/iu;
|
|
148
174
|
function localImageSrc(value, astro) {
|
|
149
175
|
const localPaths = astro?.localImagePaths;
|
|
150
176
|
if (typeof value !== 'string' || localPaths === undefined)
|
|
@@ -153,6 +179,59 @@ function localImageSrc(value, astro) {
|
|
|
153
179
|
const isLocal = localPaths instanceof Set ? localPaths.has(src) : localPaths.includes(src);
|
|
154
180
|
return isLocal ? src : undefined;
|
|
155
181
|
}
|
|
182
|
+
/** A file URL or absolute path as a filesystem path; `undefined` for anything else. */
|
|
183
|
+
function toFilePath(value) {
|
|
184
|
+
const text = value instanceof URL ? value.href : value;
|
|
185
|
+
if (typeof text !== 'string')
|
|
186
|
+
return undefined;
|
|
187
|
+
if (text.startsWith('file:'))
|
|
188
|
+
return fileURLToPath(text);
|
|
189
|
+
return isAbsolute(text) ? text : undefined;
|
|
190
|
+
}
|
|
191
|
+
function documentFormat(filePath) {
|
|
192
|
+
const text = String(filePath ?? '');
|
|
193
|
+
if (text.endsWith('.mdx'))
|
|
194
|
+
return 'mdx';
|
|
195
|
+
if (text.endsWith('.md'))
|
|
196
|
+
return 'markdown';
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
function withoutQueryOrFragment(src) {
|
|
200
|
+
return src.replace(/[?#].*$/u, '');
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The file a relative reference names, resolved against the document's directory.
|
|
204
|
+
* Site-root, bare and aliased specifiers, and a missing document location, resolve to `undefined`.
|
|
205
|
+
*/
|
|
206
|
+
function resolveLocalFile(src, documentPath) {
|
|
207
|
+
if (documentPath === undefined)
|
|
208
|
+
return undefined;
|
|
209
|
+
const file = withoutQueryOrFragment(src);
|
|
210
|
+
if (!file.startsWith('./') && !file.startsWith('../'))
|
|
211
|
+
return undefined;
|
|
212
|
+
return resolve(dirname(documentPath), file);
|
|
213
|
+
}
|
|
214
|
+
function isInsideDirectory(directory, file) {
|
|
215
|
+
const offset = relative(directory, file);
|
|
216
|
+
return offset !== '..' && !offset.startsWith(`..${sep}`) && !isAbsolute(offset);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The decoded `src` when the picture step may wrap this image, otherwise `undefined`. One rule for
|
|
220
|
+
* `.md` and `.mdx`: collected by Astro as local, not SVG or GIF, not inside `publicDir`, step enabled.
|
|
221
|
+
* Without a document location or a `publicDir`, the `public/` check is skipped.
|
|
222
|
+
*/
|
|
223
|
+
function pictureSource(value, astro, picture, documentPath) {
|
|
224
|
+
if (!picture.enabled)
|
|
225
|
+
return undefined;
|
|
226
|
+
const src = localImageSrc(value, astro);
|
|
227
|
+
if (src === undefined || VECTOR_OR_ANIMATED.test(withoutQueryOrFragment(src)))
|
|
228
|
+
return undefined;
|
|
229
|
+
const file = resolveLocalFile(src, documentPath);
|
|
230
|
+
if (picture.publicDir !== undefined && file !== undefined && isInsideDirectory(picture.publicDir, file)) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
return src;
|
|
234
|
+
}
|
|
156
235
|
function pictureElement(img, src, index) {
|
|
157
236
|
const sources = PICTURE_FORMATS.map((format) => ({
|
|
158
237
|
type: 'element',
|
|
@@ -170,6 +249,106 @@ function pictureElement(img, src, index) {
|
|
|
170
249
|
}));
|
|
171
250
|
return { type: 'element', tagName: 'picture', properties: {}, children: [...sources, img] };
|
|
172
251
|
}
|
|
252
|
+
function estreeProgram(body) {
|
|
253
|
+
return { type: 'Program', sourceType: 'module', comments: [], body };
|
|
254
|
+
}
|
|
255
|
+
function jsxAttributeNode(attribute, flavor) {
|
|
256
|
+
if ('text' in attribute)
|
|
257
|
+
return { type: 'mdxJsxAttribute', name: attribute.name, value: attribute.text };
|
|
258
|
+
const expression = 'identifier' in attribute ? attribute.identifier : JSON.stringify(attribute.list);
|
|
259
|
+
const value = { type: 'mdxJsxAttributeValueExpression', value: expression };
|
|
260
|
+
if (flavor === 'estree') {
|
|
261
|
+
const node = 'identifier' in attribute
|
|
262
|
+
? { type: 'Identifier', name: attribute.identifier }
|
|
263
|
+
: {
|
|
264
|
+
type: 'ArrayExpression',
|
|
265
|
+
elements: attribute.list.map((item) => ({ type: 'Literal', value: item, raw: JSON.stringify(item) })),
|
|
266
|
+
};
|
|
267
|
+
value.data = { estree: estreeProgram([{ type: 'ExpressionStatement', expression: node }]) };
|
|
268
|
+
}
|
|
269
|
+
return { type: 'mdxJsxAttribute', name: attribute.name, value };
|
|
270
|
+
}
|
|
271
|
+
function importNode(flavor, text, declaration) {
|
|
272
|
+
if (flavor === 'source')
|
|
273
|
+
return { type: 'mdxjsEsm', value: text };
|
|
274
|
+
return { type: 'mdxjsEsm', value: '', data: { estree: estreeProgram([declaration]) } };
|
|
275
|
+
}
|
|
276
|
+
function newMdxPictureScope() {
|
|
277
|
+
return { assets: new Map(), component: false, pending: [] };
|
|
278
|
+
}
|
|
279
|
+
/** The identifier an asset import binds for `src`, queuing the imports the first time each is needed. */
|
|
280
|
+
function assetIdentifier(scope, src, flavor) {
|
|
281
|
+
const existing = scope.assets.get(src);
|
|
282
|
+
if (existing !== undefined)
|
|
283
|
+
return existing;
|
|
284
|
+
const name = `HagilightContentImage${scope.assets.size}`;
|
|
285
|
+
if (!scope.component) {
|
|
286
|
+
scope.component = true;
|
|
287
|
+
scope.pending.push(importNode(flavor, `import { Picture as ${PICTURE_COMPONENT} } from "${PICTURE_COMPONENT_SOURCE}";`, {
|
|
288
|
+
type: 'ImportDeclaration',
|
|
289
|
+
attributes: [],
|
|
290
|
+
source: { type: 'Literal', value: PICTURE_COMPONENT_SOURCE, raw: JSON.stringify(PICTURE_COMPONENT_SOURCE) },
|
|
291
|
+
specifiers: [{
|
|
292
|
+
type: 'ImportSpecifier',
|
|
293
|
+
imported: { type: 'Identifier', name: 'Picture' },
|
|
294
|
+
local: { type: 'Identifier', name: PICTURE_COMPONENT },
|
|
295
|
+
}],
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
scope.pending.push(importNode(flavor, `import ${name} from ${JSON.stringify(src)};`, {
|
|
299
|
+
type: 'ImportDeclaration',
|
|
300
|
+
attributes: [],
|
|
301
|
+
source: { type: 'Literal', value: src, raw: JSON.stringify(src) },
|
|
302
|
+
specifiers: [{ type: 'ImportDefaultSpecifier', local: { type: 'Identifier', name } }],
|
|
303
|
+
}));
|
|
304
|
+
scope.assets.set(src, name);
|
|
305
|
+
return name;
|
|
306
|
+
}
|
|
307
|
+
/** The image's properties after the baseline patch, with `alt` present, as `Picture` receives them. */
|
|
308
|
+
function pictureProperties(properties, options) {
|
|
309
|
+
const patch = hastImagePatch(properties, options);
|
|
310
|
+
const next = { ...properties };
|
|
311
|
+
if (patch.loading !== undefined)
|
|
312
|
+
next.loading = patch.loading;
|
|
313
|
+
if (patch.decoding !== undefined)
|
|
314
|
+
next.decoding = patch.decoding;
|
|
315
|
+
if (patch.classNames !== undefined)
|
|
316
|
+
next.className = patch.classNames;
|
|
317
|
+
if (next.alt === undefined || next.alt === null)
|
|
318
|
+
next.alt = '';
|
|
319
|
+
return next;
|
|
320
|
+
}
|
|
321
|
+
/** The `Picture` element an author would write by hand (D2), with the image's own attributes. */
|
|
322
|
+
function pictureComponent(properties, identifier, flavor) {
|
|
323
|
+
const attributes = [
|
|
324
|
+
{ name: 'src', identifier },
|
|
325
|
+
{ name: 'formats', list: [...PICTURE_FORMATS] },
|
|
326
|
+
{ name: 'fallbackFormat', text: PICTURE_FALLBACK_FORMAT },
|
|
327
|
+
];
|
|
328
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
329
|
+
if (key === 'src' || value === undefined || value === null || value === false)
|
|
330
|
+
continue;
|
|
331
|
+
const name = key === 'className' ? 'class' : key === 'htmlFor' ? 'for' : key;
|
|
332
|
+
attributes.push({ name, text: Array.isArray(value) ? value.map(String).join(' ') : String(value) });
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
type: 'mdxJsxFlowElement',
|
|
336
|
+
name: PICTURE_COMPONENT,
|
|
337
|
+
attributes: attributes.map((attribute) => jsxAttributeNode(attribute, flavor)),
|
|
338
|
+
children: [],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/** The root of the document, which holds the ESM imports a `Picture` needs. */
|
|
342
|
+
function documentRoot(node, ctx) {
|
|
343
|
+
let current = node;
|
|
344
|
+
for (let parent = ctx.parent(current); parent; parent = ctx.parent(current))
|
|
345
|
+
current = parent;
|
|
346
|
+
return current;
|
|
347
|
+
}
|
|
348
|
+
function mdxScope(data) {
|
|
349
|
+
data.hagilightContentPicture ??= newMdxPictureScope();
|
|
350
|
+
return data.hagilightContentPicture;
|
|
351
|
+
}
|
|
173
352
|
/** Sätteri hast plugin: Astro's default Markdown processor, through `hastPlugins`. */
|
|
174
353
|
export function satteriContentImages(options = {}, picture = { enabled: true }) {
|
|
175
354
|
const resolved = resolveContentImageOptions(options);
|
|
@@ -195,6 +374,19 @@ export function satteriContentImages(options = {}, picture = { enabled: true })
|
|
|
195
374
|
visit(node, ctx) {
|
|
196
375
|
if (isInsidePicture(node, ctx))
|
|
197
376
|
return;
|
|
377
|
+
const isDocument = ctx.sourceFormat === 'markdown' || ctx.sourceFormat === 'mdx';
|
|
378
|
+
const src = isDocument
|
|
379
|
+
? pictureSource(node.properties?.src, ctx.data.astro, picture, toFilePath(ctx.fileURL))
|
|
380
|
+
: undefined;
|
|
381
|
+
if (src !== undefined && ctx.sourceFormat === 'mdx') {
|
|
382
|
+
const root = documentRoot(node, ctx);
|
|
383
|
+
const scope = mdxScope(ctx.data);
|
|
384
|
+
const identifier = assetIdentifier(scope, src, 'source');
|
|
385
|
+
ctx.replaceNode(node, pictureComponent(pictureProperties(node.properties ?? {}, resolved), identifier, 'source'));
|
|
386
|
+
if (scope.pending.length > 0)
|
|
387
|
+
ctx.prependChild(root, scope.pending.splice(0));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
198
390
|
const patch = hastImagePatch(node.properties ?? {}, resolved);
|
|
199
391
|
if (patch.loading !== undefined)
|
|
200
392
|
ctx.setProperty(node, 'loading', patch.loading);
|
|
@@ -202,10 +394,8 @@ export function satteriContentImages(options = {}, picture = { enabled: true })
|
|
|
202
394
|
ctx.setProperty(node, 'decoding', patch.decoding);
|
|
203
395
|
if (patch.classNames !== undefined)
|
|
204
396
|
ctx.setProperty(node, 'className', patch.classNames);
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
if (src !== undefined)
|
|
208
|
-
ctx.replaceNode(node, pictureElement(node, src, ++pictureCount));
|
|
397
|
+
if (src !== undefined && ctx.sourceFormat === 'markdown') {
|
|
398
|
+
ctx.replaceNode(node, pictureElement(node, src, ++pictureCount));
|
|
209
399
|
}
|
|
210
400
|
},
|
|
211
401
|
},
|
|
@@ -268,6 +458,9 @@ export function registerContentImages({ config, logger, enabled }) {
|
|
|
268
458
|
return;
|
|
269
459
|
}
|
|
270
460
|
const picture = { enabled: true };
|
|
461
|
+
const publicDir = toFilePath(config.publicDir);
|
|
462
|
+
if (publicDir !== undefined)
|
|
463
|
+
picture.publicDir = publicDir;
|
|
271
464
|
const plugin = processor.name === 'satteri'
|
|
272
465
|
? satteriContentImages({}, picture)
|
|
273
466
|
: (options) => rehypeContentImages(options, picture);
|
package/dist/promoto-banner.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { type PromotionCard, type PromotionImage, type PromotionImageSource } from './promotions.js';
|
|
2
|
-
export type PromotionVisibility = 'dismissed' | 'footer-hidden' | 'hidden' | 'ready';
|
|
2
|
+
export type PromotionVisibility = 'dismissed' | 'footer-hidden' | 'hidden' | 'ready' | 'top-hidden';
|
|
3
3
|
export declare function selectPromotionCards(remote: PromotionCard[], fallback?: PromotionCard | null): PromotionCard[];
|
|
4
4
|
export declare function getPromotionImageSources(image: PromotionImage | undefined): PromotionImageSource[];
|
|
5
5
|
export declare function getPromotionSetSignature(cards: PromotionCard[]): string | null;
|
|
6
|
-
export declare function
|
|
6
|
+
export declare function isScrolledToTop(scrollY: number): boolean;
|
|
7
|
+
export declare function isPageTopRuleActive(visitCount: number): boolean;
|
|
8
|
+
export declare function getVisitCountKey(hostname: string): string;
|
|
9
|
+
export declare function parseVisitCount(raw: string | null): number;
|
|
10
|
+
export declare function getPromotionVisibility(cards: PromotionCard[], signature: string | null, dismissedSignature: string | null, footerVisible: boolean, atPageTop?: boolean): PromotionVisibility;
|
|
7
11
|
export declare function getNextPromotionIndex(index: number, count: number, direction: -1 | 1): number;
|
|
8
12
|
export declare function shouldAutoRotate(count: number, paused: boolean, reducedMotion: boolean, documentVisible: boolean, visibility: PromotionVisibility): boolean;
|
|
13
|
+
export declare function recordSiteVisit(): number;
|
|
9
14
|
export declare function definePromotoBannerElement(): void;
|
package/dist/promoto-banner.js
CHANGED
|
@@ -2,8 +2,13 @@ import { gaEventAttributes } from './analytics-events.js';
|
|
|
2
2
|
import { loadActivePromotions, } from './promotions.js';
|
|
3
3
|
const ELEMENT_NAME = 'hagilight-promoto-banner';
|
|
4
4
|
const DISMISSED_SIGNATURE_KEY = 'hagilight:promoto-banner:dismissed-signature';
|
|
5
|
+
const VISIT_COUNT_KEY_PREFIX = 'hagilight:promoto-banner:visit-count:';
|
|
6
|
+
const VISIT_EVENT = 'hagilight:promoto-visit';
|
|
7
|
+
const PAGE_TOP_VISIT_LIMIT = 30;
|
|
5
8
|
const ROTATION_INTERVAL_MS = 8000;
|
|
6
9
|
const REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
10
|
+
let lastCountedPage = null;
|
|
11
|
+
let siteVisitCount = 0;
|
|
7
12
|
function isRecord(value) {
|
|
8
13
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
14
|
}
|
|
@@ -67,12 +72,29 @@ export function getPromotionSetSignature(cards) {
|
|
|
67
72
|
image?.variant ?? '', image?.width ?? 0, image?.height ?? 0,
|
|
68
73
|
]));
|
|
69
74
|
}
|
|
70
|
-
export function
|
|
75
|
+
export function isScrolledToTop(scrollY) {
|
|
76
|
+
return scrollY <= 1;
|
|
77
|
+
}
|
|
78
|
+
export function isPageTopRuleActive(visitCount) {
|
|
79
|
+
return visitCount <= PAGE_TOP_VISIT_LIMIT;
|
|
80
|
+
}
|
|
81
|
+
export function getVisitCountKey(hostname) {
|
|
82
|
+
return `${VISIT_COUNT_KEY_PREFIX}${hostname}`;
|
|
83
|
+
}
|
|
84
|
+
export function parseVisitCount(raw) {
|
|
85
|
+
if (raw === null || !/^\d+$/u.test(raw))
|
|
86
|
+
return 0;
|
|
87
|
+
const count = Number(raw);
|
|
88
|
+
return Number.isSafeInteger(count) ? count : 0;
|
|
89
|
+
}
|
|
90
|
+
export function getPromotionVisibility(cards, signature, dismissedSignature, footerVisible, atPageTop = false) {
|
|
71
91
|
if (!cards.length)
|
|
72
92
|
return 'hidden';
|
|
73
93
|
if (signature && signature === dismissedSignature)
|
|
74
94
|
return 'dismissed';
|
|
75
|
-
|
|
95
|
+
if (footerVisible)
|
|
96
|
+
return 'footer-hidden';
|
|
97
|
+
return atPageTop ? 'top-hidden' : 'ready';
|
|
76
98
|
}
|
|
77
99
|
export function getNextPromotionIndex(index, count, direction) {
|
|
78
100
|
return count > 0 ? (index + direction + count) % count : 0;
|
|
@@ -96,6 +118,48 @@ function saveDismissedSignature(signature) {
|
|
|
96
118
|
console.warn('Unable to save the dismissed promotion preference.', error);
|
|
97
119
|
}
|
|
98
120
|
}
|
|
121
|
+
function getLocalStorage() {
|
|
122
|
+
try {
|
|
123
|
+
return window.localStorage || null;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function countVisit(storage, key) {
|
|
130
|
+
let count;
|
|
131
|
+
try {
|
|
132
|
+
count = parseVisitCount(storage.getItem(key));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
if (!isPageTopRuleActive(count))
|
|
138
|
+
return count;
|
|
139
|
+
try {
|
|
140
|
+
storage.setItem(key, String(count + 1));
|
|
141
|
+
return count + 1;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return count;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export function recordSiteVisit() {
|
|
148
|
+
if (typeof window === 'undefined' || !window.location)
|
|
149
|
+
return siteVisitCount;
|
|
150
|
+
const page = `${window.location.pathname}${window.location.search}`;
|
|
151
|
+
if (page === lastCountedPage)
|
|
152
|
+
return siteVisitCount;
|
|
153
|
+
const storage = getLocalStorage();
|
|
154
|
+
if (!storage)
|
|
155
|
+
return siteVisitCount;
|
|
156
|
+
lastCountedPage = page;
|
|
157
|
+
siteVisitCount = countVisit(storage, getVisitCountKey(window.location.hostname));
|
|
158
|
+
if (typeof document !== 'undefined' && typeof document.dispatchEvent === 'function') {
|
|
159
|
+
document.dispatchEvent(new Event(VISIT_EVENT));
|
|
160
|
+
}
|
|
161
|
+
return siteVisitCount;
|
|
162
|
+
}
|
|
99
163
|
export function definePromotoBannerElement() {
|
|
100
164
|
if (typeof customElements === 'undefined' || typeof HTMLElement === 'undefined'
|
|
101
165
|
|| customElements.get(ELEMENT_NAME))
|
|
@@ -152,6 +216,7 @@ export function definePromotoBannerElement() {
|
|
|
152
216
|
this.syncRotation();
|
|
153
217
|
};
|
|
154
218
|
onPageLoad = () => {
|
|
219
|
+
recordSiteVisit();
|
|
155
220
|
// Re-fetch in the current page locale after a Starlight view-transition
|
|
156
221
|
// navigation. The element may persist across a soft language switch, so
|
|
157
222
|
// its previously loaded locale can differ from the new page's data-locale.
|
|
@@ -159,6 +224,7 @@ export function definePromotoBannerElement() {
|
|
|
159
224
|
void this.reload();
|
|
160
225
|
};
|
|
161
226
|
connectedCallback() {
|
|
227
|
+
recordSiteVisit();
|
|
162
228
|
this.footer = document.querySelector('footer');
|
|
163
229
|
this.motionQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
164
230
|
this.reducedMotion = this.motionQuery?.matches ?? false;
|
|
@@ -171,6 +237,7 @@ export function definePromotoBannerElement() {
|
|
|
171
237
|
document.addEventListener('visibilitychange', this.onVisibilityChange);
|
|
172
238
|
this.motionQuery?.addEventListener('change', this.onMotionChange);
|
|
173
239
|
document.addEventListener('astro:page-load', this.onPageLoad);
|
|
240
|
+
document.addEventListener(VISIT_EVENT, this.onLayoutChange);
|
|
174
241
|
if (typeof ResizeObserver !== 'undefined') {
|
|
175
242
|
this.resizeObserver = new ResizeObserver(this.onLayoutChange);
|
|
176
243
|
if (this.footer)
|
|
@@ -192,6 +259,7 @@ export function definePromotoBannerElement() {
|
|
|
192
259
|
document.removeEventListener('visibilitychange', this.onVisibilityChange);
|
|
193
260
|
this.motionQuery?.removeEventListener('change', this.onMotionChange);
|
|
194
261
|
document.removeEventListener('astro:page-load', this.onPageLoad);
|
|
262
|
+
document.removeEventListener(VISIT_EVENT, this.onLayoutChange);
|
|
195
263
|
this.resizeObserver?.disconnect();
|
|
196
264
|
this.stopRotation();
|
|
197
265
|
this.stopRefresh();
|
|
@@ -341,13 +409,15 @@ export function definePromotoBannerElement() {
|
|
|
341
409
|
}
|
|
342
410
|
updateVisibility() {
|
|
343
411
|
const signature = getPromotionSetSignature(this.cards);
|
|
344
|
-
const
|
|
412
|
+
const atPageTop = isScrolledToTop(window.scrollY ?? document.documentElement.scrollTop)
|
|
413
|
+
&& isPageTopRuleActive(siteVisitCount);
|
|
414
|
+
const state = getPromotionVisibility(this.cards, signature, this.dismissedSignature, this.isFooterVisible(), atPageTop);
|
|
345
415
|
const hidden = state === 'hidden' || state === 'dismissed';
|
|
346
416
|
const shell = this.querySelector('[data-promoto-shell]');
|
|
347
417
|
this.hidden = hidden;
|
|
348
418
|
if (shell) {
|
|
349
419
|
shell.hidden = hidden;
|
|
350
|
-
shell.inert =
|
|
420
|
+
shell.inert = state !== 'ready';
|
|
351
421
|
}
|
|
352
422
|
this.dataset.state = state;
|
|
353
423
|
}
|
|
@@ -360,7 +430,7 @@ export function definePromotoBannerElement() {
|
|
|
360
430
|
syncRotation() {
|
|
361
431
|
this.stopRotation();
|
|
362
432
|
const state = this.dataset.state;
|
|
363
|
-
if (state !== 'ready' && state !== 'footer-hidden' && state !== 'hidden' && state !== 'dismissed')
|
|
433
|
+
if (state !== 'ready' && state !== 'footer-hidden' && state !== 'top-hidden' && state !== 'hidden' && state !== 'dismissed')
|
|
364
434
|
return;
|
|
365
435
|
if (!shouldAutoRotate(this.cards.length, this.paused, this.reducedMotion, document.visibilityState !== 'hidden', state))
|
|
366
436
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hagicode/hagilight-core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2-dev.102.1.daf22c1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared TypeScript utilities, components, and assets for HagiCode Astro and Starlight sites",
|
|
6
6
|
"repository": {
|
|
@@ -58,6 +58,14 @@
|
|
|
58
58
|
"types": "./dist/content-images.d.ts",
|
|
59
59
|
"default": "./dist/content-images.js"
|
|
60
60
|
},
|
|
61
|
+
"./build-info": {
|
|
62
|
+
"types": "./dist/build-info.d.ts",
|
|
63
|
+
"default": "./dist/build-info.js"
|
|
64
|
+
},
|
|
65
|
+
"./build-info-resolver": {
|
|
66
|
+
"types": "./dist/build-info-resolver.d.ts",
|
|
67
|
+
"default": "./dist/build-info-resolver.js"
|
|
68
|
+
},
|
|
61
69
|
"./Copyright": "./Copyright.astro",
|
|
62
70
|
"./PromotoBanner": "./PromotoBanner.astro",
|
|
63
71
|
"./GoogleAnalytics": "./GoogleAnalytics.astro",
|