bsmnt 0.12.0 → 0.13.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/dist/application/add-integration/index.d.ts.map +1 -1
- package/dist/application/add-integration/index.js +23 -0
- package/dist/application/add-integration/index.js.map +1 -1
- package/dist/domain/consent.d.ts +17 -0
- package/dist/domain/consent.d.ts.map +1 -0
- package/dist/domain/consent.js +13 -0
- package/dist/domain/consent.js.map +1 -0
- package/dist/domain/integration.d.ts +19 -2
- package/dist/domain/integration.d.ts.map +1 -1
- package/dist/domain/integration.js +10 -1
- package/dist/domain/integration.js.map +1 -1
- package/dist/domain/jobs.d.ts +17 -0
- package/dist/domain/jobs.d.ts.map +1 -0
- package/dist/domain/jobs.js +13 -0
- package/dist/domain/jobs.js.map +1 -0
- package/dist/domain/skills.d.ts +23 -2
- package/dist/domain/skills.d.ts.map +1 -1
- package/dist/domain/skills.js +23 -2
- package/dist/domain/skills.js.map +1 -1
- package/dist/domain/templates.d.ts +7 -1
- package/dist/domain/templates.d.ts.map +1 -1
- package/dist/domain/templates.js +2 -0
- package/dist/domain/templates.js.map +1 -1
- package/dist/index.js +34 -9
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
- package/dist/infrastructure/create/setup-agent.js +11 -4
- package/dist/infrastructure/create/setup-agent.js.map +1 -1
- package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
- package/dist/infrastructure/integrations/inject-integration.js +36 -2
- package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
- package/dist/modules/features/consent/ketch/config.d.ts +18 -0
- package/dist/modules/features/consent/ketch/config.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/config.js +24 -0
- package/dist/modules/features/consent/ketch/config.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/biome.d.ts +16 -0
- package/dist/modules/features/consent/ketch/mergers/biome.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/biome.js +31 -0
- package/dist/modules/features/consent/ketch/mergers/biome.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts +13 -0
- package/dist/modules/features/consent/ketch/mergers/check-integration.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/check-integration.js +64 -0
- package/dist/modules/features/consent/ketch/mergers/check-integration.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/env-example.d.ts +12 -0
- package/dist/modules/features/consent/ketch/mergers/env-example.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/env-example.js +28 -0
- package/dist/modules/features/consent/ketch/mergers/env-example.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/footer.d.ts +17 -0
- package/dist/modules/features/consent/ketch/mergers/footer.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/footer.js +40 -0
- package/dist/modules/features/consent/ketch/mergers/footer.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/knip.d.ts +18 -0
- package/dist/modules/features/consent/ketch/mergers/knip.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/knip.js +37 -0
- package/dist/modules/features/consent/ketch/mergers/knip.js.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/layout.d.ts +25 -0
- package/dist/modules/features/consent/ketch/mergers/layout.d.ts.map +1 -0
- package/dist/modules/features/consent/ketch/mergers/layout.js +55 -0
- package/dist/modules/features/consent/ketch/mergers/layout.js.map +1 -0
- package/dist/modules/features/jobs/ashby/config.d.ts +19 -0
- package/dist/modules/features/jobs/ashby/config.d.ts.map +1 -0
- package/dist/modules/features/jobs/ashby/config.js +29 -0
- package/dist/modules/features/jobs/ashby/config.js.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts +14 -0
- package/dist/modules/features/jobs/ashby/mergers/check-integration.d.ts.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/check-integration.js +78 -0
- package/dist/modules/features/jobs/ashby/mergers/check-integration.js.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts +12 -0
- package/dist/modules/features/jobs/ashby/mergers/env-example.d.ts.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/env-example.js +27 -0
- package/dist/modules/features/jobs/ashby/mergers/env-example.js.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/knip.d.ts +18 -0
- package/dist/modules/features/jobs/ashby/mergers/knip.d.ts.map +1 -0
- package/dist/modules/features/jobs/ashby/mergers/knip.js +41 -0
- package/dist/modules/features/jobs/ashby/mergers/knip.js.map +1 -0
- package/dist/paths.d.ts +12 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +12 -0
- package/dist/paths.js.map +1 -1
- package/package.json +3 -1
- package/src/agent-skills/ashby-pagebuilder-block/SKILL.md +277 -0
- package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +58 -25
- package/src/agent-skills/ketch-consent-check/SKILL.md +197 -0
- package/src/agent-skills/ketch-consent-setup/SKILL.md +326 -0
- package/src/agent-skills/ketch-consent-setup/references/ui-customization.md +329 -0
- package/src/agent-skills/page-builder-block/SKILL.md +150 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +6 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +12 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
- package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +7 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/anchor-id-input.tsx +17 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/presentation-input.tsx +146 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/spacing-steps-input.tsx +84 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/layout.ts +4 -2
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +12 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/description.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/gated-content.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/reusable/rich-text-block.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +3 -3
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +8 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/spacing-settings.ts +120 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/anchor.ts +75 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +46 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/presentation.ts +44 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/spacing.ts +164 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +7 -0
- package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +2 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/check-integration.ts +25 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/README.md +70 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/categories.ts +20 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/consent-store.ts +107 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/env.ts +20 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/ketch-experience.css +136 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/preferences-button.tsx +51 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/script.tsx +8 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/types.ts +15 -0
- package/src/modules/features/consent/ketch/files/lib/integrations/ketch/use-consent.ts +26 -0
- package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +53 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +134 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/env.ts +3 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/index.ts +40 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/normalize.ts +124 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/types.ts +20 -0
- package/src/modules/features/jobs/ashby/files/lib/integrations/check-integration.ts +25 -0
- package/src/templates/next-pagebuilder/AGENTS.md +129 -0
- package/src/templates/next-pagebuilder/CLAUDE.md +3 -0
- package/src/templates/next-pagebuilder/README.md +70 -0
- package/src/templates/next-pagebuilder/app/(content)/layout.tsx +2 -0
- package/src/templates/next-pagebuilder/components/layout/spacing-scale/index.tsx +18 -0
- package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +1 -1
- package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +2 -2
- package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +3 -7
- package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +2 -2
- package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +3 -7
- package/src/templates/next-pagebuilder/components/page-builder/components/rich-text-block.tsx +3 -5
- package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +70 -0
- package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +33 -49
- package/src/templates/next-pagebuilder/components/page-builder/section.tsx +35 -0
- package/src/templates/next-pagebuilder/components/page-document/index.tsx +1 -1
- package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +92 -4
- package/src/templates/next-pagebuilder-cache/AGENTS.md +136 -0
- package/src/templates/next-pagebuilder-cache/CLAUDE.md +3 -0
- package/src/templates/next-pagebuilder-cache/README.md +75 -0
- package/src/templates/next-pagebuilder-cache/app/(content)/layout.tsx +2 -0
- package/src/templates/next-pagebuilder-cache/components/layout/spacing-scale/index.tsx +46 -0
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/article-content/index.tsx +1 -1
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/content-collection/index.tsx +2 -2
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/description.tsx +3 -7
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +2 -2
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/hero.tsx +3 -7
- package/src/templates/next-pagebuilder-cache/components/page-builder/components/rich-text-block.tsx +3 -5
- package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +70 -0
- package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +33 -49
- package/src/templates/next-pagebuilder-cache/components/page-builder/section.tsx +35 -0
- package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +1 -1
- package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +92 -4
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
export const spacingBreakpoints = ["mobile", "tablet", "desktop"] as const;
|
|
2
|
+
|
|
3
|
+
export type SpacingBreakpoint = (typeof spacingBreakpoints)[number];
|
|
4
|
+
|
|
5
|
+
export const spacingBreakpointMinWidth: Record<SpacingBreakpoint, number> = {
|
|
6
|
+
mobile: 0,
|
|
7
|
+
tablet: 768,
|
|
8
|
+
desktop: 1440,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SpacingStep = {
|
|
12
|
+
key: string;
|
|
13
|
+
name: string;
|
|
14
|
+
isDefault: boolean;
|
|
15
|
+
} & Record<SpacingBreakpoint, number>;
|
|
16
|
+
|
|
17
|
+
const fallbackSpacingStep: SpacingStep = {
|
|
18
|
+
key: "md",
|
|
19
|
+
name: "MD",
|
|
20
|
+
mobile: 24,
|
|
21
|
+
tablet: 28,
|
|
22
|
+
desktop: 32,
|
|
23
|
+
isDefault: true,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const defaultSpacingSteps: SpacingStep[] = [
|
|
27
|
+
{
|
|
28
|
+
key: "xs",
|
|
29
|
+
name: "XS",
|
|
30
|
+
mobile: 12,
|
|
31
|
+
tablet: 14,
|
|
32
|
+
desktop: 16,
|
|
33
|
+
isDefault: false,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: "sm",
|
|
37
|
+
name: "SM",
|
|
38
|
+
mobile: 16,
|
|
39
|
+
tablet: 20,
|
|
40
|
+
desktop: 24,
|
|
41
|
+
isDefault: false,
|
|
42
|
+
},
|
|
43
|
+
fallbackSpacingStep,
|
|
44
|
+
{
|
|
45
|
+
key: "lg",
|
|
46
|
+
name: "LG",
|
|
47
|
+
mobile: 32,
|
|
48
|
+
tablet: 40,
|
|
49
|
+
desktop: 48,
|
|
50
|
+
isDefault: false,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: "xl",
|
|
54
|
+
name: "XL",
|
|
55
|
+
mobile: 48,
|
|
56
|
+
tablet: 64,
|
|
57
|
+
desktop: 80,
|
|
58
|
+
isDefault: false,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export type SpacingStepSource = {
|
|
63
|
+
_key?: string | null;
|
|
64
|
+
name?: string | null;
|
|
65
|
+
mobile?: number | null;
|
|
66
|
+
tablet?: number | null;
|
|
67
|
+
desktop?: number | null;
|
|
68
|
+
isDefault?: boolean | null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const spacingStepKey = (raw: string) =>
|
|
72
|
+
raw
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
75
|
+
.slice(0, 40);
|
|
76
|
+
|
|
77
|
+
export const spacingVariable = (key: string) => `--section-space-${key}`;
|
|
78
|
+
|
|
79
|
+
export const defaultSpacingVariable = spacingVariable("default");
|
|
80
|
+
|
|
81
|
+
const FALLBACK_PADDING = "2.5rem";
|
|
82
|
+
|
|
83
|
+
const readPx = (value: number | null | undefined) =>
|
|
84
|
+
typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
85
|
+
? Math.round(value)
|
|
86
|
+
: null;
|
|
87
|
+
|
|
88
|
+
export const resolveSpacingSteps = (
|
|
89
|
+
source?: readonly SpacingStepSource[] | null,
|
|
90
|
+
): SpacingStep[] => {
|
|
91
|
+
const steps = (source ?? []).flatMap<SpacingStep>((entry) => {
|
|
92
|
+
const key = spacingStepKey(entry?._key ?? "");
|
|
93
|
+
const name = entry?.name?.trim();
|
|
94
|
+
const desktop = readPx(entry?.desktop);
|
|
95
|
+
|
|
96
|
+
if (!(key && name && desktop !== null)) return [];
|
|
97
|
+
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
key,
|
|
101
|
+
name,
|
|
102
|
+
desktop,
|
|
103
|
+
tablet: readPx(entry?.tablet) ?? desktop,
|
|
104
|
+
mobile: readPx(entry?.mobile) ?? desktop,
|
|
105
|
+
isDefault: entry?.isDefault === true,
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (steps.length === 0) return defaultSpacingSteps;
|
|
111
|
+
|
|
112
|
+
if (steps.some((step) => step.isDefault)) {
|
|
113
|
+
let seen = false;
|
|
114
|
+
return steps.map((step) => {
|
|
115
|
+
const isDefault = step.isDefault && !seen;
|
|
116
|
+
if (isDefault) seen = true;
|
|
117
|
+
return { ...step, isDefault };
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const middle = Math.floor((steps.length - 1) / 2);
|
|
122
|
+
return steps.map((step, index) => ({ ...step, isDefault: index === middle }));
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const getDefaultSpacingStep = (steps: SpacingStep[]): SpacingStep =>
|
|
126
|
+
steps.find((step) => step.isDefault) ?? steps[0] ?? fallbackSpacingStep;
|
|
127
|
+
|
|
128
|
+
export const spacingPaddingValue = (key?: string | null) => {
|
|
129
|
+
const safe = key ? spacingStepKey(key) : "";
|
|
130
|
+
const fallback = `var(${defaultSpacingVariable}, ${FALLBACK_PADDING})`;
|
|
131
|
+
|
|
132
|
+
if (!safe || safe === "default") return fallback;
|
|
133
|
+
|
|
134
|
+
return `var(${spacingVariable(safe)}, ${fallback})`;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const spacingScaleCss = (steps: SpacingStep[]) => {
|
|
138
|
+
const defaultStep = getDefaultSpacingStep(steps);
|
|
139
|
+
|
|
140
|
+
const rule = (breakpoint: SpacingBreakpoint) => {
|
|
141
|
+
const declarations = [
|
|
142
|
+
...steps.map(
|
|
143
|
+
(step) => `${spacingVariable(step.key)}:${step[breakpoint]}px`,
|
|
144
|
+
),
|
|
145
|
+
`${defaultSpacingVariable}:${defaultStep[breakpoint]}px`,
|
|
146
|
+
].join(";");
|
|
147
|
+
|
|
148
|
+
return `:root{${declarations}}`;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return [
|
|
152
|
+
rule("mobile"),
|
|
153
|
+
`@media (min-width:${spacingBreakpointMinWidth.tablet}px){${rule("tablet")}}`,
|
|
154
|
+
`@media (min-width:${spacingBreakpointMinWidth.desktop}px){${rule("desktop")}}`,
|
|
155
|
+
].join("");
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const spacingStepOptions = (steps: SpacingStep[]) =>
|
|
159
|
+
steps.map((step) => ({
|
|
160
|
+
value: step.key,
|
|
161
|
+
title: `${step.name} (${step.desktop}px · ${step.mobile}px mobile)${
|
|
162
|
+
step.isDefault ? " · default" : ""
|
|
163
|
+
}`,
|
|
164
|
+
}));
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
footerIcon,
|
|
4
4
|
navbarIcon,
|
|
5
5
|
pageListIcon,
|
|
6
|
+
spacingSettingsIcon,
|
|
6
7
|
structuredDataIcon,
|
|
7
8
|
} from "./icons";
|
|
8
9
|
|
|
@@ -42,6 +43,12 @@ export const singletonLayout = [
|
|
|
42
43
|
title: "Structured Data",
|
|
43
44
|
icon: structuredDataIcon,
|
|
44
45
|
},
|
|
46
|
+
{
|
|
47
|
+
schemaType: "spacingSettings",
|
|
48
|
+
documentId: "spacingSettings",
|
|
49
|
+
title: "Spacing Scale",
|
|
50
|
+
icon: spacingSettingsIcon,
|
|
51
|
+
},
|
|
45
52
|
] as const;
|
|
46
53
|
|
|
47
54
|
export const singletonComponentTypes: Set<string> = new Set(
|
package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts
CHANGED
|
@@ -8,12 +8,14 @@ export const SANITY_NAVBAR_TAG = "navbar";
|
|
|
8
8
|
export const SANITY_FOOTER_TAG = "footer";
|
|
9
9
|
export const SANITY_COMPANY_DATA_TAG = "companyData";
|
|
10
10
|
export const SANITY_STRUCTURED_DATA_TAG = "structuredData";
|
|
11
|
+
export const SANITY_SPACING_SETTINGS_TAG = "spacingSettings";
|
|
11
12
|
|
|
12
13
|
export const SANITY_LAYOUT_TAGS = [
|
|
13
14
|
SANITY_NAVBAR_TAG,
|
|
14
15
|
SANITY_FOOTER_TAG,
|
|
15
16
|
SANITY_COMPANY_DATA_TAG,
|
|
16
17
|
SANITY_STRUCTURED_DATA_TAG,
|
|
18
|
+
SANITY_SPACING_SETTINGS_TAG,
|
|
17
19
|
] as const;
|
|
18
20
|
|
|
19
21
|
export const SANITY_REDIRECTS_TAG = "redirect";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ketchBootUrl } from "@/lib/integrations/ketch/env"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration Configuration Checker
|
|
5
|
+
*
|
|
6
|
+
* Utilities to check if integrations are configured via environment variables.
|
|
7
|
+
* This helps with tree-shaking unused integrations from the bundle.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { isKetchConfigured } from '@/integrations/check-integration'
|
|
12
|
+
*
|
|
13
|
+
* if (isKetchConfigured()) {
|
|
14
|
+
* // Only render the CMP once it's actually configured
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if Ketch (consent management) is configured. Derived from env.ts, not
|
|
21
|
+
* a second read of `process.env`.
|
|
22
|
+
*/
|
|
23
|
+
export function isKetchConfigured(): boolean {
|
|
24
|
+
return Boolean(ketchBootUrl)
|
|
25
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Ketch consent integration (CMP)
|
|
2
|
+
|
|
3
|
+
[Ketch](https://app.ketch.com) is the consent management platform. It decides,
|
|
4
|
+
per visitor and per region, whether the site may run analytics or marketing
|
|
5
|
+
trackers. **No tracking script may fire without asking it first.**
|
|
6
|
+
|
|
7
|
+
## 1. Environment
|
|
8
|
+
|
|
9
|
+
Both codes are public — they appear in the boot URL in the page source — so
|
|
10
|
+
they go in as plain values:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
NEXT_PUBLIC_KETCH_ORG_CODE=
|
|
14
|
+
NEXT_PUBLIC_KETCH_PROPERTY_CODE=
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Get them from the Ketch dashboard at https://app.ketch.com, or read them out
|
|
18
|
+
of the boot URL if all you have is the raw `<script>` snippet:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
https://global.ketchcdn.com/web/v3/config/<org>/<property>/boot.js
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 2. Checking consent
|
|
25
|
+
|
|
26
|
+
One helper, always — categories are `essential`, `analytics`, `marketing`:
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
const { hasConsent } = useConsent() // @/lib/integrations/ketch/use-consent
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!hasConsent("analytics")) return
|
|
33
|
+
const tracker = start()
|
|
34
|
+
return () => tracker.stop() // this is what makes revocation work
|
|
35
|
+
}, [hasConsent])
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
It returns `false` until Ketch resolves, so a tracker written this way cannot
|
|
39
|
+
fire early. Never read `window.ketch`, cookies or `localStorage` directly —
|
|
40
|
+
outside React, subscribe to `consentStore` from `./consent-store` instead.
|
|
41
|
+
|
|
42
|
+
Map each tracker to a category in `categories.ts`'s `TRACKER_CONSENT_CATEGORY`
|
|
43
|
+
so the mapping lives in one place and is reviewable. See the
|
|
44
|
+
`ketch-consent-check` agent skill for the full walkthrough (loader shape,
|
|
45
|
+
cleanup, SDKs without teardown).
|
|
46
|
+
|
|
47
|
+
Preferences are reachable from `<ConsentPreferencesButton />` in the footer
|
|
48
|
+
and from Ketch's own floating button (kept by default, since the footer entry
|
|
49
|
+
point isn't guaranteed on every project this integration is added to). Hide
|
|
50
|
+
the floating button in `ketch-experience.css` only once another entry point
|
|
51
|
+
is confirmed to exist — see the `ketch-consent-setup` skill's UI reference.
|
|
52
|
+
|
|
53
|
+
## 3. Region behaviour
|
|
54
|
+
|
|
55
|
+
Ketch resolves a jurisdiction from the visitor's IP — nothing to configure in
|
|
56
|
+
code. EEA/UK/Switzerland, California and Quebec are opt-in (nothing runs until
|
|
57
|
+
the visitor accepts); Canada and 14 US states are opt-out (on until refused);
|
|
58
|
+
everywhere else falls to `default`, which is notice-only. Because the helper
|
|
59
|
+
stays denied until Ketch confirms, the site's own trackers wait for the
|
|
60
|
+
record in every region.
|
|
61
|
+
|
|
62
|
+
In a notice-only region the purpose toggles render locked as "Always Active"
|
|
63
|
+
and the Accept/Reject buttons have nothing to change — that is the
|
|
64
|
+
jurisdiction's configuration, not a bug.
|
|
65
|
+
|
|
66
|
+
Also true for opt-out regions: consent is granted automatically with no
|
|
67
|
+
banner interaction, so the banner won't come back on reload either. To test
|
|
68
|
+
the real Accept/Reject UI from a region that defaults to notice-only, append
|
|
69
|
+
`?ketch_jurisdiction=gdpr` to the URL — see the `ketch-consent-setup` skill's
|
|
70
|
+
UI reference for the full mechanism.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const CONSENT_CATEGORIES = {
|
|
2
|
+
essential: "essential_services",
|
|
3
|
+
analytics: "analytics",
|
|
4
|
+
marketing: "targeted_advertising",
|
|
5
|
+
} as const
|
|
6
|
+
|
|
7
|
+
export type ConsentCategory = keyof typeof CONSENT_CATEGORIES
|
|
8
|
+
|
|
9
|
+
export type KetchPurposeCode = (typeof CONSENT_CATEGORIES)[ConsentCategory]
|
|
10
|
+
|
|
11
|
+
export const CONSENT_CATEGORY_LIST = Object.keys(
|
|
12
|
+
CONSENT_CATEGORIES
|
|
13
|
+
) as ConsentCategory[]
|
|
14
|
+
|
|
15
|
+
export const TRACKER_CONSENT_CATEGORY = {} as const satisfies Record<
|
|
16
|
+
string,
|
|
17
|
+
ConsentCategory
|
|
18
|
+
>
|
|
19
|
+
|
|
20
|
+
export type TrackedService = keyof typeof TRACKER_CONSENT_CATEGORY
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONSENT_CATEGORIES,
|
|
3
|
+
CONSENT_CATEGORY_LIST,
|
|
4
|
+
type ConsentCategory,
|
|
5
|
+
} from "./categories"
|
|
6
|
+
import type { KetchConsent, KetchJurisdictionEvent, KetchWindow } from "./types"
|
|
7
|
+
|
|
8
|
+
export type ConsentSnapshot = {
|
|
9
|
+
isResolved: boolean
|
|
10
|
+
jurisdiction: string | null
|
|
11
|
+
granted: Readonly<Record<ConsentCategory, boolean>>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const denyAll = () =>
|
|
15
|
+
Object.fromEntries(
|
|
16
|
+
CONSENT_CATEGORY_LIST.map((category) => [category, false])
|
|
17
|
+
) as Record<ConsentCategory, boolean>
|
|
18
|
+
|
|
19
|
+
const UNRESOLVED: ConsentSnapshot = {
|
|
20
|
+
isResolved: false,
|
|
21
|
+
jurisdiction: null,
|
|
22
|
+
granted: denyAll(),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let snapshot: ConsentSnapshot = UNRESOLVED
|
|
26
|
+
let isListening = false
|
|
27
|
+
const listeners = new Set<() => void>()
|
|
28
|
+
|
|
29
|
+
const notify = () => {
|
|
30
|
+
for (const listener of listeners) listener()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const getKetchWindow = () =>
|
|
34
|
+
typeof window === "undefined" ? null : (window as KetchWindow)
|
|
35
|
+
|
|
36
|
+
const sendKetchCommand = (...command: unknown[]) => {
|
|
37
|
+
const ketchWindow = getKetchWindow()
|
|
38
|
+
if (!ketchWindow) return
|
|
39
|
+
|
|
40
|
+
if (typeof ketchWindow.ketch === "function") {
|
|
41
|
+
ketchWindow.ketch(...command)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ketchWindow.semaphore = ketchWindow.semaphore ?? []
|
|
46
|
+
ketchWindow.semaphore.push(command)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const readGranted = (consent: KetchConsent) => {
|
|
50
|
+
const purposes = consent?.purposes ?? {}
|
|
51
|
+
const granted = denyAll()
|
|
52
|
+
|
|
53
|
+
for (const category of CONSENT_CATEGORY_LIST) {
|
|
54
|
+
const value = purposes[CONSENT_CATEGORIES[category]]
|
|
55
|
+
granted[category] = value === true || value === "true"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return granted
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const isSameGranted = (
|
|
62
|
+
a: Readonly<Record<ConsentCategory, boolean>>,
|
|
63
|
+
b: Readonly<Record<ConsentCategory, boolean>>
|
|
64
|
+
) => CONSENT_CATEGORY_LIST.every((category) => a[category] === b[category])
|
|
65
|
+
|
|
66
|
+
const handleConsent = (consent: KetchConsent) => {
|
|
67
|
+
const granted = readGranted(consent)
|
|
68
|
+
if (snapshot.isResolved && isSameGranted(snapshot.granted, granted)) return
|
|
69
|
+
|
|
70
|
+
snapshot = { ...snapshot, isResolved: true, granted }
|
|
71
|
+
notify()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const handleJurisdiction = (event: KetchJurisdictionEvent) => {
|
|
75
|
+
const code = typeof event === "string" ? event : event?.code
|
|
76
|
+
if (!code || code === snapshot.jurisdiction) return
|
|
77
|
+
|
|
78
|
+
snapshot = { ...snapshot, jurisdiction: code }
|
|
79
|
+
notify()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const startListening = () => {
|
|
83
|
+
if (isListening || !getKetchWindow()) return
|
|
84
|
+
isListening = true
|
|
85
|
+
|
|
86
|
+
sendKetchCommand("on", "consent", handleConsent)
|
|
87
|
+
sendKetchCommand("on", "jurisdiction", handleJurisdiction)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const consentStore = {
|
|
91
|
+
subscribe: (listener: () => void) => {
|
|
92
|
+
startListening()
|
|
93
|
+
listeners.add(listener)
|
|
94
|
+
|
|
95
|
+
return () => {
|
|
96
|
+
listeners.delete(listener)
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
getSnapshot: () => snapshot,
|
|
100
|
+
getServerSnapshot: () => UNRESOLVED,
|
|
101
|
+
hasConsent: (category: ConsentCategory) => snapshot.granted[category],
|
|
102
|
+
openPreferences: () => {
|
|
103
|
+
sendKetchCommand("showConsent", {
|
|
104
|
+
displayHint: "experiences.consent.modal",
|
|
105
|
+
})
|
|
106
|
+
},
|
|
107
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const KETCH_CODE_PATTERN = /^[a-z0-9_-]+$/i
|
|
2
|
+
|
|
3
|
+
const readCode = (value: string | undefined) => {
|
|
4
|
+
const code = value?.trim()
|
|
5
|
+
if (!code || !KETCH_CODE_PATTERN.test(code)) return ""
|
|
6
|
+
return code
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const ketchOrganizationCode = readCode(
|
|
10
|
+
process.env.NEXT_PUBLIC_KETCH_ORG_CODE
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
export const ketchPropertyCode = readCode(
|
|
14
|
+
process.env.NEXT_PUBLIC_KETCH_PROPERTY_CODE
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
export const ketchBootUrl =
|
|
18
|
+
ketchOrganizationCode && ketchPropertyCode
|
|
19
|
+
? `https://global.ketchcdn.com/web/v3/config/${ketchOrganizationCode}/${ketchPropertyCode}/boot.js`
|
|
20
|
+
: null
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
html:root {
|
|
2
|
+
--k-banner-container-background-color: var(--color-primary);
|
|
3
|
+
--k-banner-container-backdrop-background-color: transparent;
|
|
4
|
+
--k-banner-header-title-color: var(--color-secondary);
|
|
5
|
+
--k-banner-description-text-color: var(--color-secondary);
|
|
6
|
+
--k-banner-description-link-color: var(--color-contrast);
|
|
7
|
+
--k-banner-header-returnButton-background-color: var(--color-gray-100);
|
|
8
|
+
--k-banner-header-returnButton-icon-color: var(--color-secondary);
|
|
9
|
+
--k-banner-gpcBanner-fill-color: var(--color-gray-50);
|
|
10
|
+
--k-banner-gpcBanner-outline-color: var(--color-gray-100);
|
|
11
|
+
--k-banner-gpcBanner-title-text-color: var(--color-secondary);
|
|
12
|
+
--k-banner-gpcBanner-description-text-color: var(--color-gray-600);
|
|
13
|
+
--k-banner-gpcBanner-link-text-color: var(--color-contrast);
|
|
14
|
+
|
|
15
|
+
--k-banner-buttons-primary-background-color: var(--color-secondary);
|
|
16
|
+
--k-banner-buttons-primary-text-color: var(--color-primary);
|
|
17
|
+
--k-banner-buttons-primary-outline-color: var(--color-secondary);
|
|
18
|
+
--k-banner-buttons-primary-cornerRadius: 8px;
|
|
19
|
+
--k-banner-buttons-secondary-background-color: var(--color-primary);
|
|
20
|
+
--k-banner-buttons-secondary-text-color: var(--color-secondary);
|
|
21
|
+
--k-banner-buttons-secondary-outline-color: var(--color-secondary);
|
|
22
|
+
--k-banner-buttons-secondary-cornerRadius: 8px;
|
|
23
|
+
--k-banner-buttons-tertiary-background-color: var(--color-primary);
|
|
24
|
+
--k-banner-buttons-tertiary-text-color: var(--color-secondary);
|
|
25
|
+
--k-banner-buttons-tertiary-outline-color: var(--color-secondary);
|
|
26
|
+
--k-banner-buttons-tertiary-cornerRadius: 8px;
|
|
27
|
+
|
|
28
|
+
--k-modal-container-background-color: var(--color-primary);
|
|
29
|
+
--k-modal-container-backdrop-background-color: color-mix(
|
|
30
|
+
in srgb,
|
|
31
|
+
var(--color-secondary) 30%,
|
|
32
|
+
transparent
|
|
33
|
+
);
|
|
34
|
+
--k-modal-header-background-color: var(--color-primary);
|
|
35
|
+
--k-modal-header-title-color: var(--color-secondary);
|
|
36
|
+
--k-modal-header-returnButton-background-color: var(--color-gray-100);
|
|
37
|
+
--k-modal-header-returnButton-icon-color: var(--color-secondary);
|
|
38
|
+
--k-modal-description-title-color: var(--color-secondary);
|
|
39
|
+
--k-modal-description-text-color: var(--color-gray-600);
|
|
40
|
+
--k-modal-description-link-color: var(--color-contrast);
|
|
41
|
+
--k-modal-footer-background-color: var(--color-primary);
|
|
42
|
+
--k-modal-footer-actionButton-background-color: var(--color-secondary);
|
|
43
|
+
--k-modal-footer-actionButton-text-color: var(--color-primary);
|
|
44
|
+
--k-modal-footer-actionButton-outline-color: var(--color-secondary);
|
|
45
|
+
--k-modal-footer-actionButton-cornerRadius: 8px;
|
|
46
|
+
|
|
47
|
+
--k-modal-purposeListHeader-title-color: var(--color-secondary);
|
|
48
|
+
--k-modal-purposeListHeader-acceptAllButton-background-color: var(
|
|
49
|
+
--color-secondary
|
|
50
|
+
);
|
|
51
|
+
--k-modal-purposeListHeader-acceptAllButton-text-color: var(--color-primary);
|
|
52
|
+
--k-modal-purposeListHeader-acceptAllButton-outline-color: var(
|
|
53
|
+
--color-secondary
|
|
54
|
+
);
|
|
55
|
+
--k-modal-purposeListHeader-acceptAllButton-cornerRadius: 8px;
|
|
56
|
+
--k-modal-purposeListHeader-rejectAllButton-background-color: var(
|
|
57
|
+
--color-primary
|
|
58
|
+
);
|
|
59
|
+
--k-modal-purposeListHeader-rejectAllButton-text-color: var(
|
|
60
|
+
--color-secondary
|
|
61
|
+
);
|
|
62
|
+
--k-modal-purposeListHeader-rejectAllButton-outline-color: var(
|
|
63
|
+
--color-secondary
|
|
64
|
+
);
|
|
65
|
+
--k-modal-purposeListHeader-rejectAllButton-cornerRadius: 8px;
|
|
66
|
+
|
|
67
|
+
--k-modal-purposeList-purposeListItems-purposeFill-color: var(
|
|
68
|
+
--color-gray-50
|
|
69
|
+
);
|
|
70
|
+
--k-modal-purposeList-purposeListItems-purposeOutline-color: var(
|
|
71
|
+
--color-gray-100
|
|
72
|
+
);
|
|
73
|
+
--k-modal-purposeList-purposeListItems-purposeContent-color: var(
|
|
74
|
+
--color-secondary
|
|
75
|
+
);
|
|
76
|
+
--k-modal-purposeList-purposeListItems-purposeLinks-color: var(
|
|
77
|
+
--color-contrast
|
|
78
|
+
);
|
|
79
|
+
--k-modal-purposeList-purposeListItems-arrowIcon-color: var(--color-gray-500);
|
|
80
|
+
--k-modal-purposeList-switchButtons-on-background-color: var(
|
|
81
|
+
--color-secondary
|
|
82
|
+
);
|
|
83
|
+
--k-modal-purposeList-switchButtons-on-thumb-color: var(--color-primary);
|
|
84
|
+
--k-modal-purposeList-switchButtons-on-text-color: var(--color-secondary);
|
|
85
|
+
--k-modal-purposeList-switchButtons-off-background-color: var(
|
|
86
|
+
--color-gray-200
|
|
87
|
+
);
|
|
88
|
+
--k-modal-purposeList-switchButtons-off-thumb-color: var(--color-primary);
|
|
89
|
+
--k-modal-purposeList-switchButtons-off-text-color: var(--color-gray-500);
|
|
90
|
+
--k-modal-purposeList-checkbox-selected-background-color: var(
|
|
91
|
+
--color-secondary
|
|
92
|
+
);
|
|
93
|
+
--k-modal-purposeList-checkbox-selected-icon-color: var(--color-primary);
|
|
94
|
+
--k-modal-purposeList-checkbox-selected-label-color: var(--color-secondary);
|
|
95
|
+
--k-modal-purposeList-checkbox-unselected-background-color: var(
|
|
96
|
+
--color-primary
|
|
97
|
+
);
|
|
98
|
+
--k-modal-purposeList-checkbox-unselected-outline-color: var(
|
|
99
|
+
--color-gray-200
|
|
100
|
+
);
|
|
101
|
+
--k-modal-purposeList-checkbox-unselected-label-color: var(--color-gray-600);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#lanyard_root,
|
|
105
|
+
#lanyard_root * {
|
|
106
|
+
font-family:
|
|
107
|
+
var(--font-geist), ui-sans-serif, system-ui, sans-serif !important;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#lanyard_root *:focus-visible {
|
|
111
|
+
outline: 2px solid var(--color-contrast) !important;
|
|
112
|
+
outline-offset: 2px !important;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@media (max-width: 640px) {
|
|
116
|
+
#ketch-banner {
|
|
117
|
+
inset-inline: 0 !important;
|
|
118
|
+
width: 100% !important;
|
|
119
|
+
max-width: 100% !important;
|
|
120
|
+
border-radius: 0 !important;
|
|
121
|
+
padding-bottom: max(1rem, var(--safe-area-inset-bottom, 0px)) !important;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#ketch-banner-buttons-container-standard,
|
|
125
|
+
#ketch-banner-buttons-container-compact {
|
|
126
|
+
display: flex !important;
|
|
127
|
+
flex-direction: column !important;
|
|
128
|
+
width: 100% !important;
|
|
129
|
+
gap: 0.5rem !important;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#ketch-banner-buttons-container-standard > *,
|
|
133
|
+
#ketch-banner-buttons-container-compact > * {
|
|
134
|
+
width: 100% !important;
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/modules/features/consent/ketch/files/lib/integrations/ketch/preferences-button.tsx
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
import type { ComponentPropsWithoutRef } from "react"
|
|
5
|
+
import { isKetchConfigured } from "@/lib/integrations/check-integration"
|
|
6
|
+
import { cn } from "@/lib/styles/cn"
|
|
7
|
+
import { useConsent } from "./use-consent"
|
|
8
|
+
|
|
9
|
+
const preferencesButton = cva("w-fit text-left transition-colors", {
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
footer: "text-secondary/60 hover:text-secondary",
|
|
13
|
+
strong: "text-secondary hover:text-secondary/60",
|
|
14
|
+
link: "text-secondary underline underline-offset-4 hover:no-underline",
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
sm: "text-sm",
|
|
18
|
+
md: "text-base",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
variant: "footer",
|
|
23
|
+
size: "md",
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
type ConsentPreferencesButtonProps = ComponentPropsWithoutRef<"button"> &
|
|
28
|
+
VariantProps<typeof preferencesButton>
|
|
29
|
+
|
|
30
|
+
export const ConsentPreferencesButton = ({
|
|
31
|
+
className,
|
|
32
|
+
variant,
|
|
33
|
+
size,
|
|
34
|
+
children,
|
|
35
|
+
...props
|
|
36
|
+
}: ConsentPreferencesButtonProps) => {
|
|
37
|
+
const { openPreferences } = useConsent()
|
|
38
|
+
|
|
39
|
+
if (!isKetchConfigured()) return null
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={openPreferences}
|
|
45
|
+
className={cn(preferencesButton({ variant, size }), className)}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
{children ?? "Privacy Choices"}
|
|
49
|
+
</button>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type KetchPurposeValue = boolean | "true" | "false" | undefined
|
|
2
|
+
|
|
3
|
+
export type KetchConsent = {
|
|
4
|
+
purposes?: Record<string, KetchPurposeValue>
|
|
5
|
+
vendors?: string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type KetchJurisdictionEvent = string | { code?: string } | undefined
|
|
9
|
+
|
|
10
|
+
export type KetchSemaphore = unknown[] & { loaded?: boolean }
|
|
11
|
+
|
|
12
|
+
export type KetchWindow = Window & {
|
|
13
|
+
ketch?: (...command: unknown[]) => void
|
|
14
|
+
semaphore?: KetchSemaphore
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useSyncExternalStore } from "react"
|
|
4
|
+
import type { ConsentCategory } from "./categories"
|
|
5
|
+
import { consentStore } from "./consent-store"
|
|
6
|
+
|
|
7
|
+
export const useConsent = () => {
|
|
8
|
+
const { isResolved, jurisdiction, granted } = useSyncExternalStore(
|
|
9
|
+
consentStore.subscribe,
|
|
10
|
+
consentStore.getSnapshot,
|
|
11
|
+
consentStore.getServerSnapshot
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const hasConsent = useCallback(
|
|
15
|
+
(category: ConsentCategory) => granted[category],
|
|
16
|
+
[granted]
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
hasConsent,
|
|
21
|
+
granted,
|
|
22
|
+
isResolved,
|
|
23
|
+
jurisdiction,
|
|
24
|
+
openPreferences: consentStore.openPreferences,
|
|
25
|
+
}
|
|
26
|
+
}
|