@devslab/site-kit 0.12.2 → 0.14.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.ko.md +94 -2
- package/README.md +94 -2
- package/dist/chrome.d.ts +20 -1
- package/dist/chrome.d.ts.map +1 -1
- package/dist/solid.js +197 -136
- package/dist/solid.js.map +1 -1
- package/dist/solid.server.js +93 -33
- package/dist/solid.server.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/site-sections.css +7 -1
- package/src/core/gtm.d.mts +12 -0
- package/src/core/gtm.mjs +76 -0
- package/src/core/index.d.mts +1 -0
- package/src/core/index.mjs +1 -0
- package/src/tanstack-start.d.mts +9 -3
- package/src/tanstack-start.mjs +21 -1
- package/styles.css +28 -6
package/dist/types.d.ts
CHANGED
|
@@ -23,11 +23,25 @@ export interface SiteLink {
|
|
|
23
23
|
href: string;
|
|
24
24
|
label: string;
|
|
25
25
|
external?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Drawn heavier than its neighbours. For a link the product must make
|
|
28
|
+
* easy to find — a Korean site's 개인정보처리방침 (privacy policy), which
|
|
29
|
+
* the law asks to stand out from the other footer links.
|
|
30
|
+
*/
|
|
31
|
+
emphasis?: boolean;
|
|
26
32
|
}
|
|
27
33
|
export interface SiteBrand {
|
|
28
34
|
name: string;
|
|
29
35
|
href: string;
|
|
30
36
|
logo?: JSX.Element;
|
|
37
|
+
/**
|
|
38
|
+
* The header brand link's accessible name, when the visible name is not
|
|
39
|
+
* enough ("Acme home"). Start it with the visible name so voice users can
|
|
40
|
+
* say what they see. A product whose wordmark is its logo passes the mark
|
|
41
|
+
* as `logo` and `name: ""` — the name would otherwise print twice — and
|
|
42
|
+
* names the link here.
|
|
43
|
+
*/
|
|
44
|
+
label?: string;
|
|
31
45
|
}
|
|
32
46
|
export interface LocaleState {
|
|
33
47
|
locale: SiteLocale;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/solid/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,QAAQ;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/solid/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AACD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AACD,MAAM,WAAW,WAAW;IAAG,MAAM,EAAE,UAAU,CAAC;IAAC,aAAa,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,CAAA;CAAE;AAClG,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devslab/site-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Strict i18n, SEO/GEO, and Solid public-site shells for DevsLab products",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"./site-sections.css": "./site-sections.css"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@devslab/dds-solid": "0.
|
|
63
|
+
"@devslab/dds-solid": "0.14.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
66
|
"@tanstack/solid-router": "1.170.30",
|
package/site-sections.css
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
tokens carry light and dark. */
|
|
5
5
|
|
|
6
6
|
.site-section { padding-block: var(--dds-space-64); border-block-start: 1px solid var(--dds-color-border-default); }
|
|
7
|
+
/* The site header sticks to the top; an in-page link ("#how") stops the section below it, not under it.
|
|
8
|
+
A product that changes --site-header-block-size sets it on :root or .site-shell (an ancestor of both
|
|
9
|
+
the header and <main>) — set on .site-header it resizes the header but never reaches the sections. */
|
|
10
|
+
.site-hero, .site-section { scroll-margin-block-start: calc(var(--site-header-block-size, 64px) + var(--dds-space-8)); }
|
|
7
11
|
.site-section[data-tone="band"] { background: var(--dds-color-bg-subtle); }
|
|
8
12
|
.site-section__shell { inline-size: min(100% - var(--dds-space-32), 1120px); margin-inline: auto; }
|
|
9
13
|
|
|
@@ -53,7 +57,9 @@ html:lang(ko) .site-rows__body { word-break: keep-all; overflow-wrap: break-word
|
|
|
53
57
|
height rule — a swapping scene fixes its own height in the consumer. */
|
|
54
58
|
.site-hero { padding-block: var(--dds-space-64); }
|
|
55
59
|
.site-hero__shell { inline-size: min(100% - var(--dds-space-32), 1120px); margin-inline: auto; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--dds-space-48); align-items: start; }
|
|
56
|
-
|
|
60
|
+
/* Tracking and weight are hooks: wide mono tracking suits Latin caps, not Hangul — a product whose
|
|
61
|
+
eyebrow is Korean sets --site-hero-eyebrow-tracking: 0 (and a weight) on its landing root. */
|
|
62
|
+
.site-hero__eyebrow { margin: 0; margin-block-end: var(--dds-space-12); color: var(--dds-color-text-brand); font-family: var(--dds-font-family-mono); font-size: var(--dds-typo-caption-font-size); font-weight: var(--site-hero-eyebrow-weight, normal); letter-spacing: var(--site-hero-eyebrow-tracking, .18em); }
|
|
57
63
|
.site-hero__copy h1 { margin: 0; font-size: var(--dds-typo-display-font-size); line-height: var(--dds-typo-display-line-height); letter-spacing: -.025em; text-wrap: balance; white-space: pre-line; }
|
|
58
64
|
.site-hero__lede { margin: 0; margin-block-start: var(--dds-space-24); max-inline-size: 46ch; color: var(--dds-color-text-secondary); font-size: var(--dds-typo-body-1-font-size); line-height: 1.7; }
|
|
59
65
|
.site-hero__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--dds-space-16) var(--dds-space-24); margin-block-start: var(--dds-space-32); }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** `GTM-` followed by uppercase letters and digits. Anything else is refused before it reaches a script string. */
|
|
2
|
+
export declare const GTM_CONTAINER_ID_PATTERN: RegExp;
|
|
3
|
+
/**
|
|
4
|
+
* Body of Google's nonce-aware Tag Manager head loader for `containerId`, without the `<script>` tag or a nonce —
|
|
5
|
+
* the renderer owns the element and its per-request nonce. Throws RangeError for an id that does not match GTM_CONTAINER_ID_PATTERN.
|
|
6
|
+
*/
|
|
7
|
+
export declare function gtmHeadScript(containerId: string): string;
|
|
8
|
+
/** The `<iframe>` HTML Google puts inside `<noscript>` right after `<body>` opens. Throws RangeError for a malformed id. */
|
|
9
|
+
export declare function gtmNoscriptIframe(containerId: string): string;
|
|
10
|
+
export type GtmCspDirective = "script-src" | "connect-src" | "img-src" | "frame-src";
|
|
11
|
+
/** CSP sources for Tag Manager + GA4 without Ads features (Google's CSP guide). Excludes preview mode, Custom JavaScript variables and Ads hosts. */
|
|
12
|
+
export declare const GTM_CSP_SOURCES: Readonly<Record<GtmCspDirective, readonly string[]>>;
|
package/src/core/gtm.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Tag Manager, written once for the family sites (D-031).
|
|
3
|
+
*
|
|
4
|
+
* The strings below are Google's, character for character, with only the
|
|
5
|
+
* container id substituted. The loader is the nonce-aware variant Google
|
|
6
|
+
* publishes for pages under a Content Security Policy
|
|
7
|
+
* (https://developers.google.com/tag-platform/security/guides/csp): it is the
|
|
8
|
+
* standard snippet plus one statement that copies the page's nonce onto the
|
|
9
|
+
* gtm.js element it creates, so Tag Manager can pass that nonce on to the
|
|
10
|
+
* scripts it adds. Every family site runs a nonce CSP, so this is the variant
|
|
11
|
+
* that fits all of them; on a page without nonces the extra statement finds no
|
|
12
|
+
* `[nonce]` element and does nothing.
|
|
13
|
+
*
|
|
14
|
+
* The id is validated before it is placed inside a script string: a value
|
|
15
|
+
* that is not `GTM-` plus uppercase letters and digits never reaches the
|
|
16
|
+
* output. The pattern admits no quote, backslash, `<` or whitespace, so the
|
|
17
|
+
* id cannot close the string literal or the element it sits in.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const GTM_CONTAINER_ID_PATTERN = /^GTM-[A-Z0-9]+$/;
|
|
21
|
+
|
|
22
|
+
const containerId = (value) => {
|
|
23
|
+
if (typeof value !== "string" || !GTM_CONTAINER_ID_PATTERN.test(value)) {
|
|
24
|
+
throw new RangeError(`Google Tag Manager container id must match ${GTM_CONTAINER_ID_PATTERN} (for example "GTM-AB12CD3"); got ${JSON.stringify(value)}`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The body of the head loader `<script>` — no `<script>` tag and no nonce:
|
|
31
|
+
* the page's renderer owns the element and puts the per-request nonce on it
|
|
32
|
+
* (in TanStack Start the router does, from `ssr.nonce`).
|
|
33
|
+
*/
|
|
34
|
+
export function gtmHeadScript(id) {
|
|
35
|
+
return `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
|
36
|
+
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
|
37
|
+
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
38
|
+
'https://www.googletagmanager.com/gtm.js?id='+i+dl;var n=d.querySelector('[nonce]');
|
|
39
|
+
n&&j.setAttribute('nonce',n.nonce||n.getAttribute('nonce'));f.parentNode.insertBefore(j,f);
|
|
40
|
+
})(window,document,'script','dataLayer','${containerId(id)}');`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The contents of the `<noscript>` Google places right after `<body>` opens. */
|
|
44
|
+
export function gtmNoscriptIframe(id) {
|
|
45
|
+
return `<iframe src="https://www.googletagmanager.com/ns.html?id=${containerId(id)}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const sources = (...list) => Object.freeze(list);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* CSP sources a page needs for Tag Manager plus Google Analytics 4 without
|
|
52
|
+
* Ads features, per Google's "Use Tag Manager with a Content Security
|
|
53
|
+
* Policy" guide. Add each list to the directive of the same name, next to
|
|
54
|
+
* the page's own nonce.
|
|
55
|
+
*
|
|
56
|
+
* - script-src: Google lists it under `script-src-elem`; a policy without
|
|
57
|
+
* that directive falls back to `script-src`, which is where the family
|
|
58
|
+
* sites keep their nonce.
|
|
59
|
+
* - connect-src: `*.google.com` is Google's own entry and also covers
|
|
60
|
+
* `www.google.com` (the container's) and `*.analytics.google.com` (GA4's
|
|
61
|
+
* regional collection hosts) — a CSP wildcard matches any depth of
|
|
62
|
+
* subdomain.
|
|
63
|
+
* - frame-src: the `<noscript>` iframe.
|
|
64
|
+
*
|
|
65
|
+
* Not included, by design: Tag Manager preview mode (tagmanager.google.com,
|
|
66
|
+
* gstatic, Google Fonts), Custom JavaScript variables (`'unsafe-eval'`), and
|
|
67
|
+
* the Google Ads / Google signals hosts (doubleclick, googlesyndication,
|
|
68
|
+
* googleadservices, `*.google.<TLD>`). A product that turns one of those on
|
|
69
|
+
* adds its sources from the same guide.
|
|
70
|
+
*/
|
|
71
|
+
export const GTM_CSP_SOURCES = Object.freeze({
|
|
72
|
+
"script-src": sources("https://www.googletagmanager.com"),
|
|
73
|
+
"connect-src": sources("https://www.googletagmanager.com", "https://*.google-analytics.com", "https://*.google.com"),
|
|
74
|
+
"img-src": sources("https://www.googletagmanager.com", "https://*.google-analytics.com"),
|
|
75
|
+
"frame-src": sources("https://www.googletagmanager.com"),
|
|
76
|
+
});
|
package/src/core/index.d.mts
CHANGED
package/src/core/index.mjs
CHANGED
|
@@ -3,3 +3,4 @@ export { CatalogValidationError, createTranslator, validateCatalogs } from "./ca
|
|
|
3
3
|
export { BRAND_ICON_FILES, ROBOTS_USER_AGENTS, brandIconLinks, buildMetadata, buildRobots, buildSitemap, localizedPath, localizedUrl, renderSitemapXml } from "./seo.mjs";
|
|
4
4
|
export { VerifiedFactRegistry, buildVerifiedJsonLd, renderLlmsTxt } from "./geo.mjs";
|
|
5
5
|
export { definePublisher, buildPublisher, serializeJsonLd, renderPublisherHtml } from "./publisher.mjs";
|
|
6
|
+
export { GTM_CONTAINER_ID_PATTERN, GTM_CSP_SOURCES, gtmHeadScript, gtmNoscriptIframe } from "./gtm.mjs";
|
package/src/tanstack-start.d.mts
CHANGED
|
@@ -6,7 +6,13 @@ import type { SiteLocale } from "./core/locales.mjs";
|
|
|
6
6
|
* registry (D-018) yields `SiteMetadata<string>`; the adapter reads the
|
|
7
7
|
* same fields whatever the code type is, so it must not refuse that.
|
|
8
8
|
*/
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* `icons: true` appends brandIconLinks() (the /brand default); an object passes its options through. Omitted, no icon links are emitted.
|
|
11
|
+
* `gtm` adds Google Tag Manager's head loader for that container id as `scripts[0]`; omitted or `undefined` (a route without Tag Manager), the result has no `scripts` key.
|
|
12
|
+
*/
|
|
13
|
+
export interface TanStackHeadOptions { icons?: boolean | { basePath?: string }; gtm?: string | undefined }
|
|
14
|
+
/** A route `head().scripts` entry: the loader body only. The router adds `ssr.nonce` when it renders the tag. Throws RangeError for a malformed id. */
|
|
15
|
+
export interface GtmHeadEntry { children: string }
|
|
16
|
+
export declare function gtmHeadEntry(containerId: string): GtmHeadEntry;
|
|
17
|
+
export declare function toTanStackHead<Code extends string = SiteLocale>(metadata: SiteMetadata<Code>, options?: TanStackHeadOptions): { meta: Array<Record<string, string>>; links: Array<Record<string, string>>; scripts?: GtmHeadEntry[] };
|
|
12
18
|
export declare const toHtmlAttributes: <Code extends string = SiteLocale>(metadata: SiteMetadata<Code>) => { lang: Code; dir: string };
|
package/src/tanstack-start.mjs
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import { brandIconLinks } from "./core/seo.mjs";
|
|
2
|
+
import { gtmHeadScript } from "./core/gtm.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A route `head().scripts` entry that loads Google Tag Manager (D-031).
|
|
6
|
+
*
|
|
7
|
+
* Only `children`: no `nonce`, on purpose. The router's HeadContent stamps
|
|
8
|
+
* `router.options.ssr.nonce` on every head script it renders, after the
|
|
9
|
+
* entry's own attributes, so the per-request nonce reaches this script the
|
|
10
|
+
* same way it reaches the meta and link tags. A nonce written here would be
|
|
11
|
+
* overwritten anyway. The product sets `ssr.nonce` when it creates the
|
|
12
|
+
* router (asklinq#427); without it the loader renders with no nonce and the
|
|
13
|
+
* page's CSP blocks it.
|
|
14
|
+
*/
|
|
15
|
+
export function gtmHeadEntry(containerId) {
|
|
16
|
+
return { children: gtmHeadScript(containerId) };
|
|
17
|
+
}
|
|
2
18
|
|
|
3
19
|
// `icons` is opt-in: the adapter cannot know where (or whether) a product
|
|
4
20
|
// serves the linq-brand files, and a head that links icons the server 404s
|
|
5
21
|
// is worse than one that links none. `true` takes the /brand default.
|
|
22
|
+
// `gtm` is opt-in the same way, and per route: the product decides which
|
|
23
|
+
// pages carry Tag Manager (public marketing and legal pages, not consoles).
|
|
6
24
|
export function toTanStackHead(metadata, options = {}) {
|
|
7
25
|
const icons = options.icons === true ? brandIconLinks() : options.icons ? brandIconLinks(options.icons) : [];
|
|
8
|
-
|
|
26
|
+
const head = {
|
|
9
27
|
meta: [
|
|
10
28
|
{ title: metadata.title },
|
|
11
29
|
{ name: "description", content: metadata.description },
|
|
@@ -27,6 +45,8 @@ export function toTanStackHead(metadata, options = {}) {
|
|
|
27
45
|
...icons,
|
|
28
46
|
],
|
|
29
47
|
};
|
|
48
|
+
if (options.gtm !== undefined) head.scripts = [gtmHeadEntry(options.gtm)];
|
|
49
|
+
return head;
|
|
30
50
|
}
|
|
31
51
|
|
|
32
52
|
export const toHtmlAttributes = (metadata) => ({ lang: metadata.html.lang, dir: metadata.html.dir });
|
package/styles.css
CHANGED
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
.site-main { flex: 1; inline-size: min(100% - var(--dds-space-32), 1120px); margin-inline: auto; }
|
|
9
9
|
.site-main--bleed { inline-size: 100%; }
|
|
10
10
|
.site-header { position: sticky; inset-block-start: 0; z-index: 100; border-block-end: 1px solid var(--dds-color-border-default); background: var(--dds-color-bg-default); }
|
|
11
|
-
.site-header__inner { min-block-size: 64px; inline-size: min(100% - var(--dds-space-32), 1120px); margin-inline: auto; display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: var(--dds-space-16); }
|
|
11
|
+
.site-header__inner { min-block-size: var(--site-header-block-size, 64px); inline-size: min(100% - var(--dds-space-32), 1120px); margin-inline: auto; display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: var(--dds-space-16); }
|
|
12
12
|
.site-brand { justify-self: start; color: var(--dds-color-text-primary); font-weight: 700; text-decoration: none; }
|
|
13
13
|
.site-nav { grid-column: 2; justify-self: center; min-inline-size: 0; }
|
|
14
14
|
.site-nav__list, .site-footer__links { display: flex; flex-wrap: wrap; gap: var(--dds-space-16); padding: 0; margin: 0; list-style: none; }
|
|
15
15
|
.site-nav__list a, .site-footer__links a { color: var(--dds-color-text-secondary); text-decoration: none; }
|
|
16
16
|
.site-nav__list a:hover, .site-footer__links a:hover { color: var(--dds-color-text-primary); text-decoration: underline; }
|
|
17
|
+
/* SiteLink.emphasis — a link the product must make easy to find (a Korean privacy policy). */
|
|
18
|
+
:is(.site-nav__list, .site-footer__links, .site-footer__brand) a.site-link--emphasis { color: var(--dds-color-text-primary); font-weight: 700; }
|
|
17
19
|
.site-header__controls { grid-column: 3; justify-self: end; display: flex; align-items: center; gap: var(--dds-space-8); }
|
|
18
20
|
.site-menu-button { display: none; }
|
|
19
21
|
.site-locale { inline-size: auto; min-inline-size: 9rem; }
|
|
@@ -22,7 +24,8 @@
|
|
|
22
24
|
/* The kit gave these rows layout and colour but no size, so the footer
|
|
23
25
|
inherited the 16px body and read a step larger than the same row on a
|
|
24
26
|
sibling product's page. Every consumer that noticed fixed it locally. */
|
|
25
|
-
|
|
27
|
+
/* Baseline, so a bare copyright line sits level with links that are 44px targets on touch. */
|
|
28
|
+
.site-footer__links { font-size: var(--dds-typo-body-2-font-size); align-items: baseline; }
|
|
26
29
|
.site-footer__langs { font-size: var(--dds-typo-caption-font-size); }
|
|
27
30
|
.site-footer__langs-menu { position: relative; display: inline-block; }
|
|
28
31
|
.site-footer__langs-trigger { display: inline-flex; align-items: center; gap: var(--dds-space-4); color: var(--dds-color-text-muted); cursor: pointer; list-style: none; }
|
|
@@ -38,9 +41,17 @@
|
|
|
38
41
|
.site-footer__langs-list a[aria-current="page"] { color: var(--dds-color-text-primary); font-weight: 600; }
|
|
39
42
|
.site-footer__row { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: var(--dds-space-16) var(--dds-space-32); }
|
|
40
43
|
.site-footer__brand { display: inline-flex; align-items: center; gap: var(--dds-space-8); margin: 0; color: var(--dds-color-text-secondary); }
|
|
41
|
-
|
|
44
|
+
/* The icon mark beside a printed name. A wordmark passed as the logo (name "") keeps its own size. */
|
|
45
|
+
.site-footer__brand:has(> strong) :is(img, svg) { block-size: 16px; inline-size: 16px; }
|
|
42
46
|
.site-footer__brand a { color: inherit; text-decoration: none; }
|
|
43
47
|
.site-footer__brand a:hover { color: var(--dds-color-text-primary); text-decoration: underline; }
|
|
48
|
+
/* SiteFooter.details — a block under the brand line (business registration, address). */
|
|
49
|
+
.site-footer__identity { display: grid; gap: var(--dds-space-8); min-inline-size: 0; }
|
|
50
|
+
.site-footer__details { display: grid; gap: var(--dds-space-4); color: var(--dds-color-text-secondary); font-size: var(--dds-typo-body-2-font-size); }
|
|
51
|
+
.site-footer__details > * { margin: 0; }
|
|
52
|
+
.site-footer__details address { font-style: normal; }
|
|
53
|
+
/* With details the left column runs several lines; the links sit level with its first line, not its middle. */
|
|
54
|
+
.site-footer__row:has(> .site-footer__identity) { align-items: first baseline; }
|
|
44
55
|
.site-legal { max-inline-size: 760px; margin-inline: auto; padding-block: var(--dds-space-40); }
|
|
45
56
|
.site-status { border: 1px solid var(--dds-color-border-default); border-inline-start-width: var(--dds-space-4); border-radius: var(--dds-radius-md); padding: var(--dds-space-12) var(--dds-space-16); }
|
|
46
57
|
.site-status--danger { border-inline-start-color: var(--dds-color-status-danger); }
|
|
@@ -54,14 +65,25 @@
|
|
|
54
65
|
.oss-product-mark--md { --oss-product-mark-size: 32px; }
|
|
55
66
|
.oss-product-mark--lg { --oss-product-mark-size: 48px; }
|
|
56
67
|
|
|
68
|
+
/* Touch: every link in the chrome is a 44px target (spec §6), as every button already is
|
|
69
|
+
(dds-css button.css). Placed before the narrow block so its menu rows can override the display. */
|
|
70
|
+
@media (pointer: coarse) {
|
|
71
|
+
.site-brand, .site-nav__list a, .site-footer__links a, .site-footer__brand a, .site-footer__langs-trigger { display: inline-flex; align-items: center; min-block-size: 44px; }
|
|
72
|
+
.site-footer__langs-list a { display: flex; align-items: center; min-block-size: 44px; }
|
|
73
|
+
}
|
|
74
|
+
|
|
57
75
|
@media (max-width: 720px) {
|
|
58
|
-
|
|
76
|
+
/* The first row is the header's own height whatever the button's size (44px on touch), so the
|
|
77
|
+
closed header is the same 64px everywhere and a section's scroll offset holds. */
|
|
78
|
+
.site-header__inner { grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: var(--site-header-block-size, 64px); padding-block: 0; }
|
|
59
79
|
.site-menu-button { display: inline-flex; grid-column: 2; grid-row: 1; }
|
|
60
80
|
.site-nav { display: none; grid-column: 1 / -1; grid-row: 2; justify-self: stretch; }
|
|
61
81
|
.site-nav[data-open="true"] { display: grid; }
|
|
62
|
-
|
|
82
|
+
/* The open menu is a list of full-width 44px rows (about the pitch the 16px gap gave text lines). */
|
|
83
|
+
.site-nav__list { display: grid; gap: 0; }
|
|
84
|
+
.site-nav__list a { display: flex; align-items: center; min-block-size: 44px; }
|
|
63
85
|
.site-header__controls { display: none; grid-column: 1 / -1; grid-row: 3; flex-wrap: wrap; }
|
|
64
|
-
.site-header__controls[data-open="true"] { display: flex; }
|
|
86
|
+
.site-header__controls[data-open="true"] { display: flex; padding-block-end: var(--dds-space-12); }
|
|
65
87
|
}
|
|
66
88
|
|
|
67
89
|
@media (prefers-reduced-motion: reduce) { .site-header { scroll-behavior: auto; } }
|