@brandlab-digital/site-next 3.7.0 → 3.9.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/instrumentation-client.d.ts +23 -4
- package/dist/instrumentation-client.d.ts.map +1 -1
- package/dist/instrumentation-client.js +65 -3
- package/dist/instrumentation-client.js.map +1 -1
- package/dist/navigation/constants.d.ts +25 -0
- package/dist/navigation/constants.d.ts.map +1 -0
- package/dist/navigation/constants.js +30 -0
- package/dist/navigation/constants.js.map +1 -0
- package/dist/navigation/index.d.ts +11 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +11 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/navigation/use-back-to-top.d.ts +5 -0
- package/dist/navigation/use-back-to-top.d.ts.map +1 -0
- package/dist/navigation/use-back-to-top.js +32 -0
- package/dist/navigation/use-back-to-top.js.map +1 -0
- package/dist/navigation/use-header-metrics.d.ts +17 -0
- package/dist/navigation/use-header-metrics.d.ts.map +1 -0
- package/dist/navigation/use-header-metrics.js +52 -0
- package/dist/navigation/use-header-metrics.js.map +1 -0
- package/dist/navigation/use-navigation-menu.d.ts +39 -0
- package/dist/navigation/use-navigation-menu.d.ts.map +1 -0
- package/dist/navigation/use-navigation-menu.js +206 -0
- package/dist/navigation/use-navigation-menu.js.map +1 -0
- package/dist/navigation/use-sticky-call-bar.d.ts +15 -0
- package/dist/navigation/use-sticky-call-bar.d.ts.map +1 -0
- package/dist/navigation/use-sticky-call-bar.js +110 -0
- package/dist/navigation/use-sticky-call-bar.js.map +1 -0
- package/dist/ui-kit/brandlab-skin.d.ts +27 -0
- package/dist/ui-kit/brandlab-skin.d.ts.map +1 -0
- package/dist/ui-kit/brandlab-skin.js +2 -0
- package/dist/ui-kit/brandlab-skin.js.map +1 -0
- package/dist/ui-kit/icons.d.ts +15 -0
- package/dist/ui-kit/icons.d.ts.map +1 -0
- package/dist/ui-kit/icons.js +72 -0
- package/dist/ui-kit/icons.js.map +1 -0
- package/dist/ui-kit/primitives/index.d.ts +6 -0
- package/dist/ui-kit/primitives/index.d.ts.map +1 -0
- package/dist/ui-kit/primitives/index.js +6 -0
- package/dist/ui-kit/primitives/index.js.map +1 -0
- package/dist/ui-kit/primitives/layout.d.ts +73 -0
- package/dist/ui-kit/primitives/layout.d.ts.map +1 -0
- package/dist/ui-kit/primitives/layout.js +34 -0
- package/dist/ui-kit/primitives/layout.js.map +1 -0
- package/dist/ui-kit/primitives/links.d.ts +48 -0
- package/dist/ui-kit/primitives/links.d.ts.map +1 -0
- package/dist/ui-kit/primitives/links.js +36 -0
- package/dist/ui-kit/primitives/links.js.map +1 -0
- package/dist/ui-kit/primitives/quote-button.d.ts +20 -0
- package/dist/ui-kit/primitives/quote-button.d.ts.map +1 -0
- package/dist/ui-kit/primitives/quote-button.js +15 -0
- package/dist/ui-kit/primitives/quote-button.js.map +1 -0
- package/dist/ui-kit/shell/back-to-top-client.d.ts +9 -0
- package/dist/ui-kit/shell/back-to-top-client.d.ts.map +1 -0
- package/dist/ui-kit/shell/back-to-top-client.js +8 -0
- package/dist/ui-kit/shell/back-to-top-client.js.map +1 -0
- package/dist/ui-kit/shell/back-to-top.d.ts +13 -0
- package/dist/ui-kit/shell/back-to-top.d.ts.map +1 -0
- package/dist/ui-kit/shell/back-to-top.js +14 -0
- package/dist/ui-kit/shell/back-to-top.js.map +1 -0
- package/dist/ui-kit/shell/call-bar-client.d.ts +7 -0
- package/dist/ui-kit/shell/call-bar-client.d.ts.map +1 -0
- package/dist/ui-kit/shell/call-bar-client.js +13 -0
- package/dist/ui-kit/shell/call-bar-client.js.map +1 -0
- package/dist/ui-kit/shell/current-link.d.ts +15 -0
- package/dist/ui-kit/shell/current-link.d.ts.map +1 -0
- package/dist/ui-kit/shell/current-link.js +34 -0
- package/dist/ui-kit/shell/current-link.js.map +1 -0
- package/dist/ui-kit/shell/drawer-client.d.ts +22 -0
- package/dist/ui-kit/shell/drawer-client.d.ts.map +1 -0
- package/dist/ui-kit/shell/drawer-client.js +70 -0
- package/dist/ui-kit/shell/drawer-client.js.map +1 -0
- package/dist/ui-kit/shell/header-client.d.ts +44 -0
- package/dist/ui-kit/shell/header-client.d.ts.map +1 -0
- package/dist/ui-kit/shell/header-client.js +59 -0
- package/dist/ui-kit/shell/header-client.js.map +1 -0
- package/dist/ui-kit/shell/index.d.ts +6 -0
- package/dist/ui-kit/shell/index.d.ts.map +1 -0
- package/dist/ui-kit/shell/index.js +6 -0
- package/dist/ui-kit/shell/index.js.map +1 -0
- package/dist/ui-kit/shell/mobile-call-bar.d.ts +16 -0
- package/dist/ui-kit/shell/mobile-call-bar.d.ts.map +1 -0
- package/dist/ui-kit/shell/mobile-call-bar.js +15 -0
- package/dist/ui-kit/shell/mobile-call-bar.js.map +1 -0
- package/dist/ui-kit/shell/mobile-drawer.d.ts +37 -0
- package/dist/ui-kit/shell/mobile-drawer.d.ts.map +1 -0
- package/dist/ui-kit/shell/mobile-drawer.js +25 -0
- package/dist/ui-kit/shell/mobile-drawer.js.map +1 -0
- package/dist/ui-kit/shell/shell-context.d.ts +3 -0
- package/dist/ui-kit/shell/shell-context.d.ts.map +1 -0
- package/dist/ui-kit/shell/shell-context.js +5 -0
- package/dist/ui-kit/shell/shell-context.js.map +1 -0
- package/dist/ui-kit/shell/site-header.d.ts +52 -0
- package/dist/ui-kit/shell/site-header.d.ts.map +1 -0
- package/dist/ui-kit/shell/site-header.js +36 -0
- package/dist/ui-kit/shell/site-header.js.map +1 -0
- package/dist/ui-kit/shell/social-links.d.ts +8 -0
- package/dist/ui-kit/shell/social-links.d.ts.map +1 -0
- package/dist/ui-kit/shell/social-links.js +9 -0
- package/dist/ui-kit/shell/social-links.js.map +1 -0
- package/dist/ui-kit/shell/utility-bar.d.ts +30 -0
- package/dist/ui-kit/shell/utility-bar.d.ts.map +1 -0
- package/dist/ui-kit/shell/utility-bar.js +15 -0
- package/dist/ui-kit/shell/utility-bar.js.map +1 -0
- package/dist/ui-kit/skin-index.d.ts +3 -0
- package/dist/ui-kit/skin-index.d.ts.map +1 -0
- package/dist/ui-kit/skin-index.js +2 -0
- package/dist/ui-kit/skin-index.js.map +1 -0
- package/dist/ui-kit/skin.d.ts +48 -0
- package/dist/ui-kit/skin.d.ts.map +1 -0
- package/dist/ui-kit/skin.js +47 -0
- package/dist/ui-kit/skin.js.map +1 -0
- package/dist/ui-kit/structure.css +717 -0
- package/dist/ui-kit/types.d.ts +27 -0
- package/dist/ui-kit/types.d.ts.map +1 -0
- package/dist/ui-kit/types.js +6 -0
- package/dist/ui-kit/types.js.map +1 -0
- package/package.json +25 -4
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout primitives: `Container`, `Section`, `SectionHeading`, `Eyebrow`, `RatingStars` and `VisuallyHidden`.
|
|
3
|
+
* Server components. Heading levels come from the page outline (one H1 in the hero or page header, an H2 per
|
|
4
|
+
* section), never from a look.
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactNode } from 'react';
|
|
7
|
+
import type { PhoneCallPlacement } from '../../analytics.js';
|
|
8
|
+
import { type SkinEntry } from '../skin.js';
|
|
9
|
+
export declare const CONTAINER_WIDTHS: readonly ["default", "narrow", "wide"];
|
|
10
|
+
export type ContainerWidth = (typeof CONTAINER_WIDTHS)[number];
|
|
11
|
+
export type ContainerSkin = SkinEntry<'root', never, {
|
|
12
|
+
width?: ContainerWidth;
|
|
13
|
+
}>;
|
|
14
|
+
/** One container width shared by the header, the page and the footer (`--bl-container`, `--bl-gutter`). */
|
|
15
|
+
export declare function Container({ children, layout, classes }: ContainerSkin & {
|
|
16
|
+
readonly children: ReactNode;
|
|
17
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
/** The four section tones `SectionSequence` assigns: two alternating grounds, one optional band, one photo ground. */
|
|
19
|
+
export declare const SECTION_TONES: readonly ["canvas", "surface", "band", "photo"];
|
|
20
|
+
export type SectionTone = (typeof SECTION_TONES)[number];
|
|
21
|
+
export type SectionSlot = 'root' | 'inner';
|
|
22
|
+
export interface SectionProps {
|
|
23
|
+
/** The matrix id, rendered as `data-bl-section` (for example `reviews`, `cta`, `faqs`). */
|
|
24
|
+
readonly id: string;
|
|
25
|
+
/** The id of the section's H2. A section is a `<section>` labelled by its heading. */
|
|
26
|
+
readonly labelledBy: string;
|
|
27
|
+
readonly tone?: SectionTone | undefined;
|
|
28
|
+
readonly variant?: string | undefined;
|
|
29
|
+
readonly classes?: Readonly<Partial<Record<SectionSlot, string>>> | undefined;
|
|
30
|
+
/** Signature art, rendered `aria-hidden`. */
|
|
31
|
+
readonly decoration?: ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* Extra layout hooks on the root, for example `{ 'data-columns': '3' }`. They cannot override `data-bl-section`,
|
|
34
|
+
* `data-variant`, `data-tone` or `data-phone-location`.
|
|
35
|
+
*/
|
|
36
|
+
readonly hooks?: Readonly<Record<`data-${string}`, string | undefined>>;
|
|
37
|
+
/** The placement a `tel:` link inside the section reports when it has none of its own. */
|
|
38
|
+
readonly phonePlacement?: PhoneCallPlacement | undefined;
|
|
39
|
+
readonly children: ReactNode;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The root every section component renders: `<section data-bl-section aria-labelledby>`, its tone and variant, an
|
|
43
|
+
* optional `aria-hidden` decoration and the inner container slot.
|
|
44
|
+
*/
|
|
45
|
+
export declare function Section({ id, labelledBy, tone, variant, classes, decoration, hooks, phonePlacement, children }: SectionProps): import("react/jsx-runtime").JSX.Element;
|
|
46
|
+
export type SectionHeadingSlot = 'head' | 'eyebrow' | 'heading' | 'lead';
|
|
47
|
+
export type SectionHeadingSkin = SkinEntry<SectionHeadingSlot, never, {
|
|
48
|
+
align?: 'start' | 'center';
|
|
49
|
+
}>;
|
|
50
|
+
export interface SectionHeadingProps extends SectionHeadingSkin {
|
|
51
|
+
/** The H2's id, which the section's `aria-labelledby` points at. */
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly title: ReactNode;
|
|
54
|
+
/** A short label above the H2 (at least 13 px; never the only label for the section). */
|
|
55
|
+
readonly eyebrow?: ReactNode;
|
|
56
|
+
readonly lead?: ReactNode;
|
|
57
|
+
}
|
|
58
|
+
/** A section's heading block: an optional eyebrow, the H2 and an optional lead paragraph. */
|
|
59
|
+
export declare function SectionHeading({ id, title, eyebrow, lead, layout, classes }: SectionHeadingProps): import("react/jsx-runtime").JSX.Element;
|
|
60
|
+
/**
|
|
61
|
+
* Five stars with one text alternative ("Rated 4.9 out of 5"). The stars use `--bl-color-star`; whole stars only,
|
|
62
|
+
* rounded to the nearest. The rating always comes from the feed, never typed by hand (T4).
|
|
63
|
+
*/
|
|
64
|
+
export declare function RatingStars({ rating, label, className }: {
|
|
65
|
+
readonly rating: number;
|
|
66
|
+
readonly label?: string;
|
|
67
|
+
readonly className?: string;
|
|
68
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
69
|
+
/** Text for assistive technology only. */
|
|
70
|
+
export declare function VisuallyHidden({ children }: {
|
|
71
|
+
readonly children: ReactNode;
|
|
72
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
73
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/primitives/layout.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Icon } from '../icons.js';
|
|
3
|
+
import { slot } from '../skin.js';
|
|
4
|
+
export const CONTAINER_WIDTHS = ['default', 'narrow', 'wide'];
|
|
5
|
+
/** One container width shared by the header, the page and the footer (`--bl-container`, `--bl-gutter`). */
|
|
6
|
+
export function Container({ children, layout, classes }) {
|
|
7
|
+
return (_jsx("div", { "data-bl": "container", "data-width": layout?.width ?? 'default', className: classes?.root, children: children }));
|
|
8
|
+
}
|
|
9
|
+
/** The four section tones `SectionSequence` assigns: two alternating grounds, one optional band, one photo ground. */
|
|
10
|
+
export const SECTION_TONES = ['canvas', 'surface', 'band', 'photo'];
|
|
11
|
+
/**
|
|
12
|
+
* The root every section component renders: `<section data-bl-section aria-labelledby>`, its tone and variant, an
|
|
13
|
+
* optional `aria-hidden` decoration and the inner container slot.
|
|
14
|
+
*/
|
|
15
|
+
export function Section({ id, labelledBy, tone, variant, classes, decoration, hooks, phonePlacement, children }) {
|
|
16
|
+
return (_jsxs("section", { ...hooks, "data-bl-section": id, "data-variant": variant, "data-tone": tone, "data-phone-location": phonePlacement, "aria-labelledby": labelledBy, className: classes?.root, children: [decoration ? (_jsx("div", { "data-slot": "decoration", "aria-hidden": "true", children: decoration })) : null, _jsx("div", { ...slot(classes, 'inner'), children: children })] }));
|
|
17
|
+
}
|
|
18
|
+
/** A section's heading block: an optional eyebrow, the H2 and an optional lead paragraph. */
|
|
19
|
+
export function SectionHeading({ id, title, eyebrow, lead, layout, classes }) {
|
|
20
|
+
return (_jsxs("div", { ...slot(classes, 'head'), "data-align": layout?.align ?? 'start', children: [eyebrow ? _jsx("p", { ...slot(classes, 'eyebrow'), children: eyebrow }) : null, _jsx("h2", { id: id, ...slot(classes, 'heading'), children: title }), lead ? _jsx("p", { ...slot(classes, 'lead'), children: lead }) : null] }));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Five stars with one text alternative ("Rated 4.9 out of 5"). The stars use `--bl-color-star`; whole stars only,
|
|
24
|
+
* rounded to the nearest. The rating always comes from the feed, never typed by hand (T4).
|
|
25
|
+
*/
|
|
26
|
+
export function RatingStars({ rating, label, className }) {
|
|
27
|
+
const filled = Math.max(0, Math.min(5, Math.round(rating)));
|
|
28
|
+
return (_jsx("span", { "data-bl": "stars", role: "img", "aria-label": label ?? `Rated ${rating.toFixed(1)} out of 5`, className: className, children: Array.from({ length: 5 }, (_, index) => (_jsx("span", { "data-slot": "star", "data-state": index < filled ? 'on' : 'off', children: _jsx(Icon, { name: "star" }) }, index))) }));
|
|
29
|
+
}
|
|
30
|
+
/** Text for assistive technology only. */
|
|
31
|
+
export function VisuallyHidden({ children }) {
|
|
32
|
+
return _jsx("span", { "data-bl-visually-hidden": "", children: children });
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../../../src/ui-kit/primitives/layout.tsx"],"names":[],"mappings":";AAOA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAClC,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AAEjD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAU,CAAA;AAItE,2GAA2G;AAC3G,MAAM,UAAU,SAAS,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAoD;IACvG,OAAO,CACL,yBAAa,WAAW,gBAAa,MAAM,EAAE,KAAK,IAAI,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,YACtF,QAAQ,GACL,CACP,CAAA;AACH,CAAC;AAED,sHAAsH;AACtH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAU,CAAA;AAyB5E;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAgB;IAC3H,OAAO,CACL,sBACM,KAAK,qBACQ,EAAE,kBACL,OAAO,eACV,IAAI,yBACM,cAAc,qBAClB,UAAU,EAC3B,SAAS,EAAE,OAAO,EAAE,IAAI,aAEvB,UAAU,CAAC,CAAC,CAAC,CACZ,2BAAe,YAAY,iBAAa,MAAM,YAC3C,UAAU,GACP,CACP,CAAC,CAAC,CAAC,IAAI,EACR,iBAAS,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,YAAG,QAAQ,GAAO,IACzC,CACX,CAAA;AACH,CAAC;AAcD,6FAA6F;AAC7F,MAAM,UAAU,cAAc,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAuB;IAC/F,OAAO,CACL,kBAAS,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,gBAAc,MAAM,EAAE,KAAK,IAAI,OAAO,aACjE,OAAO,CAAC,CAAC,CAAC,eAAO,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,YAAG,OAAO,GAAK,CAAC,CAAC,CAAC,IAAI,EAChE,aAAI,EAAE,EAAE,EAAE,KAAM,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,YACrC,KAAK,GACH,EACJ,IAAI,CAAC,CAAC,CAAC,eAAO,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,YAAG,IAAI,GAAK,CAAC,CAAC,CAAC,IAAI,IACnD,CACP,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAqF;IACzI,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAC3D,OAAO,CACL,0BAAc,OAAO,EAAC,IAAI,EAAC,KAAK,gBAAa,KAAK,IAAI,SAAS,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,EAAE,SAAS,EAAE,SAAS,YAC9G,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CACvC,4BAA4B,MAAM,gBAAa,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,YAC1E,KAAC,IAAI,IAAC,IAAI,EAAC,MAAM,GAAG,IADX,KAAK,CAET,CACR,CAAC,GACG,CACR,CAAA;AACH,CAAC;AAED,0CAA0C;AAC1C,MAAM,UAAU,cAAc,CAAC,EAAE,QAAQ,EAAoC;IAC3E,OAAO,0CAA8B,EAAE,YAAE,QAAQ,GAAQ,CAAA;AAC3D,CAAC","sourcesContent":["/**\n * Layout primitives: `Container`, `Section`, `SectionHeading`, `Eyebrow`, `RatingStars` and `VisuallyHidden`.\n * Server components. Heading levels come from the page outline (one H1 in the hero or page header, an H2 per\n * section), never from a look.\n */\nimport type { ReactNode } from 'react'\nimport type { PhoneCallPlacement } from '../../analytics.js'\nimport { Icon } from '../icons.js'\nimport { slot, type SkinEntry } from '../skin.js'\n\nexport const CONTAINER_WIDTHS = ['default', 'narrow', 'wide'] as const\nexport type ContainerWidth = (typeof CONTAINER_WIDTHS)[number]\nexport type ContainerSkin = SkinEntry<'root', never, { width?: ContainerWidth }>\n\n/** One container width shared by the header, the page and the footer (`--bl-container`, `--bl-gutter`). */\nexport function Container({ children, layout, classes }: ContainerSkin & { readonly children: ReactNode }) {\n return (\n <div data-bl=\"container\" data-width={layout?.width ?? 'default'} className={classes?.root}>\n {children}\n </div>\n )\n}\n\n/** The four section tones `SectionSequence` assigns: two alternating grounds, one optional band, one photo ground. */\nexport const SECTION_TONES = ['canvas', 'surface', 'band', 'photo'] as const\nexport type SectionTone = (typeof SECTION_TONES)[number]\n\nexport type SectionSlot = 'root' | 'inner'\n\nexport interface SectionProps {\n /** The matrix id, rendered as `data-bl-section` (for example `reviews`, `cta`, `faqs`). */\n readonly id: string\n /** The id of the section's H2. A section is a `<section>` labelled by its heading. */\n readonly labelledBy: string\n readonly tone?: SectionTone | undefined\n readonly variant?: string | undefined\n readonly classes?: Readonly<Partial<Record<SectionSlot, string>>> | undefined\n /** Signature art, rendered `aria-hidden`. */\n readonly decoration?: ReactNode\n /**\n * Extra layout hooks on the root, for example `{ 'data-columns': '3' }`. They cannot override `data-bl-section`,\n * `data-variant`, `data-tone` or `data-phone-location`.\n */\n readonly hooks?: Readonly<Record<`data-${string}`, string | undefined>>\n /** The placement a `tel:` link inside the section reports when it has none of its own. */\n readonly phonePlacement?: PhoneCallPlacement | undefined\n readonly children: ReactNode\n}\n\n/**\n * The root every section component renders: `<section data-bl-section aria-labelledby>`, its tone and variant, an\n * optional `aria-hidden` decoration and the inner container slot.\n */\nexport function Section({ id, labelledBy, tone, variant, classes, decoration, hooks, phonePlacement, children }: SectionProps) {\n return (\n <section\n {...hooks}\n data-bl-section={id}\n data-variant={variant}\n data-tone={tone}\n data-phone-location={phonePlacement}\n aria-labelledby={labelledBy}\n className={classes?.root}\n >\n {decoration ? (\n <div data-slot=\"decoration\" aria-hidden=\"true\">\n {decoration}\n </div>\n ) : null}\n <div {...slot(classes, 'inner')}>{children}</div>\n </section>\n )\n}\n\nexport type SectionHeadingSlot = 'head' | 'eyebrow' | 'heading' | 'lead'\nexport type SectionHeadingSkin = SkinEntry<SectionHeadingSlot, never, { align?: 'start' | 'center' }>\n\nexport interface SectionHeadingProps extends SectionHeadingSkin {\n /** The H2's id, which the section's `aria-labelledby` points at. */\n readonly id: string\n readonly title: ReactNode\n /** A short label above the H2 (at least 13 px; never the only label for the section). */\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n}\n\n/** A section's heading block: an optional eyebrow, the H2 and an optional lead paragraph. */\nexport function SectionHeading({ id, title, eyebrow, lead, layout, classes }: SectionHeadingProps) {\n return (\n <div {...slot(classes, 'head')} data-align={layout?.align ?? 'start'}>\n {eyebrow ? <p {...slot(classes, 'eyebrow')}>{eyebrow}</p> : null}\n <h2 id={id} {...slot(classes, 'heading')}>\n {title}\n </h2>\n {lead ? <p {...slot(classes, 'lead')}>{lead}</p> : null}\n </div>\n )\n}\n\n/**\n * Five stars with one text alternative (\"Rated 4.9 out of 5\"). The stars use `--bl-color-star`; whole stars only,\n * rounded to the nearest. The rating always comes from the feed, never typed by hand (T4).\n */\nexport function RatingStars({ rating, label, className }: { readonly rating: number; readonly label?: string; readonly className?: string }) {\n const filled = Math.max(0, Math.min(5, Math.round(rating)))\n return (\n <span data-bl=\"stars\" role=\"img\" aria-label={label ?? `Rated ${rating.toFixed(1)} out of 5`} className={className}>\n {Array.from({ length: 5 }, (_, index) => (\n <span key={index} data-slot=\"star\" data-state={index < filled ? 'on' : 'off'}>\n <Icon name=\"star\" />\n </span>\n ))}\n </span>\n )\n}\n\n/** Text for assistive technology only. */\nexport function VisuallyHidden({ children }: { readonly children: ReactNode }) {\n return <span data-bl-visually-hidden=\"\">{children}</span>\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { PhoneCallPlacement } from '../../analytics.js';
|
|
3
|
+
import { type UiIconName } from '../icons.js';
|
|
4
|
+
import { type SkinEntry } from '../skin.js';
|
|
5
|
+
import type { UiPhone } from '../types.js';
|
|
6
|
+
export type ButtonLinkSlot = 'root' | 'label' | 'icon';
|
|
7
|
+
export declare const BUTTON_VARIANTS: readonly ["primary", "secondary", "quiet"];
|
|
8
|
+
export type ButtonVariant = (typeof BUTTON_VARIANTS)[number];
|
|
9
|
+
export type ButtonLinkSkin = SkinEntry<ButtonLinkSlot, ButtonVariant, {
|
|
10
|
+
size?: 'regular' | 'large';
|
|
11
|
+
}>;
|
|
12
|
+
export interface ButtonLinkProps extends ButtonLinkSkin {
|
|
13
|
+
readonly href: string;
|
|
14
|
+
readonly children: ReactNode;
|
|
15
|
+
/** An icon after the label (`arrow`) or before it (`phone`). */
|
|
16
|
+
readonly icon?: UiIconName;
|
|
17
|
+
readonly iconPosition?: 'start' | 'end';
|
|
18
|
+
/** Force external handling; by default `http(s)://` links are external. */
|
|
19
|
+
readonly external?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** A link styled as a button. A primary action is at least 44 px tall (structure.css). */
|
|
22
|
+
export declare function ButtonLink({ href, children, icon, iconPosition, external, variant, layout, classes }: ButtonLinkProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export type TextLinkSlot = 'root';
|
|
24
|
+
export type TextLinkSkin = SkinEntry<TextLinkSlot, 'inline' | 'standalone'>;
|
|
25
|
+
export interface TextLinkProps extends TextLinkSkin {
|
|
26
|
+
readonly href: string;
|
|
27
|
+
readonly children: ReactNode;
|
|
28
|
+
readonly external?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** An inline or standalone text link. */
|
|
31
|
+
export declare function TextLink({ href, children, external, variant, classes }: TextLinkProps): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
export type PhoneLinkSlot = 'root' | 'icon' | 'label' | 'number';
|
|
33
|
+
export type PhoneLinkSkin = SkinEntry<PhoneLinkSlot, 'text' | 'button'>;
|
|
34
|
+
export interface PhoneLinkProps extends PhoneLinkSkin {
|
|
35
|
+
readonly phone: UiPhone;
|
|
36
|
+
/** Where the link sits, for `phone_link_click`. Required: a section sets its own. */
|
|
37
|
+
readonly placement: PhoneCallPlacement;
|
|
38
|
+
/** Optional words before the number, for example "Call". The accessible name is always the visible text. */
|
|
39
|
+
readonly label?: string;
|
|
40
|
+
/** The phone icon on the left (header and hero). */
|
|
41
|
+
readonly showIcon?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A `tel:` link. Its accessible name is its visible text, which always includes the number (WCAG 2.5.3). Use it for
|
|
45
|
+
* every call link: a literal `tel:` anywhere else fails lint (K1) and C10 checks the href against the evidence file.
|
|
46
|
+
*/
|
|
47
|
+
export declare function PhoneLink({ phone, placement, label, showIcon, variant, classes }: PhoneLinkProps): import("react/jsx-runtime").JSX.Element;
|
|
48
|
+
//# sourceMappingURL=links.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/primitives/links.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAwB1C,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;AACtD,eAAO,MAAM,eAAe,4CAA6C,CAAA;AACzE,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAC5D,MAAM,MAAM,cAAc,GAAG,SAAS,CAAC,cAAc,EAAE,aAAa,EAAE;IAAE,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAA;CAAE,CAAC,CAAA;AAErG,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAA;IAC1B,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,GAAG,KAAK,CAAA;IACvC,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAoB,EAAE,QAAQ,EAAE,OAAmB,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,eAAe,2CAazI;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,CAAA;AACjC,MAAM,MAAM,YAAY,GAAG,SAAS,CAAC,YAAY,EAAE,QAAQ,GAAG,YAAY,CAAC,CAAA;AAE3E,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,yCAAyC;AACzC,wBAAgB,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAkB,EAAE,OAAO,EAAE,EAAE,aAAa,2CAMhG;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;AAChE,MAAM,MAAM,aAAa,GAAG,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAA;AAEvE,MAAM,WAAW,cAAe,SAAQ,aAAa;IACnD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,qFAAqF;IACrF,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAA;IACtC,4GAA4G;IAC5G,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,QAAgB,EAAE,OAAgB,EAAE,OAAO,EAAE,EAAE,cAAc,2CAgBjH"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Link primitives: `ButtonLink`, `TextLink` and `PhoneLink`. Server components; every link is a real `<a href>`.
|
|
4
|
+
* External links open in a new tab with `rel="noopener noreferrer"`. `PhoneLink` requires a typed placement, which the
|
|
5
|
+
* shared instrumentation reads to send one `phone_link_click` per tap; nothing here emits events itself.
|
|
6
|
+
*/
|
|
7
|
+
import Link from 'next/link';
|
|
8
|
+
import { Icon } from '../icons.js';
|
|
9
|
+
import { slot } from '../skin.js';
|
|
10
|
+
function isExternal(href) {
|
|
11
|
+
return /^https?:\/\//i.test(href);
|
|
12
|
+
}
|
|
13
|
+
function Anchor({ href, external, hooks, children }) {
|
|
14
|
+
if (external ?? isExternal(href)) {
|
|
15
|
+
return (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", ...hooks, children: children }));
|
|
16
|
+
}
|
|
17
|
+
return (_jsx(Link, { href: href, ...hooks, children: children }));
|
|
18
|
+
}
|
|
19
|
+
export const BUTTON_VARIANTS = ['primary', 'secondary', 'quiet'];
|
|
20
|
+
/** A link styled as a button. A primary action is at least 44 px tall (structure.css). */
|
|
21
|
+
export function ButtonLink({ href, children, icon, iconPosition = 'end', external, variant = 'primary', layout, classes }) {
|
|
22
|
+
const iconNode = icon ? _jsx("span", { ...slot(classes, 'icon'), children: _jsx(Icon, { name: icon }) }) : null;
|
|
23
|
+
return (_jsxs(Anchor, { href: href, external: external, hooks: { 'data-bl': 'button', 'data-variant': variant, 'data-size': layout?.size ?? 'regular', className: classes?.root }, children: [iconPosition === 'start' ? iconNode : null, _jsx("span", { ...slot(classes, 'label'), children: children }), iconPosition === 'end' ? iconNode : null] }));
|
|
24
|
+
}
|
|
25
|
+
/** An inline or standalone text link. */
|
|
26
|
+
export function TextLink({ href, children, external, variant = 'inline', classes }) {
|
|
27
|
+
return (_jsx(Anchor, { href: href, external: external, hooks: { 'data-bl': 'text-link', 'data-variant': variant, className: classes?.root }, children: children }));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A `tel:` link. Its accessible name is its visible text, which always includes the number (WCAG 2.5.3). Use it for
|
|
31
|
+
* every call link: a literal `tel:` anywhere else fails lint (K1) and C10 checks the href against the evidence file.
|
|
32
|
+
*/
|
|
33
|
+
export function PhoneLink({ phone, placement, label, showIcon = false, variant = 'text', classes }) {
|
|
34
|
+
return (_jsxs("a", { href: phone.telHref, "data-bl": "phone-link", "data-variant": variant, "data-phone-location": placement, className: classes?.root, children: [showIcon ? (_jsx("span", { ...slot(classes, 'icon'), children: _jsx(Icon, { name: "phone" }) })) : null, label ? (_jsxs(_Fragment, { children: [_jsx("span", { ...slot(classes, 'label'), children: label }), ' '] })) : null, _jsx("span", { ...slot(classes, 'number'), children: phone.display })] }));
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.js","sourceRoot":"","sources":["../../../src/ui-kit/primitives/links.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * Link primitives: `ButtonLink`, `TextLink` and `PhoneLink`. Server components; every link is a real `<a href>`.\n * External links open in a new tab with `rel=\"noopener noreferrer\"`. `PhoneLink` requires a typed placement, which the\n * shared instrumentation reads to send one `phone_link_click` per tap; nothing here emits events itself.\n */\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\nimport type { PhoneCallPlacement } from '../../analytics.js'\nimport { Icon, type UiIconName } from '../icons.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { UiPhone } from '../types.js'\n\nfunction isExternal(href: string) {\n return /^https?:\\/\\//i.test(href)\n}\n\n/** Data hooks and the skin class for an anchor's root. */\ntype AnchorHooks = { readonly className: string | undefined } & Readonly<Record<`data-${string}`, string | undefined>>\n\nfunction Anchor({ href, external, hooks, children }: { href: string; external?: boolean | undefined; hooks: AnchorHooks; children: ReactNode }) {\n if (external ?? isExternal(href)) {\n return (\n <a href={href} target=\"_blank\" rel=\"noopener noreferrer\" {...hooks}>\n {children}\n </a>\n )\n }\n return (\n <Link href={href} {...hooks}>\n {children}\n </Link>\n )\n}\n\nexport type ButtonLinkSlot = 'root' | 'label' | 'icon'\nexport const BUTTON_VARIANTS = ['primary', 'secondary', 'quiet'] as const\nexport type ButtonVariant = (typeof BUTTON_VARIANTS)[number]\nexport type ButtonLinkSkin = SkinEntry<ButtonLinkSlot, ButtonVariant, { size?: 'regular' | 'large' }>\n\nexport interface ButtonLinkProps extends ButtonLinkSkin {\n readonly href: string\n readonly children: ReactNode\n /** An icon after the label (`arrow`) or before it (`phone`). */\n readonly icon?: UiIconName\n readonly iconPosition?: 'start' | 'end'\n /** Force external handling; by default `http(s)://` links are external. */\n readonly external?: boolean\n}\n\n/** A link styled as a button. A primary action is at least 44 px tall (structure.css). */\nexport function ButtonLink({ href, children, icon, iconPosition = 'end', external, variant = 'primary', layout, classes }: ButtonLinkProps) {\n const iconNode = icon ? <span {...slot(classes, 'icon')}><Icon name={icon} /></span> : null\n return (\n <Anchor\n href={href}\n external={external}\n hooks={{ 'data-bl': 'button', 'data-variant': variant, 'data-size': layout?.size ?? 'regular', className: classes?.root }}\n >\n {iconPosition === 'start' ? iconNode : null}\n <span {...slot(classes, 'label')}>{children}</span>\n {iconPosition === 'end' ? iconNode : null}\n </Anchor>\n )\n}\n\nexport type TextLinkSlot = 'root'\nexport type TextLinkSkin = SkinEntry<TextLinkSlot, 'inline' | 'standalone'>\n\nexport interface TextLinkProps extends TextLinkSkin {\n readonly href: string\n readonly children: ReactNode\n readonly external?: boolean\n}\n\n/** An inline or standalone text link. */\nexport function TextLink({ href, children, external, variant = 'inline', classes }: TextLinkProps) {\n return (\n <Anchor href={href} external={external} hooks={{ 'data-bl': 'text-link', 'data-variant': variant, className: classes?.root }}>\n {children}\n </Anchor>\n )\n}\n\nexport type PhoneLinkSlot = 'root' | 'icon' | 'label' | 'number'\nexport type PhoneLinkSkin = SkinEntry<PhoneLinkSlot, 'text' | 'button'>\n\nexport interface PhoneLinkProps extends PhoneLinkSkin {\n readonly phone: UiPhone\n /** Where the link sits, for `phone_link_click`. Required: a section sets its own. */\n readonly placement: PhoneCallPlacement\n /** Optional words before the number, for example \"Call\". The accessible name is always the visible text. */\n readonly label?: string\n /** The phone icon on the left (header and hero). */\n readonly showIcon?: boolean\n}\n\n/**\n * A `tel:` link. Its accessible name is its visible text, which always includes the number (WCAG 2.5.3). Use it for\n * every call link: a literal `tel:` anywhere else fails lint (K1) and C10 checks the href against the evidence file.\n */\nexport function PhoneLink({ phone, placement, label, showIcon = false, variant = 'text', classes }: PhoneLinkProps) {\n return (\n <a href={phone.telHref} data-bl=\"phone-link\" data-variant={variant} data-phone-location={placement} className={classes?.root}>\n {showIcon ? (\n <span {...slot(classes, 'icon')}>\n <Icon name=\"phone\" />\n </span>\n ) : null}\n {label ? (\n <>\n <span {...slot(classes, 'label')}>{label}</span>{' '}\n </>\n ) : null}\n <span {...slot(classes, 'number')}>{phone.display}</span>\n </a>\n )\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { LeadCtaPlacement } from '../../analytics.js';
|
|
2
|
+
import { type SkinEntry } from '../skin.js';
|
|
3
|
+
import type { ButtonVariant } from './links.js';
|
|
4
|
+
export type QuoteButtonSlot = 'root' | 'label' | 'icon';
|
|
5
|
+
export type QuoteButtonSkin = SkinEntry<QuoteButtonSlot, ButtonVariant, {
|
|
6
|
+
size?: 'regular' | 'large';
|
|
7
|
+
icon?: boolean;
|
|
8
|
+
}>;
|
|
9
|
+
export interface QuoteButtonProps extends QuoteButtonSkin {
|
|
10
|
+
/** The site's recorded quote label, or one from the closed list (C5), for example "Get a free quote". */
|
|
11
|
+
readonly label: string;
|
|
12
|
+
/** The quote form this falls back to without scripts, for example `/contact#quote`. */
|
|
13
|
+
readonly href: string;
|
|
14
|
+
/** Where the button sits, for `lead_cta_click`. */
|
|
15
|
+
readonly placement: LeadCtaPlacement;
|
|
16
|
+
/** A stable id for `lead_cta_click`, for example `quote-header` or `quote-cta`. */
|
|
17
|
+
readonly ctaId: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function QuoteButton({ label, href, placement, ctaId, variant, layout, classes }: QuoteButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=quote-button.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-button.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/primitives/quote-button.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE1D,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;AACvD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAC,eAAe,EAAE,aAAa,EAAE;IAAE,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAEvH,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,yGAAyG;IACzG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAA;IACpC,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAmB,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,gBAAgB,2CAmBpH"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* `QuoteButton`: the quote action. A server-rendered link to the page's quote form (for example `/contact#quote`), so
|
|
4
|
+
* it works before scripts run. When a `QuoteDialog` is on the page (3.10.0) it opens the dialog instead; on the
|
|
5
|
+
* contact page the link scrolls to the form. It carries the `data-bl-cta` hooks only: from 3.9.0 the shared
|
|
6
|
+
* instrumentation reads them and sends `lead_cta_click` when lead diagnostics are on. Nothing is sent in 3.8.0, and
|
|
7
|
+
* nothing here emits events itself.
|
|
8
|
+
*/
|
|
9
|
+
import Link from 'next/link';
|
|
10
|
+
import { Icon } from '../icons.js';
|
|
11
|
+
import { slot } from '../skin.js';
|
|
12
|
+
export function QuoteButton({ label, href, placement, ctaId, variant = 'primary', layout, classes }) {
|
|
13
|
+
return (_jsxs(Link, { href: href, "data-bl": "quote-button", "data-variant": variant, "data-size": layout?.size ?? 'regular', "data-bl-cta": ctaId, "data-bl-cta-placement": placement, className: classes?.root, children: [_jsx("span", { ...slot(classes, 'label'), children: label }), layout?.icon === false ? null : (_jsx("span", { ...slot(classes, 'icon'), children: _jsx(Icon, { name: "arrow" }) }))] }));
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=quote-button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-button.js","sourceRoot":"","sources":["../../../src/ui-kit/primitives/quote-button.tsx"],"names":[],"mappings":";AAAA;;;;;;GAMG;AACH,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAClC,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AAiBjD,MAAM,UAAU,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,EAAE,MAAM,EAAE,OAAO,EAAoB;IACnH,OAAO,CACL,MAAC,IAAI,IACH,IAAI,EAAE,IAAI,aACF,cAAc,kBACR,OAAO,eACV,MAAM,EAAE,IAAI,IAAI,SAAS,iBACvB,KAAK,2BACK,SAAS,EAChC,SAAS,EAAE,OAAO,EAAE,IAAI,aAExB,kBAAU,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,YAAG,KAAK,GAAQ,EAC/C,MAAM,EAAE,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC/B,kBAAU,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,YAC7B,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,GAAG,GAChB,CACR,IACI,CACR,CAAA;AACH,CAAC","sourcesContent":["/**\n * `QuoteButton`: the quote action. A server-rendered link to the page's quote form (for example `/contact#quote`), so\n * it works before scripts run. When a `QuoteDialog` is on the page (3.10.0) it opens the dialog instead; on the\n * contact page the link scrolls to the form. It carries the `data-bl-cta` hooks only: from 3.9.0 the shared\n * instrumentation reads them and sends `lead_cta_click` when lead diagnostics are on. Nothing is sent in 3.8.0, and\n * nothing here emits events itself.\n */\nimport Link from 'next/link'\nimport type { LeadCtaPlacement } from '../../analytics.js'\nimport { Icon } from '../icons.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { ButtonVariant } from './links.js'\n\nexport type QuoteButtonSlot = 'root' | 'label' | 'icon'\nexport type QuoteButtonSkin = SkinEntry<QuoteButtonSlot, ButtonVariant, { size?: 'regular' | 'large'; icon?: boolean }>\n\nexport interface QuoteButtonProps extends QuoteButtonSkin {\n /** The site's recorded quote label, or one from the closed list (C5), for example \"Get a free quote\". */\n readonly label: string\n /** The quote form this falls back to without scripts, for example `/contact#quote`. */\n readonly href: string\n /** Where the button sits, for `lead_cta_click`. */\n readonly placement: LeadCtaPlacement\n /** A stable id for `lead_cta_click`, for example `quote-header` or `quote-cta`. */\n readonly ctaId: string\n}\n\nexport function QuoteButton({ label, href, placement, ctaId, variant = 'primary', layout, classes }: QuoteButtonProps) {\n return (\n <Link\n href={href}\n data-bl=\"quote-button\"\n data-variant={variant}\n data-size={layout?.size ?? 'regular'}\n data-bl-cta={ctaId}\n data-bl-cta-placement={placement}\n className={classes?.root}\n >\n <span {...slot(classes, 'label')}>{label}</span>\n {layout?.icon === false ? null : (\n <span {...slot(classes, 'icon')}>\n <Icon name=\"arrow\" />\n </span>\n )}\n </Link>\n )\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The back-to-top button's client leaf (F7 rules in the hook). */
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
export declare function BackToTopButton({ corner, offset, className, children, }: {
|
|
4
|
+
readonly corner: string;
|
|
5
|
+
readonly offset: string;
|
|
6
|
+
readonly className?: string | undefined;
|
|
7
|
+
readonly children: ReactNode;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=back-to-top-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"back-to-top-client.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/back-to-top-client.tsx"],"names":[],"mappings":"AAEA,mEAAmE;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,MAAM,EACN,SAAS,EACT,QAAQ,GACT,EAAE;IACD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,2CAiBA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useBackToTop } from '../../navigation/use-back-to-top.js';
|
|
4
|
+
export function BackToTopButton({ corner, offset, className, children, }) {
|
|
5
|
+
const { shown, scrollToTop } = useBackToTop();
|
|
6
|
+
return (_jsx("button", { type: "button", "data-bl": "back-to-top", "data-corner": corner, "data-offset": offset, "data-state": shown ? 'shown' : 'hidden', inert: !shown, "aria-label": "Back to top", className: className, onClick: scrollToTop, children: children }));
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=back-to-top-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"back-to-top-client.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/back-to-top-client.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAIZ,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAElE,MAAM,UAAU,eAAe,CAAC,EAC9B,MAAM,EACN,MAAM,EACN,SAAS,EACT,QAAQ,GAMT;IACC,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,YAAY,EAAE,CAAA;IAC7C,OAAO,CACL,iBACE,IAAI,EAAC,QAAQ,aACL,aAAa,iBACR,MAAM,iBACN,MAAM,gBACP,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EACtC,KAAK,EAAE,CAAC,KAAK,gBACF,aAAa,EACxB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,WAAW,YAEnB,QAAQ,GACF,CACV,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\n/** The back-to-top button's client leaf (F7 rules in the hook). */\nimport type { ReactNode } from 'react'\nimport { useBackToTop } from '../../navigation/use-back-to-top.js'\n\nexport function BackToTopButton({\n corner,\n offset,\n className,\n children,\n}: {\n readonly corner: string\n readonly offset: string\n readonly className?: string | undefined\n readonly children: ReactNode\n}) {\n const { shown, scrollToTop } = useBackToTop()\n return (\n <button\n type=\"button\"\n data-bl=\"back-to-top\"\n data-corner={corner}\n data-offset={offset}\n data-state={shown ? 'shown' : 'hidden'}\n inert={!shown}\n aria-label=\"Back to top\"\n className={className}\n onClick={scrollToTop}\n >\n {children}\n </button>\n )\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SkinEntry } from '../skin.js';
|
|
2
|
+
export type BackToTopSlot = 'root' | 'icon';
|
|
3
|
+
export declare const BACK_TO_TOP_OFFSETS: readonly ["small", "medium", "large"];
|
|
4
|
+
export interface BackToTopLayout {
|
|
5
|
+
/** The bottom corner it sits in (default `end`). */
|
|
6
|
+
readonly corner?: 'end' | 'start';
|
|
7
|
+
/** Its distance from the side and from the bottom (or the call bar): 0.75, 1.25 or 2 rem (default `medium`). */
|
|
8
|
+
readonly offset?: (typeof BACK_TO_TOP_OFFSETS)[number];
|
|
9
|
+
}
|
|
10
|
+
export type BackToTopSkin = SkinEntry<BackToTopSlot, never, BackToTopLayout>;
|
|
11
|
+
export type BackToTopProps = BackToTopSkin;
|
|
12
|
+
export declare function BackToTop({ layout, classes }: BackToTopProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=back-to-top.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"back-to-top.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/back-to-top.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAGjD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAA;AAC3C,eAAO,MAAM,mBAAmB,uCAAwC,CAAA;AACxE,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,OAAO,CAAA;IACjC,gHAAgH;IAChH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;CACvD;AACD,MAAM,MAAM,aAAa,GAAG,SAAS,CAAC,aAAa,EAAE,KAAK,EAAE,eAAe,CAAC,CAAA;AAE5E,MAAM,MAAM,cAAc,GAAG,aAAa,CAAA;AAE1C,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,cAAc,2CAQ5D"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* F7: back-to-top (server component). A 44 px `<button>` named "Back to top" that appears after one and a half
|
|
4
|
+
* screens, sits above the call bar on phones, scrolls smoothly unless the visitor prefers reduced motion and moves
|
|
5
|
+
* focus to `#main`. Never a footer text link. Render it once, after the page's `<main>`.
|
|
6
|
+
*/
|
|
7
|
+
import { Icon } from '../icons.js';
|
|
8
|
+
import { slot } from '../skin.js';
|
|
9
|
+
import { BackToTopButton } from './back-to-top-client.js';
|
|
10
|
+
export const BACK_TO_TOP_OFFSETS = ['small', 'medium', 'large'];
|
|
11
|
+
export function BackToTop({ layout, classes }) {
|
|
12
|
+
return (_jsx(BackToTopButton, { corner: layout?.corner ?? 'end', offset: layout?.offset ?? 'medium', className: classes?.root, children: _jsx("span", { ...slot(classes, 'icon'), children: _jsx(Icon, { name: "arrow-up" }) }) }));
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=back-to-top.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"back-to-top.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/back-to-top.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAClC,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAGzD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAU,CAAA;AAWxE,MAAM,UAAU,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAkB;IAC3D,OAAO,CACL,KAAC,eAAe,IAAC,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,YAC5G,kBAAU,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,YAC7B,KAAC,IAAI,IAAC,IAAI,EAAC,UAAU,GAAG,GACnB,GACS,CACnB,CAAA;AACH,CAAC","sourcesContent":["/**\n * F7: back-to-top (server component). A 44 px `<button>` named \"Back to top\" that appears after one and a half\n * screens, sits above the call bar on phones, scrolls smoothly unless the visitor prefers reduced motion and moves\n * focus to `#main`. Never a footer text link. Render it once, after the page's `<main>`.\n */\nimport { Icon } from '../icons.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport { BackToTopButton } from './back-to-top-client.js'\n\nexport type BackToTopSlot = 'root' | 'icon'\nexport const BACK_TO_TOP_OFFSETS = ['small', 'medium', 'large'] as const\nexport interface BackToTopLayout {\n /** The bottom corner it sits in (default `end`). */\n readonly corner?: 'end' | 'start'\n /** Its distance from the side and from the bottom (or the call bar): 0.75, 1.25 or 2 rem (default `medium`). */\n readonly offset?: (typeof BACK_TO_TOP_OFFSETS)[number]\n}\nexport type BackToTopSkin = SkinEntry<BackToTopSlot, never, BackToTopLayout>\n\nexport type BackToTopProps = BackToTopSkin\n\nexport function BackToTop({ layout, classes }: BackToTopProps) {\n return (\n <BackToTopButton corner={layout?.corner ?? 'end'} offset={layout?.offset ?? 'medium'} className={classes?.root}>\n <span {...slot(classes, 'icon')}>\n <Icon name=\"arrow-up\" />\n </span>\n </BackToTopButton>\n )\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export declare function CallBarController({ variant, className, children }: {
|
|
3
|
+
readonly variant: string;
|
|
4
|
+
readonly className?: string | undefined;
|
|
5
|
+
readonly children: ReactNode;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=call-bar-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"call-bar-client.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/call-bar-client.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAG9C,wBAAgB,iBAAiB,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAStK"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/** The call bar's client leaf: shows and hides the server-rendered Call and quote buttons (F6 rules in the hook). */
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { useRef } from 'react';
|
|
6
|
+
import { useStickyCallBar } from '../../navigation/use-sticky-call-bar.js';
|
|
7
|
+
export function CallBarController({ variant, className, children }) {
|
|
8
|
+
const ref = useRef(null);
|
|
9
|
+
const pathname = usePathname();
|
|
10
|
+
const shown = useStickyCallBar(ref, pathname);
|
|
11
|
+
return (_jsx("div", { ref: ref, "data-bl": "callbar", "data-variant": variant, "data-state": shown ? 'shown' : 'hidden', inert: !shown, className: className, children: children }));
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=call-bar-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"call-bar-client.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/call-bar-client.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,qHAAqH;AACrH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAkB,MAAM,OAAO,CAAA;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAA;AAE1E,MAAM,UAAU,iBAAiB,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAuG;IACrK,MAAM,GAAG,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAA;IACxC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IAC7C,OAAO,CACL,cAAK,GAAG,EAAE,GAAG,aAAU,SAAS,kBAAe,OAAO,gBAAc,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,YAChI,QAAQ,GACL,CACP,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\n/** The call bar's client leaf: shows and hides the server-rendered Call and quote buttons (F6 rules in the hook). */\nimport { usePathname } from 'next/navigation'\nimport { useRef, type ReactNode } from 'react'\nimport { useStickyCallBar } from '../../navigation/use-sticky-call-bar.js'\n\nexport function CallBarController({ variant, className, children }: { readonly variant: string; readonly className?: string | undefined; readonly children: ReactNode }) {\n const ref = useRef<HTMLDivElement>(null)\n const pathname = usePathname()\n const shown = useStickyCallBar(ref, pathname)\n return (\n <div ref={ref} data-bl=\"callbar\" data-variant={variant} data-state={shown ? 'shown' : 'hidden'} inert={!shown} className={className}>\n {children}\n </div>\n )\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/** True when `href` is the current page. External links are never current. */
|
|
3
|
+
export declare function isCurrentPage(pathname: string, href: string): boolean;
|
|
4
|
+
/** True when the page is one of `paths` or below it (the home path matches only itself). */
|
|
5
|
+
export declare function isCurrentSection(pathname: string, paths: readonly string[]): boolean;
|
|
6
|
+
export declare function CurrentLink({ href, slot, hub, prefetch, className, children, }: {
|
|
7
|
+
readonly href: string;
|
|
8
|
+
readonly slot: string;
|
|
9
|
+
readonly hub?: boolean;
|
|
10
|
+
/** False for links in panels and the drawer: hidden panels count as in view and would prefetch every menu page. */
|
|
11
|
+
readonly prefetch?: boolean | undefined;
|
|
12
|
+
readonly className?: string | undefined;
|
|
13
|
+
readonly children: ReactNode;
|
|
14
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=current-link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"current-link.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/current-link.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAOtC,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,WAG3D;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,WAO1E;AAED,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,IAAI,EACJ,GAAW,EACX,QAAQ,EACR,SAAS,EACT,QAAQ,GACT,EAAE;IACD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;IACtB,mHAAmH;IACnH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,2CAcA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* A menu link that marks itself `aria-current="page"` on its own page. A client leaf only because the root layout
|
|
5
|
+
* can't read the route: it receives one href and its server-rendered label, and renders in the server HTML with the
|
|
6
|
+
* right state.
|
|
7
|
+
*/
|
|
8
|
+
import Link from 'next/link';
|
|
9
|
+
import { usePathname } from 'next/navigation';
|
|
10
|
+
function normalisePath(path) {
|
|
11
|
+
const bare = path.split(/[?#]/, 1)[0] ?? '';
|
|
12
|
+
return bare.replace(/\/+$/, '') || '/';
|
|
13
|
+
}
|
|
14
|
+
/** True when `href` is the current page. External links are never current. */
|
|
15
|
+
export function isCurrentPage(pathname, href) {
|
|
16
|
+
if (!href.startsWith('/') || href.startsWith('//'))
|
|
17
|
+
return false;
|
|
18
|
+
return normalisePath(pathname) === normalisePath(href);
|
|
19
|
+
}
|
|
20
|
+
/** True when the page is one of `paths` or below it (the home path matches only itself). */
|
|
21
|
+
export function isCurrentSection(pathname, paths) {
|
|
22
|
+
const current = normalisePath(pathname);
|
|
23
|
+
return paths.some((path) => {
|
|
24
|
+
if (!path.startsWith('/') || path.startsWith('//'))
|
|
25
|
+
return false;
|
|
26
|
+
const section = normalisePath(path);
|
|
27
|
+
return current === section || (section !== '/' && current.startsWith(`${section}/`));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function CurrentLink({ href, slot, hub = false, prefetch, className, children, }) {
|
|
31
|
+
const pathname = usePathname();
|
|
32
|
+
return (_jsx(Link, { href: href, prefetch: prefetch, "data-slot": slot, "data-hub": hub ? 'true' : undefined, "aria-current": isCurrentPage(pathname, href) ? 'page' : undefined, className: className, children: children }));
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=current-link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"current-link.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/current-link.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ;;;;GAIG;AACH,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAG7C,SAAS,aAAa,CAAC,IAAY;IACjC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IAC3C,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,CAAA;AACxC,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,IAAY;IAC1D,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAA;IAChE,OAAO,aAAa,CAAC,QAAQ,CAAC,KAAK,aAAa,CAAC,IAAI,CAAC,CAAA;AACxD,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,gBAAgB,CAAC,QAAgB,EAAE,KAAwB;IACzE,MAAM,OAAO,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAA;IACvC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QACzB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAA;QAChE,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;QACnC,OAAO,OAAO,KAAK,OAAO,IAAI,CAAC,OAAO,KAAK,GAAG,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,OAAO,GAAG,CAAC,CAAC,CAAA;IACtF,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAC1B,IAAI,EACJ,IAAI,EACJ,GAAG,GAAG,KAAK,EACX,QAAQ,EACR,SAAS,EACT,QAAQ,GAST;IACC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,OAAO,CACL,KAAC,IAAI,IACH,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,eACP,IAAI,cACL,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,kBACpB,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAChE,SAAS,EAAE,SAAS,YAEnB,QAAQ,GACJ,CACR,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\n/**\n * A menu link that marks itself `aria-current=\"page\"` on its own page. A client leaf only because the root layout\n * can't read the route: it receives one href and its server-rendered label, and renders in the server HTML with the\n * right state.\n */\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport type { ReactNode } from 'react'\n\nfunction normalisePath(path: string) {\n const bare = path.split(/[?#]/, 1)[0] ?? ''\n return bare.replace(/\\/+$/, '') || '/'\n}\n\n/** True when `href` is the current page. External links are never current. */\nexport function isCurrentPage(pathname: string, href: string) {\n if (!href.startsWith('/') || href.startsWith('//')) return false\n return normalisePath(pathname) === normalisePath(href)\n}\n\n/** True when the page is one of `paths` or below it (the home path matches only itself). */\nexport function isCurrentSection(pathname: string, paths: readonly string[]) {\n const current = normalisePath(pathname)\n return paths.some((path) => {\n if (!path.startsWith('/') || path.startsWith('//')) return false\n const section = normalisePath(path)\n return current === section || (section !== '/' && current.startsWith(`${section}/`))\n })\n}\n\nexport function CurrentLink({\n href,\n slot,\n hub = false,\n prefetch,\n className,\n children,\n}: {\n readonly href: string\n readonly slot: string\n readonly hub?: boolean\n /** False for links in panels and the drawer: hidden panels count as in view and would prefetch every menu page. */\n readonly prefetch?: boolean | undefined\n readonly className?: string | undefined\n readonly children: ReactNode\n}) {\n const pathname = usePathname()\n return (\n <Link\n href={href}\n prefetch={prefetch}\n data-slot={slot}\n data-hub={hub ? 'true' : undefined}\n aria-current={isCurrentPage(pathname, href) ? 'page' : undefined}\n className={className}\n >\n {children}\n </Link>\n )\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface DrawerControllerClasses {
|
|
3
|
+
readonly root?: string | undefined;
|
|
4
|
+
readonly menuButton?: string | undefined;
|
|
5
|
+
readonly menuLabel?: string | undefined;
|
|
6
|
+
readonly panel?: string | undefined;
|
|
7
|
+
readonly top?: string | undefined;
|
|
8
|
+
readonly close?: string | undefined;
|
|
9
|
+
readonly title?: string | undefined;
|
|
10
|
+
readonly logo?: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
export declare function DrawerController({ id, variant, side, classes, logo, menuIcon, closeIcon, children, }: {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly variant: string;
|
|
15
|
+
readonly side: string;
|
|
16
|
+
readonly classes: DrawerControllerClasses;
|
|
17
|
+
readonly logo?: ReactNode;
|
|
18
|
+
readonly menuIcon: ReactNode;
|
|
19
|
+
readonly closeIcon: ReactNode;
|
|
20
|
+
readonly children: ReactNode;
|
|
21
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
//# sourceMappingURL=drawer-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawer-client.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/drawer-client.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAwD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAG5F,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACnC;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,EAAE,EACF,OAAO,EACP,IAAI,EACJ,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,QAAQ,GACT,EAAE;IACD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,uBAAuB,CAAA;IACzC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;IAC5B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,2CAkGA"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The drawer's client leaf: the Menu button and the modal `<dialog>` (top layer, the page inert, focus kept inside).
|
|
5
|
+
* The menu, the actions and the social links are server-rendered children. The drawer closes on Escape, the close
|
|
6
|
+
* button, a tap on the backdrop, a link click, a route change and any change of the header's mode (crossing 1024 or
|
|
7
|
+
* 1280 px), so the page is never left inert. Groups are `<details name>` (one open at a time, no script needed); the
|
|
8
|
+
* toggle listener below keeps that rule in browsers without exclusive `<details>`.
|
|
9
|
+
*/
|
|
10
|
+
import { usePathname } from 'next/navigation';
|
|
11
|
+
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
12
|
+
import { HeaderModeContext } from './shell-context.js';
|
|
13
|
+
export function DrawerController({ id, variant, side, classes, logo, menuIcon, closeIcon, children, }) {
|
|
14
|
+
const pathname = usePathname();
|
|
15
|
+
const mode = useContext(HeaderModeContext);
|
|
16
|
+
const dialogRef = useRef(null);
|
|
17
|
+
const buttonRef = useRef(null);
|
|
18
|
+
const closeRef = useRef(null);
|
|
19
|
+
const [open, setOpen] = useState(false);
|
|
20
|
+
const titleId = `${id}-title`;
|
|
21
|
+
const close = useCallback(() => {
|
|
22
|
+
const dialog = dialogRef.current;
|
|
23
|
+
if (dialog?.open)
|
|
24
|
+
dialog.close();
|
|
25
|
+
}, []);
|
|
26
|
+
const show = () => {
|
|
27
|
+
const dialog = dialogRef.current;
|
|
28
|
+
if (!dialog || dialog.open)
|
|
29
|
+
return;
|
|
30
|
+
for (const group of dialog.querySelectorAll('details[open]'))
|
|
31
|
+
group.open = false;
|
|
32
|
+
dialog.showModal();
|
|
33
|
+
// The close button comes first in focus order and takes focus when the drawer opens.
|
|
34
|
+
closeRef.current?.focus();
|
|
35
|
+
setOpen(true);
|
|
36
|
+
};
|
|
37
|
+
// A route change or a mode change (breakpoint crossing) resets the drawer.
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
close();
|
|
40
|
+
}, [pathname, mode, close]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const dialog = dialogRef.current;
|
|
43
|
+
if (!dialog)
|
|
44
|
+
return;
|
|
45
|
+
const onToggle = (event) => {
|
|
46
|
+
const opened = event.target;
|
|
47
|
+
if (!(opened instanceof HTMLDetailsElement) || !opened.open)
|
|
48
|
+
return;
|
|
49
|
+
for (const other of dialog.querySelectorAll('details[open]'))
|
|
50
|
+
if (other !== opened)
|
|
51
|
+
other.open = false;
|
|
52
|
+
};
|
|
53
|
+
dialog.addEventListener('toggle', onToggle, true);
|
|
54
|
+
return () => dialog.removeEventListener('toggle', onToggle, true);
|
|
55
|
+
}, []);
|
|
56
|
+
return (_jsxs(_Fragment, { children: [_jsxs("button", { ref: buttonRef, type: "button", "data-slot": "menuButton", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": id, className: classes.menuButton, onClick: show, children: [menuIcon, _jsx("span", { "data-slot": "menuLabel", className: classes.menuLabel, children: "Menu" })] }), _jsx("dialog", { ref: dialogRef, id: id, "data-bl": "drawer", "data-variant": variant, "data-side": side, "aria-labelledby": titleId, className: classes.root, ...{ closedby: 'any' }, onClose: () => {
|
|
57
|
+
setOpen(false);
|
|
58
|
+
// Safari doesn't focus a clicked button, so the dialog may have nothing to return focus to.
|
|
59
|
+
const active = document.activeElement;
|
|
60
|
+
if (!active || active === document.body || dialogRef.current?.contains(active))
|
|
61
|
+
buttonRef.current?.focus();
|
|
62
|
+
}, onClick: (event) => {
|
|
63
|
+
// A tap on the backdrop (where closedby="any" isn't supported yet) or on any link closes the drawer.
|
|
64
|
+
if (event.target === dialogRef.current)
|
|
65
|
+
close();
|
|
66
|
+
else if (event.target.closest('a[href]'))
|
|
67
|
+
close();
|
|
68
|
+
}, children: _jsxs("div", { "data-slot": "panel", className: classes.panel, children: [_jsxs("div", { "data-slot": "top", className: classes.top, children: [_jsx("button", { ref: closeRef, type: "button", "data-slot": "close", "aria-label": "Close menu", className: classes.close, onClick: close, children: closeIcon }), _jsx("p", { id: titleId, "data-slot": "title", className: classes.title, children: "Menu" }), logo ? (_jsx("span", { "data-slot": "logo", "aria-hidden": "true", className: classes.logo, children: logo })) : null] }), children] }) })] }));
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=drawer-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawer-client.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/drawer-client.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\n/**\n * The drawer's client leaf: the Menu button and the modal `<dialog>` (top layer, the page inert, focus kept inside).\n * The menu, the actions and the social links are server-rendered children. The drawer closes on Escape, the close\n * button, a tap on the backdrop, a link click, a route change and any change of the header's mode (crossing 1024 or\n * 1280 px), so the page is never left inert. Groups are `<details name>` (one open at a time, no script needed); the\n * toggle listener below keeps that rule in browsers without exclusive `<details>`.\n */\nimport { usePathname } from 'next/navigation'\nimport { useCallback, useContext, useEffect, useRef, useState, type ReactNode } from 'react'\nimport { HeaderModeContext } from './shell-context.js'\n\nexport interface DrawerControllerClasses {\n readonly root?: string | undefined\n readonly menuButton?: string | undefined\n readonly menuLabel?: string | undefined\n readonly panel?: string | undefined\n readonly top?: string | undefined\n readonly close?: string | undefined\n readonly title?: string | undefined\n readonly logo?: string | undefined\n}\n\nexport function DrawerController({\n id,\n variant,\n side,\n classes,\n logo,\n menuIcon,\n closeIcon,\n children,\n}: {\n readonly id: string\n readonly variant: string\n readonly side: string\n readonly classes: DrawerControllerClasses\n readonly logo?: ReactNode\n readonly menuIcon: ReactNode\n readonly closeIcon: ReactNode\n readonly children: ReactNode\n}) {\n const pathname = usePathname()\n const mode = useContext(HeaderModeContext)\n const dialogRef = useRef<HTMLDialogElement>(null)\n const buttonRef = useRef<HTMLButtonElement>(null)\n const closeRef = useRef<HTMLButtonElement>(null)\n const [open, setOpen] = useState(false)\n const titleId = `${id}-title`\n\n const close = useCallback(() => {\n const dialog = dialogRef.current\n if (dialog?.open) dialog.close()\n }, [])\n\n const show = () => {\n const dialog = dialogRef.current\n if (!dialog || dialog.open) return\n for (const group of dialog.querySelectorAll<HTMLDetailsElement>('details[open]')) group.open = false\n dialog.showModal()\n // The close button comes first in focus order and takes focus when the drawer opens.\n closeRef.current?.focus()\n setOpen(true)\n }\n\n // A route change or a mode change (breakpoint crossing) resets the drawer.\n useEffect(() => {\n close()\n }, [pathname, mode, close])\n\n useEffect(() => {\n const dialog = dialogRef.current\n if (!dialog) return\n const onToggle = (event: Event) => {\n const opened = event.target\n if (!(opened instanceof HTMLDetailsElement) || !opened.open) return\n for (const other of dialog.querySelectorAll<HTMLDetailsElement>('details[open]')) if (other !== opened) other.open = false\n }\n dialog.addEventListener('toggle', onToggle, true)\n return () => dialog.removeEventListener('toggle', onToggle, true)\n }, [])\n\n return (\n <>\n <button\n ref={buttonRef}\n type=\"button\"\n data-slot=\"menuButton\"\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n aria-controls={id}\n className={classes.menuButton}\n onClick={show}\n >\n {menuIcon}\n <span data-slot=\"menuLabel\" className={classes.menuLabel}>\n Menu\n </span>\n </button>\n <dialog\n ref={dialogRef}\n id={id}\n data-bl=\"drawer\"\n data-variant={variant}\n data-side={side}\n aria-labelledby={titleId}\n className={classes.root}\n {...({ closedby: 'any' } as Record<string, string>)}\n onClose={() => {\n setOpen(false)\n // Safari doesn't focus a clicked button, so the dialog may have nothing to return focus to.\n const active = document.activeElement\n if (!active || active === document.body || dialogRef.current?.contains(active)) buttonRef.current?.focus()\n }}\n onClick={(event) => {\n // A tap on the backdrop (where closedby=\"any\" isn't supported yet) or on any link closes the drawer.\n if (event.target === dialogRef.current) close()\n else if ((event.target as Element).closest('a[href]')) close()\n }}\n >\n <div data-slot=\"panel\" className={classes.panel}>\n <div data-slot=\"top\" className={classes.top}>\n <button ref={closeRef} type=\"button\" data-slot=\"close\" aria-label=\"Close menu\" className={classes.close} onClick={close}>\n {closeIcon}\n </button>\n <p id={titleId} data-slot=\"title\" className={classes.title}>\n Menu\n </p>\n {logo ? (\n <span data-slot=\"logo\" aria-hidden=\"true\" className={classes.logo}>\n {logo}\n </span>\n ) : null}\n </div>\n {children}\n </div>\n </dialog>\n </>\n )\n}\n"]}
|