@tonycodes-ui/mortar 0.11.0 → 0.12.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/AGENTS.md +1 -1
- package/CHANGELOG.md +17 -25
- package/CONTRIBUTING.md +14 -2
- package/README.md +1 -1
- package/catalog.json +553 -356
- package/dist/agent-data.json +1 -1
- package/dist/illustration.d.ts +47 -0
- package/dist/illustration.js +47 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/internal.d.ts +24 -2
- package/dist/internal.js +62 -3
- package/dist/marketing.d.ts +30 -7
- package/dist/marketing.js +21 -8
- package/package.json +1 -1
- package/src/illustration.tsx +169 -0
- package/src/index.ts +3 -0
- package/src/internal.tsx +87 -1
- package/src/marketing.css +181 -2
- package/src/marketing.tsx +99 -20
package/src/internal.tsx
CHANGED
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
createContext,
|
|
6
6
|
createElement,
|
|
7
7
|
useContext,
|
|
8
|
+
useEffect,
|
|
8
9
|
useId,
|
|
10
|
+
useState,
|
|
9
11
|
type HTMLAttributes,
|
|
12
|
+
type RefObject,
|
|
10
13
|
type ReactNode,
|
|
11
14
|
cloneElement,
|
|
12
15
|
isValidElement,
|
|
@@ -71,6 +74,8 @@ export interface IntroProps {
|
|
|
71
74
|
eyebrow?: string;
|
|
72
75
|
/** Omit to render no heading. */
|
|
73
76
|
title?: string;
|
|
77
|
+
/** A phrase in `title` to emphasise; its first occurrence renders as `<em>`. */
|
|
78
|
+
emphasis?: string;
|
|
74
79
|
description?: string;
|
|
75
80
|
/** Id for the heading, e.g. the target of the section's aria-labelledby. */
|
|
76
81
|
id?: string;
|
|
@@ -87,6 +92,7 @@ export interface IntroProps {
|
|
|
87
92
|
export function introSlots({
|
|
88
93
|
eyebrow,
|
|
89
94
|
title,
|
|
95
|
+
emphasis,
|
|
90
96
|
description,
|
|
91
97
|
id,
|
|
92
98
|
headingLevel = 2,
|
|
@@ -95,12 +101,26 @@ export function introSlots({
|
|
|
95
101
|
const Heading = headingTag(headingLevel);
|
|
96
102
|
return [
|
|
97
103
|
eyebrow && <p className="m-eyebrow">{eyebrow}</p>,
|
|
98
|
-
title !== undefined &&
|
|
104
|
+
title !== undefined && (
|
|
105
|
+
<Heading id={id}>{emphasise(title, emphasis)}</Heading>
|
|
106
|
+
),
|
|
99
107
|
description && (
|
|
100
108
|
<p className={descriptionClassName ?? undefined}>{description}</p>
|
|
101
109
|
),
|
|
102
110
|
];
|
|
103
111
|
}
|
|
112
|
+
/** `title` with the first occurrence of `phrase` in `<em>`; unchanged when absent. */
|
|
113
|
+
function emphasise(title: string, phrase?: string): ReactNode {
|
|
114
|
+
const at = phrase ? title.indexOf(phrase) : -1;
|
|
115
|
+
if (!phrase || at < 0) return title;
|
|
116
|
+
return (
|
|
117
|
+
<>
|
|
118
|
+
{title.slice(0, at)}
|
|
119
|
+
<em className="m-emphasis">{phrase}</em>
|
|
120
|
+
{title.slice(at + phrase.length)}
|
|
121
|
+
</>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
104
124
|
/** The section intro as one element, for parents where nothing user-supplied follows it. */
|
|
105
125
|
export function SectionIntro(props: IntroProps) {
|
|
106
126
|
return <>{...introSlots(props)}</>;
|
|
@@ -210,6 +230,72 @@ export function useDomId(prefix: string) {
|
|
|
210
230
|
return prefix + useId().replace(/[^a-zA-Z0-9-]/g, "");
|
|
211
231
|
}
|
|
212
232
|
|
|
233
|
+
/** Why an animation is not running; null while it plays. */
|
|
234
|
+
export type PauseReason = "user" | "motion" | "hidden" | "offscreen" | null;
|
|
235
|
+
/**
|
|
236
|
+
* Shared pause and resume for decorative motion. It plays only while the
|
|
237
|
+
* user has not paused it, reduced motion is not requested, the document is
|
|
238
|
+
* visible and `ref` is on screen. The user's pause is kept apart from the
|
|
239
|
+
* automatic reasons, so returning to the page never overrides it. `paused`
|
|
240
|
+
* and `onPausedChange` make the user's choice controlled, for example one
|
|
241
|
+
* state shared by several animations. Every listener is removed on unmount.
|
|
242
|
+
*/
|
|
243
|
+
export function usePlayback(
|
|
244
|
+
ref: RefObject<Element | null>,
|
|
245
|
+
{
|
|
246
|
+
paused,
|
|
247
|
+
onPausedChange,
|
|
248
|
+
enabled = true,
|
|
249
|
+
}: {
|
|
250
|
+
paused?: boolean;
|
|
251
|
+
onPausedChange?: (paused: boolean) => void;
|
|
252
|
+
enabled?: boolean;
|
|
253
|
+
},
|
|
254
|
+
) {
|
|
255
|
+
const [ownPaused, setOwnPaused] = useState(false);
|
|
256
|
+
const [reduced, setReduced] = useState(false);
|
|
257
|
+
const [hidden, setHidden] = useState(false);
|
|
258
|
+
const [offscreen, setOffscreen] = useState(false);
|
|
259
|
+
const userPaused = paused ?? ownPaused;
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
if (!enabled) return;
|
|
262
|
+
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
263
|
+
const onMedia = () => setReduced(media.matches);
|
|
264
|
+
const onVisibility = () => setHidden(document.visibilityState === "hidden");
|
|
265
|
+
onMedia();
|
|
266
|
+
onVisibility();
|
|
267
|
+
media.addEventListener("change", onMedia);
|
|
268
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
269
|
+
const observer =
|
|
270
|
+
ref.current && typeof IntersectionObserver !== "undefined"
|
|
271
|
+
? new IntersectionObserver(([entry]) =>
|
|
272
|
+
setOffscreen(!entry.isIntersecting),
|
|
273
|
+
)
|
|
274
|
+
: null;
|
|
275
|
+
if (observer && ref.current) observer.observe(ref.current);
|
|
276
|
+
return () => {
|
|
277
|
+
media.removeEventListener("change", onMedia);
|
|
278
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
279
|
+
observer?.disconnect();
|
|
280
|
+
};
|
|
281
|
+
}, [enabled, ref]);
|
|
282
|
+
const reason: PauseReason = userPaused
|
|
283
|
+
? "user"
|
|
284
|
+
: reduced
|
|
285
|
+
? "motion"
|
|
286
|
+
: hidden
|
|
287
|
+
? "hidden"
|
|
288
|
+
: offscreen
|
|
289
|
+
? "offscreen"
|
|
290
|
+
: null;
|
|
291
|
+
const toggle = () => {
|
|
292
|
+
const next = !userPaused;
|
|
293
|
+
if (paused === undefined) setOwnPaused(next);
|
|
294
|
+
onPausedChange?.(next);
|
|
295
|
+
};
|
|
296
|
+
return { reason, reduced, userPaused, toggle };
|
|
297
|
+
}
|
|
298
|
+
|
|
213
299
|
/**
|
|
214
300
|
* Next index for arrow, Home and End keys in a horizontal set (wrapping), or
|
|
215
301
|
* null for any other key.
|
package/src/marketing.css
CHANGED
|
@@ -24,6 +24,21 @@
|
|
|
24
24
|
font-weight: var(--m-weight-bold);
|
|
25
25
|
letter-spacing: var(--m-tracking-heading);
|
|
26
26
|
}
|
|
27
|
+
/* A short descriptor beside the wordmark, outside the home link. */
|
|
28
|
+
.m-brand-lockup {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 12px;
|
|
32
|
+
min-width: 0;
|
|
33
|
+
}
|
|
34
|
+
.m-brand-tagline {
|
|
35
|
+
max-width: 28ch;
|
|
36
|
+
padding-left: 12px;
|
|
37
|
+
border-left: 1px solid var(--m-line);
|
|
38
|
+
color: var(--m-muted);
|
|
39
|
+
font-size: 13px;
|
|
40
|
+
line-height: 1.3;
|
|
41
|
+
}
|
|
27
42
|
.m-marketing-header nav {
|
|
28
43
|
display: flex;
|
|
29
44
|
gap: 22px;
|
|
@@ -192,7 +207,8 @@
|
|
|
192
207
|
grid-column: 1;
|
|
193
208
|
grid-row: 1;
|
|
194
209
|
}
|
|
195
|
-
.m-marketing-header[data-layout="center"] > .m-brand
|
|
210
|
+
.m-marketing-header[data-layout="center"] > .m-brand,
|
|
211
|
+
.m-marketing-header[data-layout="center"] > .m-brand-lockup {
|
|
196
212
|
grid-column: 2;
|
|
197
213
|
grid-row: 1;
|
|
198
214
|
}
|
|
@@ -253,7 +269,8 @@
|
|
|
253
269
|
.m-marketing-header[data-layout="center"] {
|
|
254
270
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
255
271
|
}
|
|
256
|
-
.m-marketing-header[data-layout="center"] > .m-brand
|
|
272
|
+
.m-marketing-header[data-layout="center"] > .m-brand,
|
|
273
|
+
.m-marketing-header[data-layout="center"] > .m-brand-lockup {
|
|
257
274
|
justify-self: center;
|
|
258
275
|
}
|
|
259
276
|
.m-marketing-header[data-layout="center"] > .m-actions .m-button {
|
|
@@ -274,6 +291,32 @@
|
|
|
274
291
|
.m-flyout-panel[data-layout="columns"] ul {
|
|
275
292
|
grid-template-columns: minmax(0, 1fr);
|
|
276
293
|
}
|
|
294
|
+
.m-brand-tagline {
|
|
295
|
+
display: none;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* == MarketingPage == */
|
|
300
|
+
/* Header, main and footer fill the viewport; main grows, nothing is clipped. */
|
|
301
|
+
.m-marketing-page {
|
|
302
|
+
display: flex;
|
|
303
|
+
flex-direction: column;
|
|
304
|
+
min-height: 100vh;
|
|
305
|
+
min-height: 100dvh;
|
|
306
|
+
}
|
|
307
|
+
/* Header, sections and footer centre themselves with auto margins, which in a
|
|
308
|
+
flex column would shrink them to their content; keep them full width. */
|
|
309
|
+
.m-marketing-page > *,
|
|
310
|
+
.m-marketing-page[data-align="center"] > .m-marketing-main > * {
|
|
311
|
+
width: 100%;
|
|
312
|
+
}
|
|
313
|
+
.m-marketing-main {
|
|
314
|
+
flex: 1 0 auto;
|
|
315
|
+
}
|
|
316
|
+
.m-marketing-page[data-align="center"] > .m-marketing-main {
|
|
317
|
+
display: flex;
|
|
318
|
+
flex-direction: column;
|
|
319
|
+
justify-content: center;
|
|
277
320
|
}
|
|
278
321
|
|
|
279
322
|
/* == Banner == */
|
|
@@ -591,6 +634,30 @@
|
|
|
591
634
|
.m-hero-split .m-section-title {
|
|
592
635
|
font-size: clamp(40px, 5vw, 64px);
|
|
593
636
|
}
|
|
637
|
+
/* Beside a preview the copy is start-aligned: no auto margins, which would
|
|
638
|
+
centre the description inside a copy column wider than its max width. */
|
|
639
|
+
.m-hero-split .m-hero-description {
|
|
640
|
+
margin-inline: 0;
|
|
641
|
+
}
|
|
642
|
+
.m-hero-split[data-wide="copy"] {
|
|
643
|
+
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
|
|
644
|
+
}
|
|
645
|
+
.m-hero-split[data-wide="preview"] {
|
|
646
|
+
grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
|
|
647
|
+
}
|
|
648
|
+
/* An emphasised phrase in the title: the brand's text-safe link colour. */
|
|
649
|
+
.m-hero .m-emphasis {
|
|
650
|
+
font-style: normal;
|
|
651
|
+
color: var(--m-link);
|
|
652
|
+
}
|
|
653
|
+
.m-hero:is([data-background], [data-tone="accent"]) .m-emphasis {
|
|
654
|
+
color: inherit;
|
|
655
|
+
}
|
|
656
|
+
.m-hero-note {
|
|
657
|
+
margin-top: 20px;
|
|
658
|
+
color: var(--m-muted);
|
|
659
|
+
font-size: 14px;
|
|
660
|
+
}
|
|
594
661
|
.m-hero-preview {
|
|
595
662
|
margin-top: 48px;
|
|
596
663
|
text-align: left;
|
|
@@ -694,6 +761,7 @@
|
|
|
694
761
|
}
|
|
695
762
|
@media (max-width: 900px) {
|
|
696
763
|
.m-hero-split,
|
|
764
|
+
.m-hero-split[data-wide],
|
|
697
765
|
.m-feature-section,
|
|
698
766
|
.m-faq {
|
|
699
767
|
grid-template-columns: minmax(0, 1fr);
|
|
@@ -1893,6 +1961,41 @@
|
|
|
1893
1961
|
text-align: center;
|
|
1894
1962
|
margin-top: 40px;
|
|
1895
1963
|
}
|
|
1964
|
+
/* Footer: one compact row that wraps in reading order. */
|
|
1965
|
+
.m-footer[data-layout="inline"] {
|
|
1966
|
+
padding-block: 24px;
|
|
1967
|
+
border-top: 1px solid var(--m-line);
|
|
1968
|
+
}
|
|
1969
|
+
.m-footer-inline {
|
|
1970
|
+
display: flex;
|
|
1971
|
+
flex-wrap: wrap;
|
|
1972
|
+
align-items: center;
|
|
1973
|
+
gap: 8px 24px;
|
|
1974
|
+
color: var(--m-muted);
|
|
1975
|
+
font-size: 13px;
|
|
1976
|
+
}
|
|
1977
|
+
.m-footer-inline .m-brand {
|
|
1978
|
+
font-size: 14px;
|
|
1979
|
+
color: var(--m-ink);
|
|
1980
|
+
}
|
|
1981
|
+
.m-footer-inline p {
|
|
1982
|
+
margin: 0;
|
|
1983
|
+
}
|
|
1984
|
+
.m-footer-inline > :is(nav, div) {
|
|
1985
|
+
display: flex;
|
|
1986
|
+
flex-wrap: wrap;
|
|
1987
|
+
gap: 8px 20px;
|
|
1988
|
+
}
|
|
1989
|
+
.m-footer-inline a {
|
|
1990
|
+
color: var(--m-muted);
|
|
1991
|
+
text-decoration: none;
|
|
1992
|
+
}
|
|
1993
|
+
.m-footer-inline a:hover {
|
|
1994
|
+
color: var(--m-ink);
|
|
1995
|
+
}
|
|
1996
|
+
.m-footer-inline > .m-icon-links {
|
|
1997
|
+
margin: 0;
|
|
1998
|
+
}
|
|
1896
1999
|
@media (max-width: 900px) {
|
|
1897
2000
|
.m-footer-grid {
|
|
1898
2001
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
@@ -2385,6 +2488,82 @@
|
|
|
2385
2488
|
background: var(--m-primary);
|
|
2386
2489
|
color: var(--m-on-primary);
|
|
2387
2490
|
}
|
|
2491
|
+
|
|
2492
|
+
/* == NetworkIllustration == */
|
|
2493
|
+
/* Nodes and connections from data; signals travel along connections. Every
|
|
2494
|
+
colour is a token, so the drawing follows the brand and appearance. */
|
|
2495
|
+
.m-network {
|
|
2496
|
+
display: flex;
|
|
2497
|
+
flex-direction: column;
|
|
2498
|
+
gap: 8px;
|
|
2499
|
+
}
|
|
2500
|
+
.m-network svg {
|
|
2501
|
+
display: block;
|
|
2502
|
+
width: 100%;
|
|
2503
|
+
height: auto;
|
|
2504
|
+
overflow: visible;
|
|
2505
|
+
}
|
|
2506
|
+
.m-network-link {
|
|
2507
|
+
stroke: var(--m-line-strong);
|
|
2508
|
+
stroke-width: 1.5;
|
|
2509
|
+
/* Lines keep their width however large the drawing is shown. */
|
|
2510
|
+
vector-effect: non-scaling-stroke;
|
|
2511
|
+
}
|
|
2512
|
+
/* The outline is the strong line (3:1) whatever the fill, so nodes stay visible. */
|
|
2513
|
+
.m-network-node {
|
|
2514
|
+
fill: var(--m-surface);
|
|
2515
|
+
stroke: var(--m-line-strong);
|
|
2516
|
+
stroke-width: 1.5;
|
|
2517
|
+
vector-effect: non-scaling-stroke;
|
|
2518
|
+
}
|
|
2519
|
+
.m-network-node[data-tone="accent"] {
|
|
2520
|
+
fill: var(--m-accent);
|
|
2521
|
+
}
|
|
2522
|
+
.m-network-label {
|
|
2523
|
+
fill: var(--m-secondary);
|
|
2524
|
+
font-size: 12px;
|
|
2525
|
+
}
|
|
2526
|
+
.m-network-signal {
|
|
2527
|
+
fill: var(--m-accent);
|
|
2528
|
+
translate: var(--m-signal-from);
|
|
2529
|
+
animation: m-network-signal 3s linear infinite;
|
|
2530
|
+
}
|
|
2531
|
+
.m-network[data-paused] .m-network-signal {
|
|
2532
|
+
animation-play-state: paused;
|
|
2533
|
+
}
|
|
2534
|
+
/* Below the drawing, never over it, so no node is covered at any size. */
|
|
2535
|
+
.m-network-toggle {
|
|
2536
|
+
align-self: flex-end;
|
|
2537
|
+
padding: 4px 10px;
|
|
2538
|
+
border: 1px solid var(--m-line);
|
|
2539
|
+
border-radius: var(--m-radius-sm);
|
|
2540
|
+
background: var(--m-surface);
|
|
2541
|
+
color: var(--m-secondary);
|
|
2542
|
+
font: inherit;
|
|
2543
|
+
font-size: 12px;
|
|
2544
|
+
cursor: pointer;
|
|
2545
|
+
}
|
|
2546
|
+
.m-network-toggle:hover {
|
|
2547
|
+
color: var(--m-ink);
|
|
2548
|
+
}
|
|
2549
|
+
/* Before scripts run, and whenever reduced motion is requested, show the
|
|
2550
|
+
static drawing; the script then stops rendering the signals. */
|
|
2551
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2552
|
+
.m-network-signal {
|
|
2553
|
+
display: none;
|
|
2554
|
+
}
|
|
2555
|
+
.m-network-toggle {
|
|
2556
|
+
display: none;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
@keyframes m-network-signal {
|
|
2561
|
+
from {
|
|
2562
|
+
translate: var(--m-signal-from);
|
|
2563
|
+
}
|
|
2564
|
+
to {
|
|
2565
|
+
translate: var(--m-signal-to);
|
|
2566
|
+
}
|
|
2388
2567
|
}
|
|
2389
2568
|
@keyframes m-chat-dot {
|
|
2390
2569
|
0%,
|
package/src/marketing.tsx
CHANGED
|
@@ -108,6 +108,7 @@ function Flyout({
|
|
|
108
108
|
}
|
|
109
109
|
export function MarketingHeader({
|
|
110
110
|
brand,
|
|
111
|
+
tagline,
|
|
111
112
|
homeHref = "/",
|
|
112
113
|
logo,
|
|
113
114
|
links,
|
|
@@ -119,6 +120,8 @@ export function MarketingHeader({
|
|
|
119
120
|
id,
|
|
120
121
|
}: {
|
|
121
122
|
brand: string;
|
|
123
|
+
/** A short descriptor beside the wordmark, outside the home link. Hidden on narrow screens. */
|
|
124
|
+
tagline?: string;
|
|
122
125
|
logo?: ReactNode;
|
|
123
126
|
homeHref?: string;
|
|
124
127
|
/** Links, or groups of items that open a flyout panel. */
|
|
@@ -137,12 +140,21 @@ export function MarketingHeader({
|
|
|
137
140
|
const navId = useDomId("m-marketing-nav");
|
|
138
141
|
const panel = mobileMenu === "panel";
|
|
139
142
|
const close = () => setOpen(false);
|
|
140
|
-
const
|
|
143
|
+
const homeLink = (
|
|
141
144
|
<KitLink className="m-brand" href={homeHref}>
|
|
142
145
|
<BrandMark logo={logo} />
|
|
143
146
|
{brand}
|
|
144
147
|
</KitLink>
|
|
145
148
|
);
|
|
149
|
+
// The tagline sits outside the link so the link's name stays the brand.
|
|
150
|
+
const brandLink = tagline ? (
|
|
151
|
+
<div className="m-brand-lockup">
|
|
152
|
+
{homeLink}
|
|
153
|
+
<span className="m-brand-tagline">{tagline}</span>
|
|
154
|
+
</div>
|
|
155
|
+
) : (
|
|
156
|
+
homeLink
|
|
157
|
+
);
|
|
146
158
|
const menuButton = (
|
|
147
159
|
<Button
|
|
148
160
|
variant="ghost"
|
|
@@ -272,15 +284,53 @@ function PanelLinks({
|
|
|
272
284
|
</nav>
|
|
273
285
|
);
|
|
274
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* A public page: the header, a main region that grows to fill the viewport, and
|
|
289
|
+
* the footer. Content is never clipped; a longer page simply scrolls.
|
|
290
|
+
*/
|
|
291
|
+
export function MarketingPage({
|
|
292
|
+
header,
|
|
293
|
+
footer,
|
|
294
|
+
children,
|
|
295
|
+
align = "start",
|
|
296
|
+
id,
|
|
297
|
+
className,
|
|
298
|
+
}: {
|
|
299
|
+
/** Usually a MarketingHeader. */
|
|
300
|
+
header?: ReactNode;
|
|
301
|
+
/** Usually a Footer. */
|
|
302
|
+
footer?: ReactNode;
|
|
303
|
+
children: ReactNode;
|
|
304
|
+
/** start: content from the top. center: content centred in the space between header and footer, for a single hero. */
|
|
305
|
+
align?: "start" | "center";
|
|
306
|
+
id?: string;
|
|
307
|
+
className?: string;
|
|
308
|
+
}) {
|
|
309
|
+
return (
|
|
310
|
+
<div
|
|
311
|
+
id={id}
|
|
312
|
+
className={cx("m-marketing-page", className)}
|
|
313
|
+
{...componentRoot("MarketingPage")}
|
|
314
|
+
data-align={align === "center" ? "center" : undefined}
|
|
315
|
+
>
|
|
316
|
+
{header}
|
|
317
|
+
<main className="m-marketing-main">{children}</main>
|
|
318
|
+
{footer}
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
275
322
|
export function Hero({
|
|
276
323
|
id,
|
|
277
324
|
className,
|
|
278
325
|
eyebrow,
|
|
279
326
|
title,
|
|
327
|
+
emphasis,
|
|
280
328
|
description,
|
|
281
329
|
actions = [],
|
|
330
|
+
note,
|
|
282
331
|
children,
|
|
283
332
|
layout = "center",
|
|
333
|
+
wide,
|
|
284
334
|
headingLevel = 1,
|
|
285
335
|
background,
|
|
286
336
|
images = [],
|
|
@@ -290,11 +340,17 @@ export function Hero({
|
|
|
290
340
|
className?: string;
|
|
291
341
|
eyebrow?: string;
|
|
292
342
|
title: string;
|
|
343
|
+
/** A phrase in the title to emphasise. Its first occurrence renders as `<em>` inside the one heading; a phrase not in the title is ignored. */
|
|
344
|
+
emphasis?: string;
|
|
293
345
|
description: string;
|
|
294
346
|
actions?: LinkAction[];
|
|
347
|
+
/** Short supporting content below the actions, such as a note or a Notice. */
|
|
348
|
+
note?: ReactNode;
|
|
295
349
|
children?: ReactNode;
|
|
296
350
|
/** center: copy above the preview. split: copy beside the preview. angled: copy beside an image that fills the end half to the edge, cut on a slant (pass one img or video as children). */
|
|
297
351
|
layout?: "center" | "split" | "angled";
|
|
352
|
+
/** Split only: the column that takes more of the width. Omitted, the columns are even. */
|
|
353
|
+
wide?: "copy" | "preview";
|
|
298
354
|
headingLevel?: HeadingLevel;
|
|
299
355
|
/** Decorative full-cover image or video behind the copy. The hero switches to the inverse tone with a scrim so text keeps its contrast. */
|
|
300
356
|
background?: ReactNode;
|
|
@@ -308,6 +364,7 @@ export function Hero({
|
|
|
308
364
|
id={id}
|
|
309
365
|
className={cx("m-hero", `m-hero-${layout}`, className)}
|
|
310
366
|
{...componentRoot("Hero")}
|
|
367
|
+
data-wide={layout === "split" ? wide : undefined}
|
|
311
368
|
data-background={background ? true : undefined}
|
|
312
369
|
data-tone={background ? "inverse" : sectionTone(tone)}
|
|
313
370
|
>
|
|
@@ -320,10 +377,12 @@ export function Hero({
|
|
|
320
377
|
<TitledIntro
|
|
321
378
|
eyebrow={eyebrow}
|
|
322
379
|
title={title}
|
|
380
|
+
emphasis={emphasis}
|
|
323
381
|
headingLevel={headingLevel}
|
|
324
382
|
/>
|
|
325
383
|
<p className="m-hero-description">{description}</p>
|
|
326
384
|
<ActionLinks actions={actions} />
|
|
385
|
+
{note && <div className="m-hero-note">{note}</div>}
|
|
327
386
|
</div>
|
|
328
387
|
{images.length > 0 && (
|
|
329
388
|
<div className="m-hero-tiles">
|
|
@@ -928,7 +987,7 @@ export function Footer({
|
|
|
928
987
|
logo,
|
|
929
988
|
description,
|
|
930
989
|
note,
|
|
931
|
-
columns,
|
|
990
|
+
columns = [],
|
|
932
991
|
copyright,
|
|
933
992
|
legal = [],
|
|
934
993
|
itemHeadingLevel = 2,
|
|
@@ -940,30 +999,31 @@ export function Footer({
|
|
|
940
999
|
className?: string;
|
|
941
1000
|
brand: string;
|
|
942
1001
|
logo?: ReactNode;
|
|
943
|
-
description
|
|
1002
|
+
description?: string;
|
|
944
1003
|
note?: ReactNode;
|
|
945
|
-
/** Link columns; each label is the column's visible heading and its navigation name. Centred, the label only names the navigation. */
|
|
946
|
-
columns
|
|
1004
|
+
/** Link columns; each label is the column's visible heading and its navigation name. Centred or inline, the label only names the navigation. */
|
|
1005
|
+
columns?: ItemGroup<LinkAction>[];
|
|
947
1006
|
copyright: string;
|
|
948
1007
|
legal?: LinkAction[];
|
|
949
1008
|
/** Level of the column headings. */
|
|
950
1009
|
itemHeadingLevel?: HeadingLevel;
|
|
951
1010
|
/** Social profiles, usually icon-only; each label names its link. */
|
|
952
1011
|
social?: IconLink[];
|
|
953
|
-
/** columns: the brand beside headed link columns. center: brand, a row of links and social links, all centred. */
|
|
954
|
-
layout?: "columns" | "center";
|
|
1012
|
+
/** columns: the brand beside headed link columns. center: brand, a row of links and social links, all centred. inline: one compact row of brand, notes, links and copyright that wraps in reading order. */
|
|
1013
|
+
layout?: "columns" | "center" | "inline";
|
|
955
1014
|
/** A signup row above the copyright, usually a Newsletter; the footer removes its section padding. */
|
|
956
1015
|
newsletter?: ReactNode;
|
|
957
1016
|
}) {
|
|
958
1017
|
const Heading = headingTag(itemHeadingLevel);
|
|
959
1018
|
const center = layout === "center";
|
|
1019
|
+
const inline = layout === "inline";
|
|
960
1020
|
const identity = (
|
|
961
1021
|
<>
|
|
962
1022
|
<strong className="m-brand">
|
|
963
1023
|
<BrandMark logo={logo} />
|
|
964
1024
|
{brand}
|
|
965
1025
|
</strong>
|
|
966
|
-
<p>{description}</p>
|
|
1026
|
+
{description && <p>{description}</p>}
|
|
967
1027
|
{note && <p>{note}</p>}
|
|
968
1028
|
</>
|
|
969
1029
|
);
|
|
@@ -972,8 +1032,8 @@ export function Footer({
|
|
|
972
1032
|
);
|
|
973
1033
|
const navs = columns.map((column) => (
|
|
974
1034
|
<nav key={column.label} aria-label={column.label}>
|
|
975
|
-
{/* Centred, the links sit in one row and the label only names the navigation. */}
|
|
976
|
-
{!center && <Heading>{column.label}</Heading>}
|
|
1035
|
+
{/* Centred or inline, the links sit in one row and the label only names the navigation. */}
|
|
1036
|
+
{!center && !inline && <Heading>{column.label}</Heading>}
|
|
977
1037
|
{column.items.map((link, index) => (
|
|
978
1038
|
<KitLink href={link.href} key={`${link.label}:${link.href}:${index}`}>
|
|
979
1039
|
{link.label}
|
|
@@ -981,6 +1041,34 @@ export function Footer({
|
|
|
981
1041
|
))}
|
|
982
1042
|
</nav>
|
|
983
1043
|
));
|
|
1044
|
+
const legalLinks = (
|
|
1045
|
+
<div>
|
|
1046
|
+
{legal.map((link, index) => (
|
|
1047
|
+
<KitLink key={`${link.label}:${link.href}:${index}`} href={link.href}>
|
|
1048
|
+
{link.label}
|
|
1049
|
+
</KitLink>
|
|
1050
|
+
))}
|
|
1051
|
+
</div>
|
|
1052
|
+
);
|
|
1053
|
+
if (inline)
|
|
1054
|
+
return (
|
|
1055
|
+
<footer
|
|
1056
|
+
id={id}
|
|
1057
|
+
className={cx("m-footer", className)}
|
|
1058
|
+
{...componentRoot("Footer")}
|
|
1059
|
+
data-layout="inline"
|
|
1060
|
+
>
|
|
1061
|
+
{/* One wrapping row; the DOM order is the reading order at every width. */}
|
|
1062
|
+
<div className="m-footer-inline">
|
|
1063
|
+
{identity}
|
|
1064
|
+
{navs}
|
|
1065
|
+
{socialLinks}
|
|
1066
|
+
<span>{copyright}</span>
|
|
1067
|
+
{legal.length > 0 && legalLinks}
|
|
1068
|
+
</div>
|
|
1069
|
+
{newsletter && <div className="m-footer-newsletter">{newsletter}</div>}
|
|
1070
|
+
</footer>
|
|
1071
|
+
);
|
|
984
1072
|
return (
|
|
985
1073
|
<footer
|
|
986
1074
|
id={id}
|
|
@@ -1006,16 +1094,7 @@ export function Footer({
|
|
|
1006
1094
|
{newsletter && <div className="m-footer-newsletter">{newsletter}</div>}
|
|
1007
1095
|
<div className="m-footer-bottom">
|
|
1008
1096
|
<span>{copyright}</span>
|
|
1009
|
-
|
|
1010
|
-
{legal.map((link, index) => (
|
|
1011
|
-
<KitLink
|
|
1012
|
-
key={`${link.label}:${link.href}:${index}`}
|
|
1013
|
-
href={link.href}
|
|
1014
|
-
>
|
|
1015
|
-
{link.label}
|
|
1016
|
-
</KitLink>
|
|
1017
|
-
))}
|
|
1018
|
-
</div>
|
|
1097
|
+
{legalLinks}
|
|
1019
1098
|
</div>
|
|
1020
1099
|
</footer>
|
|
1021
1100
|
);
|