@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** A point in the drawing, in the illustration's viewBox units. */
|
|
2
|
+
export interface NetworkNode {
|
|
3
|
+
id: string;
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
/** Short text drawn under the node. */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** accent: the brand's accent fill, to pick out a node. */
|
|
9
|
+
tone?: "default" | "accent";
|
|
10
|
+
}
|
|
11
|
+
/** A straight connection between two node ids. */
|
|
12
|
+
export interface NetworkConnection {
|
|
13
|
+
from: string;
|
|
14
|
+
to: string;
|
|
15
|
+
/** Whether a signal travels from `from` to `to`. Default true. */
|
|
16
|
+
signal?: boolean;
|
|
17
|
+
/** Seconds for this signal to cross; defaults to the illustration's duration. */
|
|
18
|
+
duration?: number;
|
|
19
|
+
/** Seconds before this signal starts; defaults to an even stagger. */
|
|
20
|
+
delay?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A lightweight network drawing: nodes and connections supplied as data, with
|
|
24
|
+
* optional signals travelling along the connections. Decorative unless it has
|
|
25
|
+
* a label. The animation is illustrative: it never represents live activity.
|
|
26
|
+
*/
|
|
27
|
+
export declare function NetworkIllustration({ nodes, connections, width, height, label, description, motion, duration, paused, onPausedChange, id, className, }: {
|
|
28
|
+
nodes: NetworkNode[];
|
|
29
|
+
connections: NetworkConnection[];
|
|
30
|
+
/** Width of the viewBox; the drawing scales to its container. */
|
|
31
|
+
width?: number;
|
|
32
|
+
/** Height of the viewBox. */
|
|
33
|
+
height?: number;
|
|
34
|
+
/** Makes the drawing informative: an image with this accessible name. Omitted, it is decorative and hidden from assistive technology. */
|
|
35
|
+
label?: string;
|
|
36
|
+
/** With a label, a longer description of what the drawing shows. */
|
|
37
|
+
description?: string;
|
|
38
|
+
/** auto: signals travel where connections carry one, with a pause control. static: the same drawing without motion. */
|
|
39
|
+
motion?: "auto" | "static";
|
|
40
|
+
/** Seconds for a signal to cross a connection. */
|
|
41
|
+
duration?: number;
|
|
42
|
+
/** Whether the user has paused the animation; pass with onPausedChange to control it or share it between illustrations. */
|
|
43
|
+
paused?: boolean;
|
|
44
|
+
onPausedChange?: (paused: boolean) => void;
|
|
45
|
+
id?: string;
|
|
46
|
+
className?: string;
|
|
47
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { componentRoot, cx, useDomId, usePlayback } from "./internal.js";
|
|
4
|
+
const NODE_RADIUS = 8;
|
|
5
|
+
const SIGNAL_RADIUS = 4;
|
|
6
|
+
/**
|
|
7
|
+
* A lightweight network drawing: nodes and connections supplied as data, with
|
|
8
|
+
* optional signals travelling along the connections. Decorative unless it has
|
|
9
|
+
* a label. The animation is illustrative: it never represents live activity.
|
|
10
|
+
*/
|
|
11
|
+
export function NetworkIllustration({ nodes, connections, width = 480, height = 320, label, description, motion = "auto", duration = 3, paused, onPausedChange, id, className, }) {
|
|
12
|
+
const ref = useRef(null);
|
|
13
|
+
const titleId = useDomId("m-network-title");
|
|
14
|
+
const descId = useDomId("m-network-desc");
|
|
15
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
16
|
+
const links = connections.flatMap((connection) => {
|
|
17
|
+
const from = byId.get(connection.from);
|
|
18
|
+
const to = byId.get(connection.to);
|
|
19
|
+
return from && to ? [{ ...connection, a: from, b: to }] : [];
|
|
20
|
+
});
|
|
21
|
+
const carriers = links.filter((link) => link.signal !== false);
|
|
22
|
+
const animated = motion === "auto" && carriers.length > 0;
|
|
23
|
+
const { reason, reduced, userPaused, toggle } = usePlayback(ref, {
|
|
24
|
+
paused,
|
|
25
|
+
onPausedChange,
|
|
26
|
+
enabled: animated,
|
|
27
|
+
});
|
|
28
|
+
const informative = Boolean(label);
|
|
29
|
+
return (_jsxs("div", { ref: ref, id: id, className: cx("m-network", className), ...componentRoot("NetworkIllustration"), "data-motion": motion === "static" ? "static" : undefined, "data-paused": animated ? (reason ?? undefined) : undefined, children: [_jsxs("svg", { viewBox: `0 0 ${width} ${height}`, ...(informative
|
|
30
|
+
? {
|
|
31
|
+
role: "img",
|
|
32
|
+
"aria-labelledby": titleId,
|
|
33
|
+
"aria-describedby": description ? descId : undefined,
|
|
34
|
+
}
|
|
35
|
+
: { "aria-hidden": true, focusable: false }), children: [informative && _jsx("title", { id: titleId, children: label }), informative && description && _jsx("desc", { id: descId, children: description }), links.map((link, index) => (_jsx("line", { className: "m-network-link", x1: link.a.x, y1: link.a.y, x2: link.b.x, y2: link.b.y }, `${link.from}:${link.to}:${index}`))), animated &&
|
|
36
|
+
!reduced &&
|
|
37
|
+
carriers.map((link, index) => {
|
|
38
|
+
const time = link.duration ?? duration;
|
|
39
|
+
const delay = link.delay ?? -((index * time) / carriers.length);
|
|
40
|
+
return (_jsx("circle", { className: "m-network-signal", r: SIGNAL_RADIUS, style: {
|
|
41
|
+
"--m-signal-from": `${link.a.x}px ${link.a.y}px`,
|
|
42
|
+
"--m-signal-to": `${link.b.x}px ${link.b.y}px`,
|
|
43
|
+
animationDuration: `${time}s`,
|
|
44
|
+
animationDelay: delay ? `${delay}s` : undefined,
|
|
45
|
+
} }, `${link.from}:${link.to}:${index}`));
|
|
46
|
+
}), nodes.map((node) => (_jsx("circle", { className: "m-network-node", cx: node.x, cy: node.y, r: NODE_RADIUS, "data-tone": node.tone === "accent" ? "accent" : undefined }, node.id))), nodes.map((node) => node.label && (_jsx("text", { className: "m-network-label", x: node.x, y: node.y + NODE_RADIUS + 16, textAnchor: "middle", children: node.label }, node.id)))] }), animated && !reduced && (_jsx("button", { type: "button", className: "m-network-toggle", onClick: toggle, children: userPaused ? "Play animation" : "Pause animation" }))] }));
|
|
47
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { AppShell, PageHeading, MetricGroup, DataTable, Table, Pagination, Setti
|
|
|
7
7
|
export type { AppShellProps, NavigationItem, DataTableProps, DataTableColumn, TableSort, SortDirection, SignInProvider, SidebarProps, BreadcrumbItem, ProgressStep, TabNavItem, StackedShellProps, NavbarProps, BreadcrumbsProps, TabNavProps, ProgressStepsProps, ActionPanelProps, PageHeadingProps, MetricGroupProps, TableProps, PaginationProps, SettingsSectionProps, ActivityFeedProps, SignInPanelProps, } from "./application.js";
|
|
8
8
|
export { AuthLayout, ProviderButtons, PasswordInput, CodeInput, } from "./auth.js";
|
|
9
9
|
export type { AuthLayoutProps, ProviderButtonsProps, PasswordInputProps, PasswordRequirement, PasswordStrength, CodeInputProps, } from "./auth.js";
|
|
10
|
-
export { MarketingHeader, Hero, FeatureGrid, FeatureSection, ProcessSteps, Testimonial, LogoCloud, Pricing, FAQ, CallToAction, Footer, HeaderSection, ContentSection, } from "./marketing.js";
|
|
10
|
+
export { MarketingHeader, MarketingPage, Hero, FeatureGrid, FeatureSection, ProcessSteps, Testimonial, LogoCloud, Pricing, FAQ, CallToAction, Footer, HeaderSection, ContentSection, } from "./marketing.js";
|
|
11
11
|
export type { PricingPlan, PricingExtra, HeaderLink, FlyoutItem, } from "./marketing.js";
|
|
12
12
|
export { SegmentedControl, CodeBlock, PreviewFrame } from "./inspection.js";
|
|
13
13
|
export { Avatar, AvatarGroup, Kbd, ProgressBar, FilterChip, CheckList, DescriptionList, GridList, AttachmentList, } from "./display.js";
|
|
@@ -32,3 +32,5 @@ export type { ProductImage, ProductCard, ProductOption, CartItem, FilterFacet, R
|
|
|
32
32
|
export { CommentComposer } from "./composer.js";
|
|
33
33
|
export type { LinkItem, ItemGroup } from "./navigation.js";
|
|
34
34
|
export type { HeadingLevel } from "./heading.js";
|
|
35
|
+
export { NetworkIllustration } from "./illustration.js";
|
|
36
|
+
export type { NetworkNode, NetworkConnection } from "./illustration.js";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { ThemeProvider, AppearanceControl } from "./foundation.js";
|
|
|
2
2
|
export { Button, ButtonGroup, ActionLink, Input, Select, TextArea, Checkbox, Fieldset, RadioGroup, Switch, Badge, Tabs, Modal, Dropdown, Notice, EmptyState, ErrorSummary, } from "./primitives.js";
|
|
3
3
|
export { AppShell, PageHeading, MetricGroup, DataTable, Table, Pagination, SettingsSection, ActivityFeed, SignInPanel, Sidebar, Navbar, StackedShell, Breadcrumbs, ProgressSteps, TabNav, ActionPanel, } from "./application.js";
|
|
4
4
|
export { AuthLayout, ProviderButtons, PasswordInput, CodeInput, } from "./auth.js";
|
|
5
|
-
export { MarketingHeader, Hero, FeatureGrid, FeatureSection, ProcessSteps, Testimonial, LogoCloud, Pricing, FAQ, CallToAction, Footer, HeaderSection, ContentSection, } from "./marketing.js";
|
|
5
|
+
export { MarketingHeader, MarketingPage, Hero, FeatureGrid, FeatureSection, ProcessSteps, Testimonial, LogoCloud, Pricing, FAQ, CallToAction, Footer, HeaderSection, ContentSection, } from "./marketing.js";
|
|
6
6
|
export { SegmentedControl, CodeBlock, PreviewFrame } from "./inspection.js";
|
|
7
7
|
export { Avatar, AvatarGroup, Kbd, ProgressBar, FilterChip, CheckList, DescriptionList, GridList, AttachmentList, } from "./display.js";
|
|
8
8
|
export { Stack, Container, Text, Section, Divider, Card, MediaObject, SectionHeading, Prose, } from "./layout.js";
|
|
@@ -15,3 +15,4 @@ export { Listbox, Combobox } from "./choice.js";
|
|
|
15
15
|
export { Calendar } from "./calendar.js";
|
|
16
16
|
export { ProductList, ProductOverview, ShoppingCart, OrderSummary, ProductQuickView, ProductFilters, ProductReviews, OrderHistory, } from "./ecommerce.js";
|
|
17
17
|
export { CommentComposer } from "./composer.js";
|
|
18
|
+
export { NetworkIllustration } from "./illustration.js";
|
package/dist/internal.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type HTMLAttributes, type ReactNode } from "react";
|
|
1
|
+
import { type HTMLAttributes, type RefObject, type ReactNode } from "react";
|
|
2
2
|
import { type ItemGroup } from "./navigation.js";
|
|
3
3
|
import { type LinkAction } from "./primitives.js";
|
|
4
4
|
import { type HeadingLevel } from "./heading.js";
|
|
@@ -36,6 +36,8 @@ export interface IntroProps {
|
|
|
36
36
|
eyebrow?: string;
|
|
37
37
|
/** Omit to render no heading. */
|
|
38
38
|
title?: string;
|
|
39
|
+
/** A phrase in `title` to emphasise; its first occurrence renders as `<em>`. */
|
|
40
|
+
emphasis?: string;
|
|
39
41
|
description?: string;
|
|
40
42
|
/** Id for the heading, e.g. the target of the section's aria-labelledby. */
|
|
41
43
|
id?: string;
|
|
@@ -49,7 +51,7 @@ export interface IntroProps {
|
|
|
49
51
|
* in the same parent: React derives useId values from each child's position,
|
|
50
52
|
* so keeping the slots flat keeps the ids of that content unchanged.
|
|
51
53
|
*/
|
|
52
|
-
export declare function introSlots({ eyebrow, title, description, id, headingLevel, descriptionClassName, }: IntroProps): ReactNode[];
|
|
54
|
+
export declare function introSlots({ eyebrow, title, emphasis, description, id, headingLevel, descriptionClassName, }: IntroProps): ReactNode[];
|
|
53
55
|
/** The section intro as one element, for parents where nothing user-supplied follows it. */
|
|
54
56
|
export declare function SectionIntro(props: IntroProps): import("react").JSX.Element;
|
|
55
57
|
/** A row of link actions: the first primary, the rest secondary. */
|
|
@@ -367,6 +369,26 @@ export declare function LinkContent({ prefix, label, icon, count, marker, }: {
|
|
|
367
369
|
}): import("react").FunctionComponentElement<import("react").FragmentProps>;
|
|
368
370
|
/** A generated id that is safe in CSS selectors and URL fragments. */
|
|
369
371
|
export declare function useDomId(prefix: string): string;
|
|
372
|
+
/** Why an animation is not running; null while it plays. */
|
|
373
|
+
export type PauseReason = "user" | "motion" | "hidden" | "offscreen" | null;
|
|
374
|
+
/**
|
|
375
|
+
* Shared pause and resume for decorative motion. It plays only while the
|
|
376
|
+
* user has not paused it, reduced motion is not requested, the document is
|
|
377
|
+
* visible and `ref` is on screen. The user's pause is kept apart from the
|
|
378
|
+
* automatic reasons, so returning to the page never overrides it. `paused`
|
|
379
|
+
* and `onPausedChange` make the user's choice controlled, for example one
|
|
380
|
+
* state shared by several animations. Every listener is removed on unmount.
|
|
381
|
+
*/
|
|
382
|
+
export declare function usePlayback(ref: RefObject<Element | null>, { paused, onPausedChange, enabled, }: {
|
|
383
|
+
paused?: boolean;
|
|
384
|
+
onPausedChange?: (paused: boolean) => void;
|
|
385
|
+
enabled?: boolean;
|
|
386
|
+
}): {
|
|
387
|
+
reason: PauseReason;
|
|
388
|
+
reduced: boolean;
|
|
389
|
+
userPaused: boolean;
|
|
390
|
+
toggle: () => void;
|
|
391
|
+
};
|
|
370
392
|
/**
|
|
371
393
|
* Next index for arrow, Home and End keys in a horizontal set (wrapping), or
|
|
372
394
|
* null for any other key.
|
package/dist/internal.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Shared building blocks for the kit's components. Internal: nothing here is
|
|
3
3
|
// exported from the package entry, so these can change without a release note.
|
|
4
|
-
import { Fragment, createContext, createElement, useContext, useId, cloneElement, isValidElement, } from "react";
|
|
4
|
+
import { Fragment, createContext, createElement, useContext, useEffect, useId, useState, cloneElement, isValidElement, } from "react";
|
|
5
5
|
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
|
|
6
6
|
import { KitLink } from "./navigation.js";
|
|
7
7
|
import { ActionLink, Button } from "./primitives.js";
|
|
@@ -46,14 +46,21 @@ export function cx(...classes) {
|
|
|
46
46
|
* in the same parent: React derives useId values from each child's position,
|
|
47
47
|
* so keeping the slots flat keeps the ids of that content unchanged.
|
|
48
48
|
*/
|
|
49
|
-
export function introSlots({ eyebrow, title, description, id, headingLevel = 2, descriptionClassName = "m-section-description", }) {
|
|
49
|
+
export function introSlots({ eyebrow, title, emphasis, description, id, headingLevel = 2, descriptionClassName = "m-section-description", }) {
|
|
50
50
|
const Heading = headingTag(headingLevel);
|
|
51
51
|
return [
|
|
52
52
|
eyebrow && _jsx("p", { className: "m-eyebrow", children: eyebrow }),
|
|
53
|
-
title !== undefined && _jsx(Heading, { id: id, children: title }),
|
|
53
|
+
title !== undefined && (_jsx(Heading, { id: id, children: emphasise(title, emphasis) })),
|
|
54
54
|
description && (_jsx("p", { className: descriptionClassName ?? undefined, children: description })),
|
|
55
55
|
];
|
|
56
56
|
}
|
|
57
|
+
/** `title` with the first occurrence of `phrase` in `<em>`; unchanged when absent. */
|
|
58
|
+
function emphasise(title, phrase) {
|
|
59
|
+
const at = phrase ? title.indexOf(phrase) : -1;
|
|
60
|
+
if (!phrase || at < 0)
|
|
61
|
+
return title;
|
|
62
|
+
return (_jsxs(_Fragment, { children: [title.slice(0, at), _jsx("em", { className: "m-emphasis", children: phrase }), title.slice(at + phrase.length)] }));
|
|
63
|
+
}
|
|
57
64
|
/** The section intro as one element, for parents where nothing user-supplied follows it. */
|
|
58
65
|
export function SectionIntro(props) {
|
|
59
66
|
return _jsxs(_Fragment, { children: [...introSlots(props)] });
|
|
@@ -84,6 +91,58 @@ export function LinkContent({ prefix, label, icon, count, marker = false, }) {
|
|
|
84
91
|
export function useDomId(prefix) {
|
|
85
92
|
return prefix + useId().replace(/[^a-zA-Z0-9-]/g, "");
|
|
86
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Shared pause and resume for decorative motion. It plays only while the
|
|
96
|
+
* user has not paused it, reduced motion is not requested, the document is
|
|
97
|
+
* visible and `ref` is on screen. The user's pause is kept apart from the
|
|
98
|
+
* automatic reasons, so returning to the page never overrides it. `paused`
|
|
99
|
+
* and `onPausedChange` make the user's choice controlled, for example one
|
|
100
|
+
* state shared by several animations. Every listener is removed on unmount.
|
|
101
|
+
*/
|
|
102
|
+
export function usePlayback(ref, { paused, onPausedChange, enabled = true, }) {
|
|
103
|
+
const [ownPaused, setOwnPaused] = useState(false);
|
|
104
|
+
const [reduced, setReduced] = useState(false);
|
|
105
|
+
const [hidden, setHidden] = useState(false);
|
|
106
|
+
const [offscreen, setOffscreen] = useState(false);
|
|
107
|
+
const userPaused = paused ?? ownPaused;
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!enabled)
|
|
110
|
+
return;
|
|
111
|
+
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
112
|
+
const onMedia = () => setReduced(media.matches);
|
|
113
|
+
const onVisibility = () => setHidden(document.visibilityState === "hidden");
|
|
114
|
+
onMedia();
|
|
115
|
+
onVisibility();
|
|
116
|
+
media.addEventListener("change", onMedia);
|
|
117
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
118
|
+
const observer = ref.current && typeof IntersectionObserver !== "undefined"
|
|
119
|
+
? new IntersectionObserver(([entry]) => setOffscreen(!entry.isIntersecting))
|
|
120
|
+
: null;
|
|
121
|
+
if (observer && ref.current)
|
|
122
|
+
observer.observe(ref.current);
|
|
123
|
+
return () => {
|
|
124
|
+
media.removeEventListener("change", onMedia);
|
|
125
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
126
|
+
observer?.disconnect();
|
|
127
|
+
};
|
|
128
|
+
}, [enabled, ref]);
|
|
129
|
+
const reason = userPaused
|
|
130
|
+
? "user"
|
|
131
|
+
: reduced
|
|
132
|
+
? "motion"
|
|
133
|
+
: hidden
|
|
134
|
+
? "hidden"
|
|
135
|
+
: offscreen
|
|
136
|
+
? "offscreen"
|
|
137
|
+
: null;
|
|
138
|
+
const toggle = () => {
|
|
139
|
+
const next = !userPaused;
|
|
140
|
+
if (paused === undefined)
|
|
141
|
+
setOwnPaused(next);
|
|
142
|
+
onPausedChange?.(next);
|
|
143
|
+
};
|
|
144
|
+
return { reason, reduced, userPaused, toggle };
|
|
145
|
+
}
|
|
87
146
|
/**
|
|
88
147
|
* Next index for arrow, Home and End keys in a horizontal set (wrapping), or
|
|
89
148
|
* null for any other key.
|
package/dist/marketing.d.ts
CHANGED
|
@@ -10,8 +10,10 @@ export type HeaderLink = LinkAction | (ItemGroup<FlyoutItem> & {
|
|
|
10
10
|
/** list: one column. columns: a wider panel in two columns. full: as wide as the viewport allows, up to four columns. */
|
|
11
11
|
layout?: "list" | "columns" | "full";
|
|
12
12
|
});
|
|
13
|
-
export declare function MarketingHeader({ brand, homeHref, logo, links, action, controls, layout, mobileMenu, className, id, }: {
|
|
13
|
+
export declare function MarketingHeader({ brand, tagline, homeHref, logo, links, action, controls, layout, mobileMenu, className, id, }: {
|
|
14
14
|
brand: string;
|
|
15
|
+
/** A short descriptor beside the wordmark, outside the home link. Hidden on narrow screens. */
|
|
16
|
+
tagline?: string;
|
|
15
17
|
logo?: ReactNode;
|
|
16
18
|
homeHref?: string;
|
|
17
19
|
/** Links, or groups of items that open a flyout panel. */
|
|
@@ -25,16 +27,37 @@ export declare function MarketingHeader({ brand, homeHref, logo, links, action,
|
|
|
25
27
|
className?: string;
|
|
26
28
|
id?: string;
|
|
27
29
|
}): import("react").JSX.Element;
|
|
28
|
-
|
|
30
|
+
/**
|
|
31
|
+
* A public page: the header, a main region that grows to fill the viewport, and
|
|
32
|
+
* the footer. Content is never clipped; a longer page simply scrolls.
|
|
33
|
+
*/
|
|
34
|
+
export declare function MarketingPage({ header, footer, children, align, id, className, }: {
|
|
35
|
+
/** Usually a MarketingHeader. */
|
|
36
|
+
header?: ReactNode;
|
|
37
|
+
/** Usually a Footer. */
|
|
38
|
+
footer?: ReactNode;
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
/** start: content from the top. center: content centred in the space between header and footer, for a single hero. */
|
|
41
|
+
align?: "start" | "center";
|
|
42
|
+
id?: string;
|
|
43
|
+
className?: string;
|
|
44
|
+
}): import("react").JSX.Element;
|
|
45
|
+
export declare function Hero({ id, className, eyebrow, title, emphasis, description, actions, note, children, layout, wide, headingLevel, background, images, tone, }: {
|
|
29
46
|
id?: string;
|
|
30
47
|
className?: string;
|
|
31
48
|
eyebrow?: string;
|
|
32
49
|
title: string;
|
|
50
|
+
/** 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. */
|
|
51
|
+
emphasis?: string;
|
|
33
52
|
description: string;
|
|
34
53
|
actions?: LinkAction[];
|
|
54
|
+
/** Short supporting content below the actions, such as a note or a Notice. */
|
|
55
|
+
note?: ReactNode;
|
|
35
56
|
children?: ReactNode;
|
|
36
57
|
/** 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). */
|
|
37
58
|
layout?: "center" | "split" | "angled";
|
|
59
|
+
/** Split only: the column that takes more of the width. Omitted, the columns are even. */
|
|
60
|
+
wide?: "copy" | "preview";
|
|
38
61
|
headingLevel?: HeadingLevel;
|
|
39
62
|
/** Decorative full-cover image or video behind the copy. The hero switches to the inverse tone with a scrim so text keeps its contrast. */
|
|
40
63
|
background?: ReactNode;
|
|
@@ -194,18 +217,18 @@ export declare function Footer({ id, className, brand, logo, description, note,
|
|
|
194
217
|
className?: string;
|
|
195
218
|
brand: string;
|
|
196
219
|
logo?: ReactNode;
|
|
197
|
-
description
|
|
220
|
+
description?: string;
|
|
198
221
|
note?: ReactNode;
|
|
199
|
-
/** Link columns; each label is the column's visible heading and its navigation name. Centred, the label only names the navigation. */
|
|
200
|
-
columns
|
|
222
|
+
/** Link columns; each label is the column's visible heading and its navigation name. Centred or inline, the label only names the navigation. */
|
|
223
|
+
columns?: ItemGroup<LinkAction>[];
|
|
201
224
|
copyright: string;
|
|
202
225
|
legal?: LinkAction[];
|
|
203
226
|
/** Level of the column headings. */
|
|
204
227
|
itemHeadingLevel?: HeadingLevel;
|
|
205
228
|
/** Social profiles, usually icon-only; each label names its link. */
|
|
206
229
|
social?: IconLink[];
|
|
207
|
-
/** columns: the brand beside headed link columns. center: brand, a row of links and social links, all centred. */
|
|
208
|
-
layout?: "columns" | "center";
|
|
230
|
+
/** 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. */
|
|
231
|
+
layout?: "columns" | "center" | "inline";
|
|
209
232
|
/** A signup row above the copyright, usually a Newsletter; the footer removes its section padding. */
|
|
210
233
|
newsletter?: ReactNode;
|
|
211
234
|
}): import("react").JSX.Element;
|
package/dist/marketing.js
CHANGED
|
@@ -13,13 +13,15 @@ function Flyout({ label, items, footer = [], layout = "list", onNavigate, }) {
|
|
|
13
13
|
const dataLayout = layout === "list" ? undefined : layout;
|
|
14
14
|
return (_jsxs(Popover, { className: "m-flyout", "data-layout": dataLayout, children: [_jsxs(PopoverButton, { className: "m-flyout-trigger", children: [label, _jsx(ChevronDown, {})] }), _jsxs(PopoverPanel, { anchor: "bottom", className: "m-flyout-panel", "data-layout": dataLayout, children: [_jsx("ul", { children: items.map((item, index) => (_jsx("li", { children: _jsxs(CloseButton, { as: KitLink, href: item.href, onClick: onNavigate, children: [item.icon && (_jsx("span", { className: "m-flyout-icon", "aria-hidden": "true", children: item.icon })), _jsxs("span", { className: "m-flyout-text", children: [_jsx("strong", { children: item.label }), item.description && _jsx("span", { children: item.description })] })] }) }, `${item.label}:${item.href}:${index}`))) }), footer.length > 0 && (_jsx("div", { className: "m-flyout-footer", children: footer.map((link, index) => (_jsx(CloseButton, { as: KitLink, href: link.href, onClick: onNavigate, children: link.label }, `${link.label}:${link.href}:${index}`))) }))] })] }));
|
|
15
15
|
}
|
|
16
|
-
export function MarketingHeader({ brand, homeHref = "/", logo, links, action, controls, layout = "inline", mobileMenu = "inline", className, id, }) {
|
|
16
|
+
export function MarketingHeader({ brand, tagline, homeHref = "/", logo, links, action, controls, layout = "inline", mobileMenu = "inline", className, id, }) {
|
|
17
17
|
const [open, setOpen] = useState(false);
|
|
18
18
|
// Generated so two headers on one page keep distinct, valid references.
|
|
19
19
|
const navId = useDomId("m-marketing-nav");
|
|
20
20
|
const panel = mobileMenu === "panel";
|
|
21
21
|
const close = () => setOpen(false);
|
|
22
|
-
const
|
|
22
|
+
const homeLink = (_jsxs(KitLink, { className: "m-brand", href: homeHref, children: [_jsx(BrandMark, { logo: logo }), brand] }));
|
|
23
|
+
// The tagline sits outside the link so the link's name stays the brand.
|
|
24
|
+
const brandLink = tagline ? (_jsxs("div", { className: "m-brand-lockup", children: [homeLink, _jsx("span", { className: "m-brand-tagline", children: tagline })] })) : (homeLink);
|
|
23
25
|
const menuButton = (_jsx(Button, { variant: "ghost", className: "m-menu-trigger", "aria-haspopup": panel ? "dialog" : undefined, "aria-expanded": open, "aria-controls": panel ? undefined : navId, onClick: () => setOpen(!open), children: "Menu" }));
|
|
24
26
|
const nav = (_jsx("nav", { id: navId, "aria-label": "Primary", "data-open": (open && !panel) || undefined, children: links.map((link, index) => "items" in link ? (_jsx(Flyout, { label: link.label, items: link.items, footer: link.footer, layout: link.layout, onNavigate: close }, `${link.label}:flyout:${index}`)) : (_jsx(KitLink, { href: link.href, onClick: close, children: link.label }, `${link.label}:${link.href}:${index}`))) }));
|
|
25
27
|
const actions = (controls || action) && (_jsxs(_Fragment, { children: [controls, action && _jsx(ActionLink, { ...action })] }));
|
|
@@ -29,8 +31,15 @@ export function MarketingHeader({ brand, homeHref = "/", logo, links, action, co
|
|
|
29
31
|
function PanelLinks({ links, onNavigate, }) {
|
|
30
32
|
return (_jsx("nav", { "aria-label": "Primary", className: "m-marketing-panel-nav", children: links.map((link, index) => "items" in link ? (_jsxs("details", { children: [_jsxs("summary", { children: [link.label, _jsx(ChevronDown, {})] }), [...link.items, ...(link.footer ?? [])].map((item, i) => (_jsx(KitLink, { href: item.href, onClick: onNavigate, children: item.label }, `${item.label}:${item.href}:${i}`)))] }, `${link.label}:group:${index}`)) : (_jsx(KitLink, { href: link.href, onClick: onNavigate, children: link.label }, `${link.label}:${link.href}:${index}`))) }));
|
|
31
33
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
/**
|
|
35
|
+
* A public page: the header, a main region that grows to fill the viewport, and
|
|
36
|
+
* the footer. Content is never clipped; a longer page simply scrolls.
|
|
37
|
+
*/
|
|
38
|
+
export function MarketingPage({ header, footer, children, align = "start", id, className, }) {
|
|
39
|
+
return (_jsxs("div", { id: id, className: cx("m-marketing-page", className), ...componentRoot("MarketingPage"), "data-align": align === "center" ? "center" : undefined, children: [header, _jsx("main", { className: "m-marketing-main", children: children }), footer] }));
|
|
40
|
+
}
|
|
41
|
+
export function Hero({ id, className, eyebrow, title, emphasis, description, actions = [], note, children, layout = "center", wide, headingLevel = 1, background, images = [], tone = "default", }) {
|
|
42
|
+
return (_jsxs("section", { id: id, className: cx("m-hero", `m-hero-${layout}`, className), ...componentRoot("Hero"), "data-wide": layout === "split" ? wide : undefined, "data-background": background ? true : undefined, "data-tone": background ? "inverse" : sectionTone(tone), children: [background && (_jsx("div", { className: "m-hero-background", "aria-hidden": "true", children: background })), _jsxs("div", { children: [_jsx(TitledIntro, { eyebrow: eyebrow, title: title, emphasis: emphasis, headingLevel: headingLevel }), _jsx("p", { className: "m-hero-description", children: description }), _jsx(ActionLinks, { actions: actions }), note && _jsx("div", { className: "m-hero-note", children: note })] }), images.length > 0 && (_jsx("div", { className: "m-hero-tiles", children: images.map((image, index) => (_jsx("div", { children: image }, index))) })), children && _jsx("div", { className: "m-hero-preview", children: children })] }));
|
|
34
43
|
}
|
|
35
44
|
export function FeatureGrid({ id, className, eyebrow, title, description, items, marker = "auto", columns, tone = "default", headingLevel = 2, }) {
|
|
36
45
|
const ItemHeading = headingTag(childLevel(headingLevel));
|
|
@@ -115,13 +124,17 @@ export function CallToAction({ id, className, title, description, actions, tone
|
|
|
115
124
|
const copy = (_jsxs(_Fragment, { children: [_jsx(TitledIntro, { title: title, description: description, descriptionClassName: null, headingLevel: headingLevel }), _jsx(ActionLinks, { actions: actions })] }));
|
|
116
125
|
return (_jsx("section", { id: id, className: cx("m-cta", className), ...componentRoot("CallToAction"), "data-tone": sectionTone(tone), "data-layout": media ? "split" : undefined, children: media ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "m-cta-copy", children: copy }), _jsx("div", { className: "m-cta-media", children: media })] })) : (copy) }));
|
|
117
126
|
}
|
|
118
|
-
export function Footer({ id, className, brand, logo, description, note, columns, copyright, legal = [], itemHeadingLevel = 2, social = [], layout = "columns", newsletter, }) {
|
|
127
|
+
export function Footer({ id, className, brand, logo, description, note, columns = [], copyright, legal = [], itemHeadingLevel = 2, social = [], layout = "columns", newsletter, }) {
|
|
119
128
|
const Heading = headingTag(itemHeadingLevel);
|
|
120
129
|
const center = layout === "center";
|
|
121
|
-
const
|
|
130
|
+
const inline = layout === "inline";
|
|
131
|
+
const identity = (_jsxs(_Fragment, { children: [_jsxs("strong", { className: "m-brand", children: [_jsx(BrandMark, { logo: logo }), brand] }), description && _jsx("p", { children: description }), note && _jsx("p", { children: note })] }));
|
|
122
132
|
const socialLinks = social.length > 0 && (_jsx(IconLinks, { label: "Social media", links: social }));
|
|
123
|
-
const navs = columns.map((column) => (_jsxs("nav", { "aria-label": column.label, children: [!center && _jsx(Heading, { children: column.label }), column.items.map((link, index) => (_jsx(KitLink, { href: link.href, children: link.label }, `${link.label}:${link.href}:${index}`)))] }, column.label)));
|
|
124
|
-
|
|
133
|
+
const navs = columns.map((column) => (_jsxs("nav", { "aria-label": column.label, children: [!center && !inline && _jsx(Heading, { children: column.label }), column.items.map((link, index) => (_jsx(KitLink, { href: link.href, children: link.label }, `${link.label}:${link.href}:${index}`)))] }, column.label)));
|
|
134
|
+
const legalLinks = (_jsx("div", { children: legal.map((link, index) => (_jsx(KitLink, { href: link.href, children: link.label }, `${link.label}:${link.href}:${index}`))) }));
|
|
135
|
+
if (inline)
|
|
136
|
+
return (_jsxs("footer", { id: id, className: cx("m-footer", className), ...componentRoot("Footer"), "data-layout": "inline", children: [_jsxs("div", { className: "m-footer-inline", children: [identity, navs, socialLinks, _jsx("span", { children: copyright }), legal.length > 0 && legalLinks] }), newsletter && _jsx("div", { className: "m-footer-newsletter", children: newsletter })] }));
|
|
137
|
+
return (_jsxs("footer", { id: id, className: cx("m-footer", className), ...componentRoot("Footer"), "data-layout": center ? "center" : undefined, children: [center ? (_jsxs("div", { className: "m-footer-center", children: [identity, navs, socialLinks] })) : (_jsxs("div", { className: "m-footer-grid", children: [_jsxs("div", { children: [identity, socialLinks] }), navs] })), newsletter && _jsx("div", { className: "m-footer-newsletter", children: newsletter }), _jsxs("div", { className: "m-footer-bottom", children: [_jsx("span", { children: copyright }), legalLinks] })] }));
|
|
125
138
|
}
|
|
126
139
|
/** Intro band for inner pages (about, pricing, careers), with optional links and stats. */
|
|
127
140
|
export function HeaderSection({ id, className, eyebrow, title, description, links = [], stats = [], tone = "default", align = "start", headingLevel = 1, }) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { useRef, type CSSProperties } from "react";
|
|
2
|
+
import { componentRoot, cx, useDomId, usePlayback } from "./internal.js";
|
|
3
|
+
|
|
4
|
+
/** A point in the drawing, in the illustration's viewBox units. */
|
|
5
|
+
export interface NetworkNode {
|
|
6
|
+
id: string;
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
/** Short text drawn under the node. */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** accent: the brand's accent fill, to pick out a node. */
|
|
12
|
+
tone?: "default" | "accent";
|
|
13
|
+
}
|
|
14
|
+
/** A straight connection between two node ids. */
|
|
15
|
+
export interface NetworkConnection {
|
|
16
|
+
from: string;
|
|
17
|
+
to: string;
|
|
18
|
+
/** Whether a signal travels from `from` to `to`. Default true. */
|
|
19
|
+
signal?: boolean;
|
|
20
|
+
/** Seconds for this signal to cross; defaults to the illustration's duration. */
|
|
21
|
+
duration?: number;
|
|
22
|
+
/** Seconds before this signal starts; defaults to an even stagger. */
|
|
23
|
+
delay?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const NODE_RADIUS = 8;
|
|
27
|
+
const SIGNAL_RADIUS = 4;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A lightweight network drawing: nodes and connections supplied as data, with
|
|
31
|
+
* optional signals travelling along the connections. Decorative unless it has
|
|
32
|
+
* a label. The animation is illustrative: it never represents live activity.
|
|
33
|
+
*/
|
|
34
|
+
export function NetworkIllustration({
|
|
35
|
+
nodes,
|
|
36
|
+
connections,
|
|
37
|
+
width = 480,
|
|
38
|
+
height = 320,
|
|
39
|
+
label,
|
|
40
|
+
description,
|
|
41
|
+
motion = "auto",
|
|
42
|
+
duration = 3,
|
|
43
|
+
paused,
|
|
44
|
+
onPausedChange,
|
|
45
|
+
id,
|
|
46
|
+
className,
|
|
47
|
+
}: {
|
|
48
|
+
nodes: NetworkNode[];
|
|
49
|
+
connections: NetworkConnection[];
|
|
50
|
+
/** Width of the viewBox; the drawing scales to its container. */
|
|
51
|
+
width?: number;
|
|
52
|
+
/** Height of the viewBox. */
|
|
53
|
+
height?: number;
|
|
54
|
+
/** Makes the drawing informative: an image with this accessible name. Omitted, it is decorative and hidden from assistive technology. */
|
|
55
|
+
label?: string;
|
|
56
|
+
/** With a label, a longer description of what the drawing shows. */
|
|
57
|
+
description?: string;
|
|
58
|
+
/** auto: signals travel where connections carry one, with a pause control. static: the same drawing without motion. */
|
|
59
|
+
motion?: "auto" | "static";
|
|
60
|
+
/** Seconds for a signal to cross a connection. */
|
|
61
|
+
duration?: number;
|
|
62
|
+
/** Whether the user has paused the animation; pass with onPausedChange to control it or share it between illustrations. */
|
|
63
|
+
paused?: boolean;
|
|
64
|
+
onPausedChange?: (paused: boolean) => void;
|
|
65
|
+
id?: string;
|
|
66
|
+
className?: string;
|
|
67
|
+
}) {
|
|
68
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
69
|
+
const titleId = useDomId("m-network-title");
|
|
70
|
+
const descId = useDomId("m-network-desc");
|
|
71
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
72
|
+
const links = connections.flatMap((connection) => {
|
|
73
|
+
const from = byId.get(connection.from);
|
|
74
|
+
const to = byId.get(connection.to);
|
|
75
|
+
return from && to ? [{ ...connection, a: from, b: to }] : [];
|
|
76
|
+
});
|
|
77
|
+
const carriers = links.filter((link) => link.signal !== false);
|
|
78
|
+
const animated = motion === "auto" && carriers.length > 0;
|
|
79
|
+
const { reason, reduced, userPaused, toggle } = usePlayback(ref, {
|
|
80
|
+
paused,
|
|
81
|
+
onPausedChange,
|
|
82
|
+
enabled: animated,
|
|
83
|
+
});
|
|
84
|
+
const informative = Boolean(label);
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
ref={ref}
|
|
88
|
+
id={id}
|
|
89
|
+
className={cx("m-network", className)}
|
|
90
|
+
{...componentRoot("NetworkIllustration")}
|
|
91
|
+
data-motion={motion === "static" ? "static" : undefined}
|
|
92
|
+
data-paused={animated ? (reason ?? undefined) : undefined}
|
|
93
|
+
>
|
|
94
|
+
<svg
|
|
95
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
96
|
+
{...(informative
|
|
97
|
+
? {
|
|
98
|
+
role: "img",
|
|
99
|
+
"aria-labelledby": titleId,
|
|
100
|
+
"aria-describedby": description ? descId : undefined,
|
|
101
|
+
}
|
|
102
|
+
: { "aria-hidden": true, focusable: false })}
|
|
103
|
+
>
|
|
104
|
+
{informative && <title id={titleId}>{label}</title>}
|
|
105
|
+
{informative && description && <desc id={descId}>{description}</desc>}
|
|
106
|
+
{links.map((link, index) => (
|
|
107
|
+
<line
|
|
108
|
+
key={`${link.from}:${link.to}:${index}`}
|
|
109
|
+
className="m-network-link"
|
|
110
|
+
x1={link.a.x}
|
|
111
|
+
y1={link.a.y}
|
|
112
|
+
x2={link.b.x}
|
|
113
|
+
y2={link.b.y}
|
|
114
|
+
/>
|
|
115
|
+
))}
|
|
116
|
+
{animated &&
|
|
117
|
+
!reduced &&
|
|
118
|
+
carriers.map((link, index) => {
|
|
119
|
+
const time = link.duration ?? duration;
|
|
120
|
+
const delay = link.delay ?? -((index * time) / carriers.length);
|
|
121
|
+
return (
|
|
122
|
+
<circle
|
|
123
|
+
key={`${link.from}:${link.to}:${index}`}
|
|
124
|
+
className="m-network-signal"
|
|
125
|
+
r={SIGNAL_RADIUS}
|
|
126
|
+
style={
|
|
127
|
+
{
|
|
128
|
+
"--m-signal-from": `${link.a.x}px ${link.a.y}px`,
|
|
129
|
+
"--m-signal-to": `${link.b.x}px ${link.b.y}px`,
|
|
130
|
+
animationDuration: `${time}s`,
|
|
131
|
+
animationDelay: delay ? `${delay}s` : undefined,
|
|
132
|
+
} as CSSProperties
|
|
133
|
+
}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
{nodes.map((node) => (
|
|
138
|
+
<circle
|
|
139
|
+
key={node.id}
|
|
140
|
+
className="m-network-node"
|
|
141
|
+
cx={node.x}
|
|
142
|
+
cy={node.y}
|
|
143
|
+
r={NODE_RADIUS}
|
|
144
|
+
data-tone={node.tone === "accent" ? "accent" : undefined}
|
|
145
|
+
/>
|
|
146
|
+
))}
|
|
147
|
+
{nodes.map(
|
|
148
|
+
(node) =>
|
|
149
|
+
node.label && (
|
|
150
|
+
<text
|
|
151
|
+
key={node.id}
|
|
152
|
+
className="m-network-label"
|
|
153
|
+
x={node.x}
|
|
154
|
+
y={node.y + NODE_RADIUS + 16}
|
|
155
|
+
textAnchor="middle"
|
|
156
|
+
>
|
|
157
|
+
{node.label}
|
|
158
|
+
</text>
|
|
159
|
+
),
|
|
160
|
+
)}
|
|
161
|
+
</svg>
|
|
162
|
+
{animated && !reduced && (
|
|
163
|
+
<button type="button" className="m-network-toggle" onClick={toggle}>
|
|
164
|
+
{userPaused ? "Play animation" : "Pause animation"}
|
|
165
|
+
</button>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -94,6 +94,7 @@ export type {
|
|
|
94
94
|
} from "./auth.js";
|
|
95
95
|
export {
|
|
96
96
|
MarketingHeader,
|
|
97
|
+
MarketingPage,
|
|
97
98
|
Hero,
|
|
98
99
|
FeatureGrid,
|
|
99
100
|
FeatureSection,
|
|
@@ -228,3 +229,5 @@ export type {
|
|
|
228
229
|
export { CommentComposer } from "./composer.js";
|
|
229
230
|
export type { LinkItem, ItemGroup } from "./navigation.js";
|
|
230
231
|
export type { HeadingLevel } from "./heading.js";
|
|
232
|
+
export { NetworkIllustration } from "./illustration.js";
|
|
233
|
+
export type { NetworkNode, NetworkConnection } from "./illustration.js";
|