@tetherto/dev-websites-core 0.3.0 → 0.3.1
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 +20 -1
- package/dist/analytics/GtmLoader.d.mts +14 -0
- package/dist/analytics/GtmLoader.d.mts.map +1 -0
- package/dist/analytics/GtmLoader.mjs +30 -0
- package/dist/analytics/GtmLoader.mjs.map +1 -0
- package/dist/analytics/createTrackedLink.d.mts +46 -0
- package/dist/analytics/createTrackedLink.d.mts.map +1 -0
- package/dist/analytics/createTrackedLink.mjs +36 -0
- package/dist/analytics/createTrackedLink.mjs.map +1 -0
- package/dist/analytics/index.d.mts +3 -0
- package/dist/analytics/index.mjs +3 -0
- package/dist/ui/components/Badge/Badge.d.mts +1 -1
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@tetherto/dev-websites-core`
|
|
2
2
|
|
|
3
|
-
Shared CMS, SEO, i18n, utils, and
|
|
3
|
+
Shared CMS, SEO, i18n, utils, UI, and analytics primitives for WDK / QVAC / PEAR / MDK websites.
|
|
4
4
|
|
|
5
5
|
| Import | Contents |
|
|
6
6
|
| --- | --- |
|
|
@@ -8,6 +8,7 @@ Shared CMS, SEO, i18n, utils, and UI primitives for WDK / QVAC websites.
|
|
|
8
8
|
| `@tetherto/dev-websites-core/seo` | `createMetadataFactory`, `getSiteUrl`, `getOgBackgroundDataUrl` |
|
|
9
9
|
| `@tetherto/dev-websites-core/i18n` | `Languages` / `LanguageLabel` |
|
|
10
10
|
| `@tetherto/dev-websites-core/utils` | `createLogger`, `toPlainValue`, `stripShikiPreBackground` |
|
|
11
|
+
| `@tetherto/dev-websites-core/analytics` | `GtmLoader`, `createTrackedLink` (optional `@next/third-parties` peer) |
|
|
11
12
|
| `@tetherto/dev-websites-core/ui` | UI primitives (`Button`, `Dialog`, `Pagination`, `Tooltip`, …) + `cn` |
|
|
12
13
|
| `@tetherto/dev-websites-core/ui/a11y` | `focusRingClassName`, `VisuallyHidden` |
|
|
13
14
|
| `@tetherto/dev-websites-core/ui/carousel` | `Carousel` (+ optional `embla-carousel-react` peer) |
|
|
@@ -50,6 +51,24 @@ rm -rf node_modules/@tetherto/dev-websites-core && npm install @tetherto/dev-web
|
|
|
50
51
|
Do **not** import `@tetherto/dev-websites-core/cms` from Client Components — it includes Node-only
|
|
51
52
|
modules (`fs`, Payload, S3). Prefer domain subpaths over the root barrel.
|
|
52
53
|
|
|
54
|
+
### Analytics
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { GtmLoader, createTrackedLink } from '@tetherto/dev-websites-core/analytics'
|
|
58
|
+
import { Link } from '@/i18n/navigation'
|
|
59
|
+
import { analytics } from '@/lib/analytics'
|
|
60
|
+
|
|
61
|
+
// Delay GTM until window.load (optional `@next/third-parties` peer)
|
|
62
|
+
<GtmLoader gtmId={gtmId} />
|
|
63
|
+
|
|
64
|
+
// App owns Link + event catalog; core owns click wiring
|
|
65
|
+
export const TrackedLink = createTrackedLink(Link, {
|
|
66
|
+
docsClicked: analytics.docsClicked,
|
|
67
|
+
getStartedClicked: analytics.getStartedClicked,
|
|
68
|
+
externalLinkClicked: analytics.externalLinkClicked,
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
53
72
|
### UI theming
|
|
54
73
|
|
|
55
74
|
One **Tailwind theme contract** for all sites (same utility names). Defaults are
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/analytics/GtmLoader.d.ts
|
|
2
|
+
type GtmLoaderProps = {
|
|
3
|
+
gtmId: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Wraps `@next/third-parties` `GoogleTagManager` and delays mounting it until
|
|
7
|
+
* the page has fully loaded (`window.load`). The Next.js helper itself uses
|
|
8
|
+
* `next/script` with `afterInteractive`, which would otherwise inject GTM
|
|
9
|
+
* during hydration and contribute to TBT.
|
|
10
|
+
*/
|
|
11
|
+
declare function GtmLoader({ gtmId }: GtmLoaderProps): import("react").JSX.Element | null;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { GtmLoader, GtmLoaderProps };
|
|
14
|
+
//# sourceMappingURL=GtmLoader.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GtmLoader.d.mts","names":[],"sources":["../../src/analytics/GtmLoader.tsx"],"mappings":";KAKY;EACV;;;;;;;;iBASc,YAAY,SAAS,iCAAc,IAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { GoogleTagManager } from "@next/third-parties/google";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/analytics/GtmLoader.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Wraps `@next/third-parties` `GoogleTagManager` and delays mounting it until
|
|
8
|
+
* the page has fully loaded (`window.load`). The Next.js helper itself uses
|
|
9
|
+
* `next/script` with `afterInteractive`, which would otherwise inject GTM
|
|
10
|
+
* during hydration and contribute to TBT.
|
|
11
|
+
*/
|
|
12
|
+
function GtmLoader({ gtmId }) {
|
|
13
|
+
const [ready, setReady] = useState(false);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!gtmId) return;
|
|
16
|
+
const onLoad = () => setReady(true);
|
|
17
|
+
if (document.readyState === "complete") {
|
|
18
|
+
const id = window.setTimeout(onLoad, 0);
|
|
19
|
+
return () => window.clearTimeout(id);
|
|
20
|
+
}
|
|
21
|
+
window.addEventListener("load", onLoad, { once: true });
|
|
22
|
+
return () => window.removeEventListener("load", onLoad);
|
|
23
|
+
}, [gtmId]);
|
|
24
|
+
if (!ready) return null;
|
|
25
|
+
return /* @__PURE__ */ jsx(GoogleTagManager, { gtmId });
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { GtmLoader };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=GtmLoader.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GtmLoader.mjs","names":[],"sources":["../../src/analytics/GtmLoader.tsx"],"sourcesContent":["'use client'\n\nimport { GoogleTagManager } from '@next/third-parties/google'\nimport { useEffect, useState } from 'react'\n\nexport type GtmLoaderProps = {\n gtmId: string\n}\n\n/**\n * Wraps `@next/third-parties` `GoogleTagManager` and delays mounting it until\n * the page has fully loaded (`window.load`). The Next.js helper itself uses\n * `next/script` with `afterInteractive`, which would otherwise inject GTM\n * during hydration and contribute to TBT.\n */\nexport function GtmLoader({ gtmId }: GtmLoaderProps) {\n const [ready, setReady] = useState(false)\n\n useEffect(() => {\n if (!gtmId) return\n\n const onLoad = () => setReady(true)\n\n if (document.readyState === 'complete') {\n // Defer one tick so this doesn't fire a synchronous setState during the\n // effect (and to give the browser a frame to settle after `load`).\n const id = window.setTimeout(onLoad, 0)\n return () => window.clearTimeout(id)\n }\n\n window.addEventListener('load', onLoad, { once: true })\n return () => window.removeEventListener('load', onLoad)\n }, [gtmId])\n\n if (!ready) return null\n return <GoogleTagManager gtmId={gtmId} />\n}\n"],"mappings":";;;;;;;;;;;AAeA,SAAgB,UAAU,EAAE,SAAyB;CACnD,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CAExC,gBAAgB;EACd,IAAI,CAAC,OAAO;EAEZ,MAAM,eAAe,SAAS,IAAI;EAElC,IAAI,SAAS,eAAe,YAAY;GAGtC,MAAM,KAAK,OAAO,WAAW,QAAQ,CAAC;GACtC,aAAa,OAAO,aAAa,EAAE;EACrC;EAEA,OAAO,iBAAiB,QAAQ,QAAQ,EAAE,MAAM,KAAK,CAAC;EACtD,aAAa,OAAO,oBAAoB,QAAQ,MAAM;CACxD,GAAG,CAAC,KAAK,CAAC;CAEV,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO,oBAAC,kBAAD,EAAyB,MAAQ,CAAA;AAC1C"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ComponentPropsWithoutRef, ElementType, ReactElement } from "react";
|
|
2
|
+
//#region src/analytics/createTrackedLink.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Discriminated tracking props. Apps keep tight `Section` / destination unions;
|
|
5
|
+
* pass them as generics to `createTrackedLink`.
|
|
6
|
+
*/
|
|
7
|
+
type CreateTrackedEvent<TSection extends string, TDestination extends string> = {
|
|
8
|
+
event?: never;
|
|
9
|
+
section?: never;
|
|
10
|
+
destination?: never;
|
|
11
|
+
} | {
|
|
12
|
+
event: 'docs_clicked';
|
|
13
|
+
section: TSection;
|
|
14
|
+
destination?: string;
|
|
15
|
+
} | {
|
|
16
|
+
event: 'get_started_clicked';
|
|
17
|
+
section: TSection;
|
|
18
|
+
destination?: never;
|
|
19
|
+
} | {
|
|
20
|
+
event: 'external_link_clicked';
|
|
21
|
+
section: TSection;
|
|
22
|
+
destination: TDestination;
|
|
23
|
+
};
|
|
24
|
+
type TrackedCapture<TSection extends string, TDestination extends string> = {
|
|
25
|
+
docsClicked: (p: {
|
|
26
|
+
section: TSection;
|
|
27
|
+
destination?: string;
|
|
28
|
+
}) => void;
|
|
29
|
+
getStartedClicked: (p: {
|
|
30
|
+
section: TSection;
|
|
31
|
+
}) => void;
|
|
32
|
+
externalLinkClicked: (p: {
|
|
33
|
+
destination: TDestination;
|
|
34
|
+
section: TSection;
|
|
35
|
+
}) => void;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Builds a site `TrackedLink` that wraps the app's Link (`next-intl` navigation
|
|
39
|
+
* Link or `next/link`) and fires the provided capture callbacks.
|
|
40
|
+
*
|
|
41
|
+
* Routing and event catalogs stay in the app — core only owns the click wiring.
|
|
42
|
+
*/
|
|
43
|
+
declare function createTrackedLink<TLink extends ElementType, TSection extends string = string, TDestination extends string = string>(Link: TLink, capture: TrackedCapture<TSection, TDestination>): (props: ComponentPropsWithoutRef<TLink> & CreateTrackedEvent<TSection, TDestination>) => ReactElement;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { CreateTrackedEvent, type CreateTrackedEvent as TrackedEvent, TrackedCapture, createTrackedLink };
|
|
46
|
+
//# sourceMappingURL=createTrackedLink.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createTrackedLink.d.mts","names":[],"sources":["../../src/analytics/createTrackedLink.tsx"],"mappings":";;;;;;KAcY,mBAAmB,yBAAyB;EAClD;EAAe;EAAiB;;EAChC;EAAuB,SAAS;EAAU;;EAC1C;EAA8B,SAAS;EAAU;;EAEjD;EACA,SAAS;EACT,aAAa;;KAGP,eAAe,yBAAyB;EAClD,cAAc;IAAK,SAAS;IAAU;;EACtC,oBAAoB;IAAK,SAAS;;EAClC,sBAAsB;IAAK,aAAa;IAAc,SAAS;;;;;;;;;iBASjD,kBACd,cAAc,aACd,kCACA,sCACA,MAAM,OAAO,SAAS,eAAe,UAAU,iBAsCxB,OAAK,yBAAA,SAAA,mBAAA,UAAA,kBAAuB"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createElement } from "react";
|
|
3
|
+
//#region src/analytics/createTrackedLink.tsx
|
|
4
|
+
/**
|
|
5
|
+
* Builds a site `TrackedLink` that wraps the app's Link (`next-intl` navigation
|
|
6
|
+
* Link or `next/link`) and fires the provided capture callbacks.
|
|
7
|
+
*
|
|
8
|
+
* Routing and event catalogs stay in the app — core only owns the click wiring.
|
|
9
|
+
*/
|
|
10
|
+
function createTrackedLink(Link, capture) {
|
|
11
|
+
function TrackedLink(props) {
|
|
12
|
+
const { event, section, destination, onClick, href, ...rest } = props;
|
|
13
|
+
const handleClick = (e) => {
|
|
14
|
+
if (event === "docs_clicked" && section) capture.docsClicked({
|
|
15
|
+
section,
|
|
16
|
+
destination: destination ?? String(href)
|
|
17
|
+
});
|
|
18
|
+
else if (event === "get_started_clicked" && section) capture.getStartedClicked({ section });
|
|
19
|
+
else if (event === "external_link_clicked" && section && destination) capture.externalLinkClicked({
|
|
20
|
+
section,
|
|
21
|
+
destination
|
|
22
|
+
});
|
|
23
|
+
onClick?.(e);
|
|
24
|
+
};
|
|
25
|
+
return createElement(Link, {
|
|
26
|
+
...rest,
|
|
27
|
+
href,
|
|
28
|
+
onClick: handleClick
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return TrackedLink;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { createTrackedLink };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=createTrackedLink.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createTrackedLink.mjs","names":[],"sources":["../../src/analytics/createTrackedLink.tsx"],"sourcesContent":["'use client'\n\nimport {\n createElement,\n type ComponentPropsWithoutRef,\n type ElementType,\n type MouseEvent,\n type ReactElement,\n} from 'react'\n\n/**\n * Discriminated tracking props. Apps keep tight `Section` / destination unions;\n * pass them as generics to `createTrackedLink`.\n */\nexport type CreateTrackedEvent<TSection extends string, TDestination extends string> =\n | { event?: never; section?: never; destination?: never }\n | { event: 'docs_clicked'; section: TSection; destination?: string }\n | { event: 'get_started_clicked'; section: TSection; destination?: never }\n | {\n event: 'external_link_clicked'\n section: TSection\n destination: TDestination\n }\n\nexport type TrackedCapture<TSection extends string, TDestination extends string> = {\n docsClicked: (p: { section: TSection; destination?: string }) => void\n getStartedClicked: (p: { section: TSection }) => void\n externalLinkClicked: (p: { destination: TDestination; section: TSection }) => void\n}\n\n/**\n * Builds a site `TrackedLink` that wraps the app's Link (`next-intl` navigation\n * Link or `next/link`) and fires the provided capture callbacks.\n *\n * Routing and event catalogs stay in the app — core only owns the click wiring.\n */\nexport function createTrackedLink<\n TLink extends ElementType,\n TSection extends string = string,\n TDestination extends string = string,\n>(Link: TLink, capture: TrackedCapture<TSection, TDestination>) {\n type LinkProps = ComponentPropsWithoutRef<TLink>\n type TrackedEvent = CreateTrackedEvent<TSection, TDestination>\n type TrackedLinkProps = LinkProps & TrackedEvent\n\n function TrackedLink(props: TrackedLinkProps): ReactElement {\n const { event, section, destination, onClick, href, ...rest } = props as TrackedLinkProps & {\n event?: TrackedEvent['event']\n section?: TSection\n destination?: string\n onClick?: (e: MouseEvent<HTMLAnchorElement>) => void\n href?: unknown\n }\n\n const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {\n if (event === 'docs_clicked' && section) {\n capture.docsClicked({\n section,\n destination: (destination as string | undefined) ?? String(href),\n })\n } else if (event === 'get_started_clicked' && section) {\n capture.getStartedClicked({ section })\n } else if (event === 'external_link_clicked' && section && destination) {\n capture.externalLinkClicked({\n section,\n destination: destination as TDestination,\n })\n }\n onClick?.(e)\n }\n\n return createElement(Link, {\n ...(rest as LinkProps),\n href,\n onClick: handleClick,\n } as LinkProps)\n }\n\n return TrackedLink as (props: TrackedLinkProps) => ReactElement\n}\n\nexport type { CreateTrackedEvent as TrackedEvent }\n"],"mappings":";;;;;;;;;AAoCA,SAAgB,kBAId,MAAa,SAAiD;CAK9D,SAAS,YAAY,OAAuC;EAC1D,MAAM,EAAE,OAAO,SAAS,aAAa,SAAS,MAAM,GAAG,SAAS;EAQhE,MAAM,eAAe,MAAqC;GACxD,IAAI,UAAU,kBAAkB,SAC9B,QAAQ,YAAY;IAClB;IACA,aAAc,eAAsC,OAAO,IAAI;GACjE,CAAC;QACI,IAAI,UAAU,yBAAyB,SAC5C,QAAQ,kBAAkB,EAAE,QAAQ,CAAC;QAChC,IAAI,UAAU,2BAA2B,WAAW,aACzD,QAAQ,oBAAoB;IAC1B;IACa;GACf,CAAC;GAEH,UAAU,CAAC;EACb;EAEA,OAAO,cAAc,MAAM;GACzB,GAAI;GACJ;GACA,SAAS;EACX,CAAc;CAChB;CAEA,OAAO;AACT"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { GtmLoader, GtmLoaderProps } from "./GtmLoader.mjs";
|
|
2
|
+
import { CreateTrackedEvent, TrackedCapture, createTrackedLink } from "./createTrackedLink.mjs";
|
|
3
|
+
export { type CreateTrackedEvent, GtmLoader, type GtmLoaderProps, type TrackedCapture, type CreateTrackedEvent as TrackedEvent, createTrackedLink };
|
|
@@ -2,7 +2,7 @@ import { HTMLAttributes } from "react";
|
|
|
2
2
|
import { VariantProps } from "class-variance-authority";
|
|
3
3
|
//#region src/ui/components/Badge/Badge.d.ts
|
|
4
4
|
declare const badgeVariants: (props?: ({
|
|
5
|
-
variant?: "
|
|
5
|
+
variant?: "active" | "inactive" | "status" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
type BadgeProps = HTMLAttributes<HTMLSpanElement> & VariantProps<typeof badgeVariants> & {
|
|
8
8
|
asChild?: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tetherto/dev-websites-core",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Shared CMS, SEO, i18n, utils, and
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Shared CMS, SEO, i18n, utils, UI, and analytics primitives for the WDK / QVAC / PEAR / MDK websites.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public",
|
|
@@ -43,6 +43,11 @@
|
|
|
43
43
|
"import": "./dist/utils/index.mjs",
|
|
44
44
|
"default": "./dist/utils/index.mjs"
|
|
45
45
|
},
|
|
46
|
+
"./analytics": {
|
|
47
|
+
"types": "./dist/analytics/index.d.mts",
|
|
48
|
+
"import": "./dist/analytics/index.mjs",
|
|
49
|
+
"default": "./dist/analytics/index.mjs"
|
|
50
|
+
},
|
|
46
51
|
"./ui": {
|
|
47
52
|
"types": "./dist/ui/index.d.mts",
|
|
48
53
|
"import": "./dist/ui/index.mjs",
|
|
@@ -83,6 +88,7 @@
|
|
|
83
88
|
},
|
|
84
89
|
"peerDependencies": {
|
|
85
90
|
"@aws-sdk/client-s3": "^3.0.0",
|
|
91
|
+
"@next/third-parties": ">=15",
|
|
86
92
|
"@payloadcms/richtext-lexical": "^3.85.1",
|
|
87
93
|
"@payloadcms/storage-s3": "^3.85.1",
|
|
88
94
|
"@radix-ui/react-dialog": "^1.1.0",
|
|
@@ -101,6 +107,9 @@
|
|
|
101
107
|
"tailwind-merge": "^3.0.0"
|
|
102
108
|
},
|
|
103
109
|
"peerDependenciesMeta": {
|
|
110
|
+
"@next/third-parties": {
|
|
111
|
+
"optional": true
|
|
112
|
+
},
|
|
104
113
|
"embla-carousel-react": {
|
|
105
114
|
"optional": true
|
|
106
115
|
},
|
|
@@ -111,6 +120,7 @@
|
|
|
111
120
|
"devDependencies": {
|
|
112
121
|
"@aws-sdk/client-s3": "^3.787.0",
|
|
113
122
|
"@eslint/js": "^9.39.0",
|
|
123
|
+
"@next/third-parties": "^16.3.2",
|
|
114
124
|
"@payloadcms/richtext-lexical": "3.88.0",
|
|
115
125
|
"@payloadcms/storage-s3": "3.88.0",
|
|
116
126
|
"@radix-ui/react-dialog": "^1.1.15",
|