@inneropen/marvin-astro 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +117 -3
- package/dist/index.js.map +1 -1
- package/dist/types-4VdGN9e_.d.ts +243 -0
- package/dist/types.d.ts +3 -162
- package/package.json +1 -1
- package/src/components/ConsentBanner.astro +87 -0
- package/src/components/ConsentBannerView.astro +104 -0
- package/src/components/CookieSettingsLink.astro +28 -0
- package/src/components/CookieTable.astro +97 -0
- package/src/components/PrivacyPolicy.astro +118 -0
- package/src/components/ProcessorTable.astro +62 -0
- package/src/components/consent.css +192 -0
- package/src/components/consent.ts +246 -0
- package/src/components/index.ts +12 -0
- package/src/components/privacy-text.ts +149 -0
- package/src/components/privacy.css +123 -0
- package/src/index.ts +8 -0
- package/src/privacy.ts +210 -0
- package/src/site.ts +8 -0
- package/src/types.ts +15 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { g as MarvinSiteEmbeds } from './embeds-BTNryQA6.js';
|
|
2
|
+
import { MarvinEntry, Entry, CollectionEntry, MarvinEntryListItem } from '@inneropen/marvin-sdk';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Privacy & cookie disclosures: the published `site.privacy` shape, read tolerantly.
|
|
6
|
+
*
|
|
7
|
+
* Marvin keeps the structured facts a privacy/cookie policy page needs (who the controller is,
|
|
8
|
+
* which processors handle visitor data, which cookies and storage entries the site sets). The
|
|
9
|
+
* components in this package render them and, when any entry is not strictly necessary, run a
|
|
10
|
+
* consent banner.
|
|
11
|
+
*
|
|
12
|
+
* Declared here rather than imported from `@inneropen/marvin-sdk` so the package works with SDKs
|
|
13
|
+
* that predate `site.privacy`. Both the published camelCase keys and snake_case are accepted, and
|
|
14
|
+
* a malformed entry is dropped rather than failing the site build.
|
|
15
|
+
*/
|
|
16
|
+
type CookieCategory = 'necessary' | 'preferences' | 'analytics' | 'marketing';
|
|
17
|
+
/** The categories a visitor can switch on; `necessary` is always on and never asked. */
|
|
18
|
+
type ConsentCategory = Exclude<CookieCategory, 'necessary'>;
|
|
19
|
+
declare const COOKIE_CATEGORIES: readonly CookieCategory[];
|
|
20
|
+
declare const CONSENT_CATEGORIES: readonly ConsentCategory[];
|
|
21
|
+
/** Who is responsible for the site's data (the GDPR "controller"). */
|
|
22
|
+
type PrivacyController = {
|
|
23
|
+
legalName: string;
|
|
24
|
+
contactEmail?: string;
|
|
25
|
+
address?: string;
|
|
26
|
+
euRepresentative?: string;
|
|
27
|
+
dpoEmail?: string;
|
|
28
|
+
};
|
|
29
|
+
/** A third party that handles visitor data on the site's behalf. */
|
|
30
|
+
type PrivacyProcessor = {
|
|
31
|
+
name: string;
|
|
32
|
+
purpose?: string;
|
|
33
|
+
dataCategories: string[];
|
|
34
|
+
region?: string;
|
|
35
|
+
transferBasis?: string;
|
|
36
|
+
dpaUrl?: string;
|
|
37
|
+
privacyUrl?: string;
|
|
38
|
+
};
|
|
39
|
+
/** A cookie (or similar storage entry) the site sets. */
|
|
40
|
+
type PrivacyCookie = {
|
|
41
|
+
name: string;
|
|
42
|
+
provider: string;
|
|
43
|
+
purpose: string;
|
|
44
|
+
duration?: string;
|
|
45
|
+
category: CookieCategory;
|
|
46
|
+
};
|
|
47
|
+
type PrivacyConsent = {
|
|
48
|
+
policyEntrySlug?: string;
|
|
49
|
+
cookiePolicyEntrySlug?: string;
|
|
50
|
+
/** ISO date (`YYYY-MM-DD`) the disclosures were last checked. */
|
|
51
|
+
lastReviewed?: string;
|
|
52
|
+
/** A manual re-prompt bump: changing it asks every visitor again. */
|
|
53
|
+
version?: string;
|
|
54
|
+
};
|
|
55
|
+
type SitePrivacy = {
|
|
56
|
+
controller?: PrivacyController;
|
|
57
|
+
processors: PrivacyProcessor[];
|
|
58
|
+
cookies: PrivacyCookie[];
|
|
59
|
+
consent: PrivacyConsent;
|
|
60
|
+
/** True when any cookie is not `necessary`: those need prior consent. */
|
|
61
|
+
bannerRequired: boolean;
|
|
62
|
+
};
|
|
63
|
+
/** True when any listed cookie needs consent. */
|
|
64
|
+
declare function cookiesNeedConsent(cookies: readonly Pick<PrivacyCookie, 'category'>[]): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Normalize `site.privacy`; `undefined` when the server doesn't send it (or sends a non-object).
|
|
67
|
+
* `bannerRequired` is taken from the payload when it is a boolean and computed from the cookies
|
|
68
|
+
* otherwise.
|
|
69
|
+
*/
|
|
70
|
+
declare function sitePrivacy(raw: unknown): SitePrivacy | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* The version a visitor's choice is bound to: FNV-1a over the sorted, non-necessary
|
|
73
|
+
* `category|name|provider` triples (plus `consent.version`, a manual bump). Adding, removing or
|
|
74
|
+
* re-categorising a cookie changes it and asks everyone again; rewording a purpose, changing a
|
|
75
|
+
* duration, or moving `lastReviewed` does not.
|
|
76
|
+
*/
|
|
77
|
+
declare function consentVersion(privacy: SitePrivacy | undefined): string;
|
|
78
|
+
/** The optional categories that appear in the cookie list, in display order. */
|
|
79
|
+
declare function consentCategories(privacy: SitePrivacy | undefined): ConsentCategory[];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Site-integration types.
|
|
83
|
+
*
|
|
84
|
+
* These are the *resolved* shapes a site renders from — Marvin's raw payloads normalized,
|
|
85
|
+
* defaulted, and made non-optional where a template would otherwise need a guard on every
|
|
86
|
+
* use. Nothing here is specific to a particular site's design system.
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Any entry shape the published API can hand back. `MarvinEntry`/`Entry` carry `data_json`;
|
|
91
|
+
* `CollectionEntry`/`MarvinEntryListItem` (list items) do not — see the `hydrate` repository
|
|
92
|
+
* option.
|
|
93
|
+
*/
|
|
94
|
+
type MarvinContentEntry = MarvinEntry | Entry | CollectionEntry | MarvinEntryListItem;
|
|
95
|
+
/** Criteria for picking one asset off an entry's `assets[]`. */
|
|
96
|
+
type AssetSelectOptions = {
|
|
97
|
+
role?: string;
|
|
98
|
+
roles?: string[];
|
|
99
|
+
usage?: string;
|
|
100
|
+
usages?: string[];
|
|
101
|
+
type?: string;
|
|
102
|
+
types?: string[];
|
|
103
|
+
mimeType?: string;
|
|
104
|
+
/** Also accept assets carrying neither a role nor a usage. */
|
|
105
|
+
allowUnroled?: boolean;
|
|
106
|
+
};
|
|
107
|
+
/** A resolved image: a URL plus the presentation hints stored alongside the asset. */
|
|
108
|
+
type ApiImage = {
|
|
109
|
+
src: string;
|
|
110
|
+
alt: string;
|
|
111
|
+
/** CSS `object-position` value, e.g. `"40% 65%"`. */
|
|
112
|
+
focalPoint?: string;
|
|
113
|
+
};
|
|
114
|
+
/** A resolved link to an attached resource (material, construction detail, document…). */
|
|
115
|
+
type ApiResourceLink = {
|
|
116
|
+
name: string;
|
|
117
|
+
/** The resource type, e.g. `'material'`, `'construction'`. */
|
|
118
|
+
type: string;
|
|
119
|
+
/** The relationship role, e.g. `'primary-material'`. */
|
|
120
|
+
role?: string;
|
|
121
|
+
slug?: string;
|
|
122
|
+
href?: string;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Resolved SEO / social-sharing metadata for the site. Mirrors Marvin's SiteSeo, with identity
|
|
126
|
+
* fallbacks already applied (title/description/siteName never empty). The rendering layer
|
|
127
|
+
* (SeoHead) turns this into the actual `<meta>`/Open Graph/Twitter tags.
|
|
128
|
+
*/
|
|
129
|
+
type ApiSeo = {
|
|
130
|
+
title: string;
|
|
131
|
+
titleTemplate?: string;
|
|
132
|
+
description: string;
|
|
133
|
+
keywords: string[];
|
|
134
|
+
robots: string;
|
|
135
|
+
image?: string;
|
|
136
|
+
imageAlt?: string;
|
|
137
|
+
ogType: string;
|
|
138
|
+
siteName: string;
|
|
139
|
+
twitterCard: string;
|
|
140
|
+
twitterHandle?: string;
|
|
141
|
+
twitterCreator?: string;
|
|
142
|
+
fbAppId?: string;
|
|
143
|
+
themeColor?: string;
|
|
144
|
+
publisher?: string;
|
|
145
|
+
canonicalUrl?: string;
|
|
146
|
+
verification: {
|
|
147
|
+
google?: string;
|
|
148
|
+
bing?: string;
|
|
149
|
+
pinterest?: string;
|
|
150
|
+
yandex?: string;
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
type ApiSite = {
|
|
154
|
+
workspaceSlug?: string;
|
|
155
|
+
workspaceName?: string;
|
|
156
|
+
name: string;
|
|
157
|
+
title: string;
|
|
158
|
+
tagline?: string;
|
|
159
|
+
description: string;
|
|
160
|
+
canonicalUrl?: string;
|
|
161
|
+
logo?: string;
|
|
162
|
+
favicon?: string;
|
|
163
|
+
/** Round maker's stamp / seal — `site_metadata_json.brand.seal`, when it is a URL. */
|
|
164
|
+
seal?: string;
|
|
165
|
+
/**
|
|
166
|
+
* Brand-asset registry: every `site_metadata_json.brand.<name>` that is a URL, path or data URI,
|
|
167
|
+
* used as-is. Bare asset slugs are left out (Marvin no longer serves assets by slug).
|
|
168
|
+
*/
|
|
169
|
+
brand?: Record<string, string>;
|
|
170
|
+
locale: string;
|
|
171
|
+
timezone: string;
|
|
172
|
+
email?: string;
|
|
173
|
+
imprint?: string;
|
|
174
|
+
social: Record<string, string>;
|
|
175
|
+
seo: ApiSeo;
|
|
176
|
+
/**
|
|
177
|
+
* Embed privacy mode, consent text and the frame sources a `frame-src` CSP must allow.
|
|
178
|
+
* Absent when the server predates media embeds.
|
|
179
|
+
*/
|
|
180
|
+
embeds?: MarvinSiteEmbeds;
|
|
181
|
+
/**
|
|
182
|
+
* Privacy & cookie disclosures (`site.privacy`), normalized by `sitePrivacy`. Absent when the
|
|
183
|
+
* server predates them or none are configured.
|
|
184
|
+
*/
|
|
185
|
+
privacy?: SitePrivacy;
|
|
186
|
+
metadata: Record<string, unknown>;
|
|
187
|
+
};
|
|
188
|
+
type ApiNavigationLink = {
|
|
189
|
+
label: string;
|
|
190
|
+
href: string;
|
|
191
|
+
description?: string;
|
|
192
|
+
external: boolean;
|
|
193
|
+
/** Membership role from the nav collection (e.g. `'primary'`, `'legal'`). Drives grouping. */
|
|
194
|
+
role?: string;
|
|
195
|
+
};
|
|
196
|
+
type ApiSocialLink = ApiNavigationLink & {
|
|
197
|
+
icon: string;
|
|
198
|
+
};
|
|
199
|
+
type ApiSiteChrome = {
|
|
200
|
+
site: ApiSite;
|
|
201
|
+
mainNavigation: ApiNavigationLink[];
|
|
202
|
+
footerNavigation: ApiNavigationLink[][];
|
|
203
|
+
/** Legal links (`role: 'legal'` in the footer nav collection), e.g. Terms/Privacy. */
|
|
204
|
+
legalLinks: ApiNavigationLink[];
|
|
205
|
+
socialLinks: ApiSocialLink[];
|
|
206
|
+
inquiry?: ApiNavigationLink;
|
|
207
|
+
};
|
|
208
|
+
type ApiActionVariant = 'primary' | 'secondary';
|
|
209
|
+
type ApiPageAction = ApiNavigationLink & {
|
|
210
|
+
variant: ApiActionVariant;
|
|
211
|
+
};
|
|
212
|
+
type ApiHeroAction = ApiPageAction;
|
|
213
|
+
type ApiHero = {
|
|
214
|
+
kicker: string;
|
|
215
|
+
headline: string;
|
|
216
|
+
body: string;
|
|
217
|
+
actions: ApiHeroAction[];
|
|
218
|
+
layout: 'split' | 'centered';
|
|
219
|
+
image?: {
|
|
220
|
+
src?: string;
|
|
221
|
+
alt: string;
|
|
222
|
+
focalPoint?: string;
|
|
223
|
+
dominantColor?: string;
|
|
224
|
+
fit?: 'cover' | 'contain';
|
|
225
|
+
caption?: string;
|
|
226
|
+
credit?: string;
|
|
227
|
+
orientation?: string;
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* A section-landing header driven by a Marvin `page` entry (e.g. `bench-notes`, `projects`).
|
|
232
|
+
* The entry is linkable in nav (collection membership) AND supplies the landing's title, intro,
|
|
233
|
+
* and hero — so the top of an index page stops being static. Absent fields resolve to
|
|
234
|
+
* `undefined` so callers fall back to their existing copy/illustration.
|
|
235
|
+
*/
|
|
236
|
+
type SectionLanding = {
|
|
237
|
+
title?: string;
|
|
238
|
+
/** The `body` field (or summary) rendered from markdown to HTML — render with `set:html`. */
|
|
239
|
+
introHtml?: string;
|
|
240
|
+
hero?: string;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export { type ApiSite as A, CONSENT_CATEGORIES as C, type MarvinContentEntry as M, type PrivacyConsent as P, type SectionLanding as S, type ApiSeo as a, type ApiSiteChrome as b, type ApiNavigationLink as c, type ApiSocialLink as d, type AssetSelectOptions as e, type ApiImage as f, type ApiResourceLink as g, type ApiActionVariant as h, type ApiHero as i, type ApiHeroAction as j, type ApiPageAction as k, COOKIE_CATEGORIES as l, type ConsentCategory as m, type CookieCategory as n, type PrivacyController as o, type PrivacyCookie as p, type PrivacyProcessor as q, type SitePrivacy as r, consentCategories as s, consentVersion as t, cookiesNeedConsent as u, sitePrivacy as v };
|
package/dist/types.d.ts
CHANGED
|
@@ -1,162 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Site-integration types.
|
|
7
|
-
*
|
|
8
|
-
* These are the *resolved* shapes a site renders from — Marvin's raw payloads normalized,
|
|
9
|
-
* defaulted, and made non-optional where a template would otherwise need a guard on every
|
|
10
|
-
* use. Nothing here is specific to a particular site's design system.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Any entry shape the published API can hand back. `MarvinEntry`/`Entry` carry `data_json`;
|
|
15
|
-
* `CollectionEntry`/`MarvinEntryListItem` (list items) do not — see the `hydrate` repository
|
|
16
|
-
* option.
|
|
17
|
-
*/
|
|
18
|
-
type MarvinContentEntry = MarvinEntry | Entry | CollectionEntry | MarvinEntryListItem;
|
|
19
|
-
/** Criteria for picking one asset off an entry's `assets[]`. */
|
|
20
|
-
type AssetSelectOptions = {
|
|
21
|
-
role?: string;
|
|
22
|
-
roles?: string[];
|
|
23
|
-
usage?: string;
|
|
24
|
-
usages?: string[];
|
|
25
|
-
type?: string;
|
|
26
|
-
types?: string[];
|
|
27
|
-
mimeType?: string;
|
|
28
|
-
/** Also accept assets carrying neither a role nor a usage. */
|
|
29
|
-
allowUnroled?: boolean;
|
|
30
|
-
};
|
|
31
|
-
/** A resolved image: a URL plus the presentation hints stored alongside the asset. */
|
|
32
|
-
type ApiImage = {
|
|
33
|
-
src: string;
|
|
34
|
-
alt: string;
|
|
35
|
-
/** CSS `object-position` value, e.g. `"40% 65%"`. */
|
|
36
|
-
focalPoint?: string;
|
|
37
|
-
};
|
|
38
|
-
/** A resolved link to an attached resource (material, construction detail, document…). */
|
|
39
|
-
type ApiResourceLink = {
|
|
40
|
-
name: string;
|
|
41
|
-
/** The resource type, e.g. `'material'`, `'construction'`. */
|
|
42
|
-
type: string;
|
|
43
|
-
/** The relationship role, e.g. `'primary-material'`. */
|
|
44
|
-
role?: string;
|
|
45
|
-
slug?: string;
|
|
46
|
-
href?: string;
|
|
47
|
-
};
|
|
48
|
-
/**
|
|
49
|
-
* Resolved SEO / social-sharing metadata for the site. Mirrors Marvin's SiteSeo, with identity
|
|
50
|
-
* fallbacks already applied (title/description/siteName never empty). The rendering layer
|
|
51
|
-
* (SeoHead) turns this into the actual `<meta>`/Open Graph/Twitter tags.
|
|
52
|
-
*/
|
|
53
|
-
type ApiSeo = {
|
|
54
|
-
title: string;
|
|
55
|
-
titleTemplate?: string;
|
|
56
|
-
description: string;
|
|
57
|
-
keywords: string[];
|
|
58
|
-
robots: string;
|
|
59
|
-
image?: string;
|
|
60
|
-
imageAlt?: string;
|
|
61
|
-
ogType: string;
|
|
62
|
-
siteName: string;
|
|
63
|
-
twitterCard: string;
|
|
64
|
-
twitterHandle?: string;
|
|
65
|
-
twitterCreator?: string;
|
|
66
|
-
fbAppId?: string;
|
|
67
|
-
themeColor?: string;
|
|
68
|
-
publisher?: string;
|
|
69
|
-
canonicalUrl?: string;
|
|
70
|
-
verification: {
|
|
71
|
-
google?: string;
|
|
72
|
-
bing?: string;
|
|
73
|
-
pinterest?: string;
|
|
74
|
-
yandex?: string;
|
|
75
|
-
};
|
|
76
|
-
};
|
|
77
|
-
type ApiSite = {
|
|
78
|
-
workspaceSlug?: string;
|
|
79
|
-
workspaceName?: string;
|
|
80
|
-
name: string;
|
|
81
|
-
title: string;
|
|
82
|
-
tagline?: string;
|
|
83
|
-
description: string;
|
|
84
|
-
canonicalUrl?: string;
|
|
85
|
-
logo?: string;
|
|
86
|
-
favicon?: string;
|
|
87
|
-
/** Round maker's stamp / seal — `site_metadata_json.brand.seal`, when it is a URL. */
|
|
88
|
-
seal?: string;
|
|
89
|
-
/**
|
|
90
|
-
* Brand-asset registry: every `site_metadata_json.brand.<name>` that is a URL, path or data URI,
|
|
91
|
-
* used as-is. Bare asset slugs are left out (Marvin no longer serves assets by slug).
|
|
92
|
-
*/
|
|
93
|
-
brand?: Record<string, string>;
|
|
94
|
-
locale: string;
|
|
95
|
-
timezone: string;
|
|
96
|
-
email?: string;
|
|
97
|
-
imprint?: string;
|
|
98
|
-
social: Record<string, string>;
|
|
99
|
-
seo: ApiSeo;
|
|
100
|
-
/**
|
|
101
|
-
* Embed privacy mode, consent text and the frame sources a `frame-src` CSP must allow.
|
|
102
|
-
* Absent when the server predates media embeds.
|
|
103
|
-
*/
|
|
104
|
-
embeds?: MarvinSiteEmbeds;
|
|
105
|
-
metadata: Record<string, unknown>;
|
|
106
|
-
};
|
|
107
|
-
type ApiNavigationLink = {
|
|
108
|
-
label: string;
|
|
109
|
-
href: string;
|
|
110
|
-
description?: string;
|
|
111
|
-
external: boolean;
|
|
112
|
-
/** Membership role from the nav collection (e.g. `'primary'`, `'legal'`). Drives grouping. */
|
|
113
|
-
role?: string;
|
|
114
|
-
};
|
|
115
|
-
type ApiSocialLink = ApiNavigationLink & {
|
|
116
|
-
icon: string;
|
|
117
|
-
};
|
|
118
|
-
type ApiSiteChrome = {
|
|
119
|
-
site: ApiSite;
|
|
120
|
-
mainNavigation: ApiNavigationLink[];
|
|
121
|
-
footerNavigation: ApiNavigationLink[][];
|
|
122
|
-
/** Legal links (`role: 'legal'` in the footer nav collection), e.g. Terms/Privacy. */
|
|
123
|
-
legalLinks: ApiNavigationLink[];
|
|
124
|
-
socialLinks: ApiSocialLink[];
|
|
125
|
-
inquiry?: ApiNavigationLink;
|
|
126
|
-
};
|
|
127
|
-
type ApiActionVariant = 'primary' | 'secondary';
|
|
128
|
-
type ApiPageAction = ApiNavigationLink & {
|
|
129
|
-
variant: ApiActionVariant;
|
|
130
|
-
};
|
|
131
|
-
type ApiHeroAction = ApiPageAction;
|
|
132
|
-
type ApiHero = {
|
|
133
|
-
kicker: string;
|
|
134
|
-
headline: string;
|
|
135
|
-
body: string;
|
|
136
|
-
actions: ApiHeroAction[];
|
|
137
|
-
layout: 'split' | 'centered';
|
|
138
|
-
image?: {
|
|
139
|
-
src?: string;
|
|
140
|
-
alt: string;
|
|
141
|
-
focalPoint?: string;
|
|
142
|
-
dominantColor?: string;
|
|
143
|
-
fit?: 'cover' | 'contain';
|
|
144
|
-
caption?: string;
|
|
145
|
-
credit?: string;
|
|
146
|
-
orientation?: string;
|
|
147
|
-
};
|
|
148
|
-
};
|
|
149
|
-
/**
|
|
150
|
-
* A section-landing header driven by a Marvin `page` entry (e.g. `bench-notes`, `projects`).
|
|
151
|
-
* The entry is linkable in nav (collection membership) AND supplies the landing's title, intro,
|
|
152
|
-
* and hero — so the top of an index page stops being static. Absent fields resolve to
|
|
153
|
-
* `undefined` so callers fall back to their existing copy/illustration.
|
|
154
|
-
*/
|
|
155
|
-
type SectionLanding = {
|
|
156
|
-
title?: string;
|
|
157
|
-
/** The `body` field (or summary) rendered from markdown to HTML — render with `set:html`. */
|
|
158
|
-
introHtml?: string;
|
|
159
|
-
hero?: string;
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
export { type ApiActionVariant, type ApiHero, type ApiHeroAction, type ApiImage, type ApiNavigationLink, type ApiPageAction, type ApiResourceLink, type ApiSeo, type ApiSite, type ApiSiteChrome, type ApiSocialLink, type AssetSelectOptions, type MarvinContentEntry, MarvinSiteEmbeds, type SectionLanding };
|
|
1
|
+
export { a as MarvinEmbed, b as MarvinEmbedIframe, c as MarvinEmbedKind, d as MarvinEmbedLink, e as MarvinEmbedMode, f as MarvinEmbedStatus, M as MarvinEmbeds, g as MarvinSiteEmbeds } from './embeds-BTNryQA6.js';
|
|
2
|
+
export { h as ApiActionVariant, i as ApiHero, j as ApiHeroAction, f as ApiImage, c as ApiNavigationLink, k as ApiPageAction, g as ApiResourceLink, a as ApiSeo, A as ApiSite, b as ApiSiteChrome, d as ApiSocialLink, e as AssetSelectOptions, m as ConsentCategory, n as CookieCategory, M as MarvinContentEntry, P as PrivacyConsent, o as PrivacyController, p as PrivacyCookie, q as PrivacyProcessor, S as SectionLanding, r as SitePrivacy } from './types-4VdGN9e_.js';
|
|
3
|
+
import '@inneropen/marvin-sdk';
|
package/package.json
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* ConsentBanner — put this once in your layout, first thing in `<body>`.
|
|
4
|
+
*
|
|
5
|
+
* Renders nothing (and ships no script or CSS) unless `privacy.bannerRequired`, i.e. at least one
|
|
6
|
+
* listed cookie or storage entry is not `necessary`. When it does render, the markup is `hidden`
|
|
7
|
+
* and the consent runtime reveals it only when the visitor has no valid choice, so without
|
|
8
|
+
* JavaScript there is no dead banner and nothing optional loads.
|
|
9
|
+
*
|
|
10
|
+
* Accept all and Reject all are the same size and style; Customise opens a native <dialog> with
|
|
11
|
+
* one unticked checkbox per category the site lists. The choice is stored in
|
|
12
|
+
* `localStorage['marvin-consent']` for `maxAgeDays` (default 180, refusals included) and is
|
|
13
|
+
* bound to a hash of the optional cookie list: change the list and visitors are asked again.
|
|
14
|
+
*
|
|
15
|
+
* Gate optional scripts with `<script type="text/plain" data-consent="analytics" data-src="…">`
|
|
16
|
+
* or `<template data-consent="marketing">`; see `consent.ts`. A gated script on a site whose
|
|
17
|
+
* `bannerRequired` is false never runs (a dev build warns about it).
|
|
18
|
+
*
|
|
19
|
+
* `embeds="marketing"` additionally loads click-to-load embeds once that category is allowed.
|
|
20
|
+
* Off by default: the click on a facade is already the visitor's consent.
|
|
21
|
+
*/
|
|
22
|
+
import { consentCategories, consentVersion, cookiesNeedConsent } from '../privacy.js';
|
|
23
|
+
import type { SitePrivacy } from '../privacy.js';
|
|
24
|
+
import ConsentBannerView from './ConsentBannerView.astro';
|
|
25
|
+
import { DEFAULT_MAX_AGE_DAYS, consentLabels, type ConsentLabelOverrides } from './privacy-text.js';
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
/** `site.privacy`. */
|
|
29
|
+
privacy: SitePrivacy | null | undefined;
|
|
30
|
+
/** Link to the cookie/privacy policy page, shown in the banner and dialog. */
|
|
31
|
+
policyHref?: string;
|
|
32
|
+
labels?: ConsentLabelOverrides;
|
|
33
|
+
/** How long a choice (including a refusal) is remembered. Default 180. */
|
|
34
|
+
maxAgeDays?: number;
|
|
35
|
+
/** Also activate click-to-load embeds once this category is allowed. */
|
|
36
|
+
embeds?: 'preferences' | 'analytics' | 'marketing';
|
|
37
|
+
/**
|
|
38
|
+
* Inline `consent.css` next to the markup (default), so it ships only where the banner renders.
|
|
39
|
+
* Set false to import `@inneropen/marvin-astro/components/consent.css` yourself, e.g. under a
|
|
40
|
+
* CSP that forbids inline styles.
|
|
41
|
+
*/
|
|
42
|
+
inlineStyles?: boolean;
|
|
43
|
+
/** Dev warning when `data-consent` markup exists but no banner renders. Default: dev builds. */
|
|
44
|
+
warn?: boolean;
|
|
45
|
+
class?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
privacy,
|
|
50
|
+
policyHref,
|
|
51
|
+
maxAgeDays = DEFAULT_MAX_AGE_DAYS,
|
|
52
|
+
embeds,
|
|
53
|
+
inlineStyles = true,
|
|
54
|
+
warn = import.meta.env?.DEV === true,
|
|
55
|
+
class: className,
|
|
56
|
+
} = Astro.props;
|
|
57
|
+
|
|
58
|
+
const required = !!privacy && (privacy.bannerRequired || cookiesNeedConsent(privacy.cookies));
|
|
59
|
+
const labels = consentLabels(Astro.props.labels);
|
|
60
|
+
const categories = consentCategories(privacy ?? undefined);
|
|
61
|
+
const optional = (privacy?.cookies ?? []).filter((cookie) => cookie.category !== 'necessary');
|
|
62
|
+
const cookieNames = Object.fromEntries(
|
|
63
|
+
categories.map((category) => [
|
|
64
|
+
category,
|
|
65
|
+
optional.filter((cookie) => cookie.category === category).map((cookie) => cookie.name),
|
|
66
|
+
])
|
|
67
|
+
);
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
{required && (
|
|
71
|
+
<ConsentBannerView
|
|
72
|
+
version={consentVersion(privacy ?? undefined)}
|
|
73
|
+
categories={categories}
|
|
74
|
+
cookieNames={cookieNames}
|
|
75
|
+
labels={labels}
|
|
76
|
+
policyHref={policyHref}
|
|
77
|
+
maxAgeDays={maxAgeDays}
|
|
78
|
+
embeds={embeds}
|
|
79
|
+
inlineStyles={inlineStyles}
|
|
80
|
+
class={className}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
{!required && warn && (
|
|
84
|
+
<script is:inline>
|
|
85
|
+
if (document.querySelector('[data-consent]')) console.warn('[marvin-astro] Found data-consent markup, but <ConsentBanner> rendered nothing because site.privacy lists no optional cookie. Those scripts will never run. List the cookie in Marvin, or drop data-consent.');
|
|
86
|
+
</script>
|
|
87
|
+
)}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The markup and script of <ConsentBanner>, split out so Astro only ships this component's script
|
|
4
|
+
* and CSS on pages where the banner is actually rendered. Use <ConsentBanner>.
|
|
5
|
+
*/
|
|
6
|
+
import type { ConsentCategory } from '../privacy.js';
|
|
7
|
+
import type { ConsentLabels } from './privacy-text.js';
|
|
8
|
+
// Inlined where it renders rather than imported: Astro ships an imported stylesheet on every page
|
|
9
|
+
// that merely imports the component, which would put banner CSS on banner-free sites.
|
|
10
|
+
import consentCss from './consent.css?raw';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
version: string;
|
|
14
|
+
categories: ConsentCategory[];
|
|
15
|
+
cookieNames: Record<string, string[]>;
|
|
16
|
+
labels: ConsentLabels;
|
|
17
|
+
policyHref?: string;
|
|
18
|
+
maxAgeDays: number;
|
|
19
|
+
embeds?: string;
|
|
20
|
+
inlineStyles?: boolean;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { version, categories, cookieNames, labels, policyHref, maxAgeDays, embeds, inlineStyles = true, class: className } = Astro.props;
|
|
25
|
+
// A site-relative path is a normal policy link; anything that isn't http(s) once resolved
|
|
26
|
+
// (javascript:, data:, …) is dropped.
|
|
27
|
+
function safeHref(value?: string): string | undefined {
|
|
28
|
+
if (!value) return undefined;
|
|
29
|
+
try {
|
|
30
|
+
return ['http:', 'https:'].includes(new URL(value, 'https://placeholder.invalid').protocol) ? value : undefined;
|
|
31
|
+
} catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const href = safeHref(policyHref);
|
|
36
|
+
// consent.css has no strings, so dropping comments and collapsing whitespace is safe.
|
|
37
|
+
const css = consentCss.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\s+/g, ' ').trim();
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
{inlineStyles && <style is:inline set:html={css} />}
|
|
41
|
+
<div
|
|
42
|
+
class={['marvin-consent', className].filter(Boolean).join(' ')}
|
|
43
|
+
data-marvin-consent-banner
|
|
44
|
+
data-version={version}
|
|
45
|
+
data-max-age={String(maxAgeDays)}
|
|
46
|
+
data-categories={categories.join(' ')}
|
|
47
|
+
data-cookies={JSON.stringify(cookieNames)}
|
|
48
|
+
data-embeds={embeds}
|
|
49
|
+
>
|
|
50
|
+
<section class="marvin-consent__bar" data-marvin-consent-bar aria-labelledby="marvin-consent-title" hidden>
|
|
51
|
+
<div class="marvin-consent__text">
|
|
52
|
+
<p class="marvin-consent__title" id="marvin-consent-title">{labels.title}</p>
|
|
53
|
+
<p class="marvin-consent__body">
|
|
54
|
+
{labels.body}
|
|
55
|
+
{href && (<>{' '}<a href={href}>{labels.policyLink}</a></>)}
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="marvin-consent__actions">
|
|
59
|
+
<button type="button" class="marvin-consent__button" data-marvin-consent-action="accept">{labels.acceptAll}</button>
|
|
60
|
+
<button type="button" class="marvin-consent__button" data-marvin-consent-action="reject">{labels.rejectAll}</button>
|
|
61
|
+
<button type="button" class="marvin-consent__button marvin-consent__button--customise" data-marvin-consent-action="customise">{labels.customise}</button>
|
|
62
|
+
</div>
|
|
63
|
+
</section>
|
|
64
|
+
|
|
65
|
+
<dialog class="marvin-consent__dialog" aria-labelledby="marvin-consent-dialog-title" aria-describedby="marvin-consent-dialog-body">
|
|
66
|
+
<h2 class="marvin-consent__title" id="marvin-consent-dialog-title">{labels.dialogTitle}</h2>
|
|
67
|
+
<p class="marvin-consent__body" id="marvin-consent-dialog-body">
|
|
68
|
+
{labels.dialogBody}
|
|
69
|
+
{href && (<>{' '}<a href={href}>{labels.policyLink}</a></>)}
|
|
70
|
+
</p>
|
|
71
|
+
<div class="marvin-consent__category">
|
|
72
|
+
<label class="marvin-consent__label">
|
|
73
|
+
<input type="checkbox" checked disabled aria-describedby="marvin-consent-desc-necessary" />
|
|
74
|
+
<span>{labels.categories.necessary}</span>
|
|
75
|
+
</label>
|
|
76
|
+
<p class="marvin-consent__description" id="marvin-consent-desc-necessary">{labels.categoryDescriptions.necessary}</p>
|
|
77
|
+
</div>
|
|
78
|
+
{categories.map((category) => (
|
|
79
|
+
<div class="marvin-consent__category">
|
|
80
|
+
<label class="marvin-consent__label">
|
|
81
|
+
<input type="checkbox" name={category} data-marvin-consent-category={category} aria-describedby={`marvin-consent-desc-${category}`} />
|
|
82
|
+
<span>{labels.categories[category]}</span>
|
|
83
|
+
</label>
|
|
84
|
+
<p class="marvin-consent__description" id={`marvin-consent-desc-${category}`}>{labels.categoryDescriptions[category]}</p>
|
|
85
|
+
{cookieNames[category]?.length ? (
|
|
86
|
+
<p class="marvin-consent__cookies">
|
|
87
|
+
{labels.cookiesList}: {cookieNames[category]!.map((name, index) => (
|
|
88
|
+
<>{index > 0 && ', '}<code>{name}</code></>
|
|
89
|
+
))}
|
|
90
|
+
</p>
|
|
91
|
+
) : null}
|
|
92
|
+
</div>
|
|
93
|
+
))}
|
|
94
|
+
<div class="marvin-consent__actions">
|
|
95
|
+
<button type="button" class="marvin-consent__button" data-marvin-consent-action="save">{labels.save}</button>
|
|
96
|
+
</div>
|
|
97
|
+
</dialog>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<script>
|
|
101
|
+
import { installConsent } from './consent';
|
|
102
|
+
|
|
103
|
+
installConsent(document);
|
|
104
|
+
</script>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* CookieSettingsLink — a "Cookie settings" link for a footer.
|
|
4
|
+
*
|
|
5
|
+
* A real link to the cookie/privacy policy page, so it works without JavaScript. With JavaScript
|
|
6
|
+
* and a rendered <ConsentBanner>, a click opens the Customise dialog instead. Pass `privacy` to
|
|
7
|
+
* render nothing on a site that has no optional cookies (and so no banner).
|
|
8
|
+
*/
|
|
9
|
+
import type { SitePrivacy } from '../privacy.js';
|
|
10
|
+
import { cookiesNeedConsent } from '../privacy.js';
|
|
11
|
+
import { consentLabels } from './privacy-text.js';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** The cookie/privacy policy page. */
|
|
15
|
+
href: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
/** `site.privacy`; when given and no banner is required, nothing renders. */
|
|
18
|
+
privacy?: SitePrivacy | null;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { href, label, privacy, class: className } = Astro.props;
|
|
23
|
+
const visible = privacy === undefined || (!!privacy && (privacy.bannerRequired || cookiesNeedConsent(privacy.cookies)));
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
{visible && (
|
|
27
|
+
<a class={['marvin-consent-link', className].filter(Boolean).join(' ')} href={href} data-marvin-consent-open>{label ?? consentLabels().settingsLink}</a>
|
|
28
|
+
)}
|