@tonycodes-ui/mortar 0.0.0-stage → 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 +10 -0
- package/CHANGELOG.md +268 -0
- package/CONTRIBUTING.md +60 -0
- package/DESIGN.md +51 -0
- package/LICENSE +21 -0
- package/README.md +155 -2
- package/TESTING.md +83 -0
- package/baseline-policy.json +16 -0
- package/bin/mortar.mjs +258 -0
- package/catalog.json +9900 -0
- package/cli/audit.mjs +943 -0
- package/cli/data.mjs +176 -0
- package/cli/mcp.mjs +216 -0
- package/cli/migrations.mjs +128 -0
- package/cli/packet.mjs +267 -0
- package/cli/search.mjs +246 -0
- package/cli/tools.mjs +229 -0
- package/design-principles.json +30 -0
- package/dist/agent-data.json +1 -0
- package/dist/application.d.ts +340 -0
- package/dist/application.js +240 -0
- package/dist/auth.d.ts +121 -0
- package/dist/auth.js +97 -0
- package/dist/brand.d.ts +69 -0
- package/dist/brand.js +266 -0
- package/dist/calendar.d.ts +75 -0
- package/dist/calendar.js +310 -0
- package/dist/choice.d.ts +36 -0
- package/dist/choice.js +36 -0
- package/dist/composer.d.ts +28 -0
- package/dist/composer.js +47 -0
- package/dist/display.d.ts +136 -0
- package/dist/display.js +75 -0
- package/dist/docs.d.ts +145 -0
- package/dist/docs.js +141 -0
- package/dist/ecommerce.d.ts +261 -0
- package/dist/ecommerce.js +177 -0
- package/dist/feedback.d.ts +80 -0
- package/dist/feedback.js +98 -0
- package/dist/foundation.d.ts +42 -0
- package/dist/foundation.js +121 -0
- package/dist/heading.d.ts +5 -0
- package/dist/heading.js +9 -0
- package/dist/icons.d.ts +50 -0
- package/dist/icons.js +70 -0
- package/dist/illustration.d.ts +47 -0
- package/dist/illustration.js +47 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +18 -0
- package/dist/inspection.d.ts +24 -0
- package/dist/inspection.js +14 -0
- package/dist/internal.d.ts +514 -0
- package/dist/internal.js +332 -0
- package/dist/layout.d.ts +107 -0
- package/dist/layout.js +66 -0
- package/dist/marketing-sections.d.ts +161 -0
- package/dist/marketing-sections.js +58 -0
- package/dist/marketing.d.ts +277 -0
- package/dist/marketing.js +163 -0
- package/dist/navigation.d.ts +35 -0
- package/dist/navigation.js +44 -0
- package/dist/primitives.d.ts +258 -0
- package/dist/primitives.js +244 -0
- package/dist/showcase.d.ts +29 -0
- package/dist/showcase.js +10 -0
- package/dist/workspace.d.ts +80 -0
- package/dist/workspace.js +48 -0
- package/docs/agents.md +127 -0
- package/docs/auth-flows.md +71 -0
- package/docs/migrating-to-0.6.md +94 -0
- package/docs/theming.md +170 -0
- package/package.json +94 -4
- package/skills/mortar/SKILL.md +36 -0
- package/src/application.css +1487 -0
- package/src/application.tsx +1413 -0
- package/src/auth.tsx +452 -0
- package/src/brand.ts +335 -0
- package/src/calendar.css +468 -0
- package/src/calendar.tsx +868 -0
- package/src/choice.tsx +240 -0
- package/src/composer.tsx +158 -0
- package/src/data.css +690 -0
- package/src/display.tsx +452 -0
- package/src/docs.css +704 -0
- package/src/docs.tsx +737 -0
- package/src/ecommerce.css +810 -0
- package/src/ecommerce.tsx +1033 -0
- package/src/feedback.tsx +388 -0
- package/src/foundation.css +1867 -0
- package/src/foundation.tsx +227 -0
- package/src/heading.ts +13 -0
- package/src/icons.tsx +237 -0
- package/src/illustration.tsx +169 -0
- package/src/index.ts +233 -0
- package/src/inspection.tsx +91 -0
- package/src/internal.tsx +700 -0
- package/src/layout.tsx +342 -0
- package/src/marketing-sections.tsx +621 -0
- package/src/marketing.css +2579 -0
- package/src/marketing.tsx +1235 -0
- package/src/navigation.tsx +118 -0
- package/src/overlays.css +350 -0
- package/src/primitives.tsx +1135 -0
- package/src/showcase.tsx +120 -0
- package/src/tokens.css +449 -0
- package/src/workspace.tsx +333 -0
- package/styles.css +56 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { IconLinks, KitLink } from "./navigation.js";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { ActionLink, Button, Input } from "./primitives.js";
|
|
5
|
+
import { Avatar } from "./display.js";
|
|
6
|
+
import { childLevel, headingTag } from "./heading.js";
|
|
7
|
+
import { ActionLinks, TermList, TitledIntro, cx, componentRoot, sectionTone, } from "./internal.js";
|
|
8
|
+
export function BentoGrid({ id, className, eyebrow, title, description, items, tone = "default", headingLevel = 2, }) {
|
|
9
|
+
const ItemHeading = headingTag(childLevel(headingLevel));
|
|
10
|
+
return (_jsxs("section", { id: id, className: cx("m-marketing-section", className), "data-tone": sectionTone(tone), ...componentRoot("BentoGrid"), children: [_jsx(TitledIntro, { eyebrow: eyebrow, title: title, description: description, headingLevel: headingLevel }), _jsx("ul", { className: "m-bento", children: items.map((item, index) => (_jsx("li", { "data-span": item.span || 1, "data-row-span": item.rowSpan === 2 ? 2 : undefined, children: _jsxs("article", { "data-tone": sectionTone(item.tone), children: [item.visual && (_jsx("div", { className: "m-bento-visual", children: item.visual })), _jsx(ItemHeading, { children: item.title }), _jsx("p", { children: item.description })] }) }, `${item.title}:${index}`))) })] }));
|
|
11
|
+
}
|
|
12
|
+
export function StatGroup({ id, className, label, items, layout = "grid", background, }) {
|
|
13
|
+
const list = (_jsx(TermList, { id: background ? undefined : id, className: cx("m-stat-group", !background && className), "aria-label": label, "data-layout": layout === "grid" ? undefined : layout, ...componentRoot("StatGroup", !background), items: items.map((item) => ({
|
|
14
|
+
term: item.label,
|
|
15
|
+
details: item.value,
|
|
16
|
+
after: item.description && (_jsx("dd", { className: "m-stat-description", children: item.description })),
|
|
17
|
+
})) }));
|
|
18
|
+
if (!background)
|
|
19
|
+
return list;
|
|
20
|
+
return (_jsxs("div", { id: id, className: cx("m-stat-band", className), ...componentRoot("StatGroup"), "data-background": "true", "data-tone": "inverse", children: [_jsx("div", { className: "m-cover-background", "aria-hidden": "true", children: background }), list] }));
|
|
21
|
+
}
|
|
22
|
+
export function TestimonialGrid({ id, className, title, items, headingLevel = 2, layout = "masonry", }) {
|
|
23
|
+
const Heading = headingTag(headingLevel);
|
|
24
|
+
return (_jsxs("section", { id: id, className: cx("m-marketing-section", className), "aria-label": title ? undefined : "Testimonials", ...componentRoot("TestimonialGrid"), children: [title && _jsx(Heading, { className: "m-section-title", children: title }), _jsx("div", { className: "m-testimonial-grid", "data-layout": layout === "grid" ? "grid" : undefined, children: items.map((item) => (_jsxs("figure", { "data-featured": item.featured ? "true" : undefined, children: [_jsx("blockquote", { children: item.quote }), _jsxs("figcaption", { children: [_jsx(Avatar, { decorative: true, name: item.name, src: item.avatar, size: "sm" }), _jsxs("span", { children: [_jsx("strong", { children: item.name }), item.role && _jsx("span", { children: item.role })] })] })] }, item.name))) })] }));
|
|
25
|
+
}
|
|
26
|
+
export function PlanComparison({ id, className, title, description, plans, featuredPlan, groups, headingLevel = 2, }) {
|
|
27
|
+
const headingId = useId();
|
|
28
|
+
return (_jsxs("section", { id: id, className: cx("m-marketing-section", className), "aria-labelledby": headingId, ...componentRoot("PlanComparison"), children: [_jsx(TitledIntro, { id: headingId, title: title, description: description, headingLevel: headingLevel }), _jsx("div", { className: "m-table-scroll m-comparison", role: "region", "aria-label": `${title} table`, tabIndex: 0, children: _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", {}), plans.map((plan) => (_jsx("th", { scope: "col", "data-featured": plan === featuredPlan || undefined, children: plan }, plan)))] }) }), groups.map((group) => (_jsxs("tbody", { children: [_jsx("tr", { className: "m-comparison-group", children: _jsx("th", { scope: "colgroup", colSpan: plans.length + 1, children: group.label }) }), group.rows.map((row) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: row.feature }), row.values.map((value, i) => (_jsx("td", { "data-featured": plans[i] === featuredPlan || undefined, children: typeof value === "boolean" ? (
|
|
29
|
+
// The glyph is decoration; the cell's text is the meaning.
|
|
30
|
+
_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: value ? undefined : "m-comparison-no", children: value ? "✓" : "—" }), _jsx("span", { className: "m-sr-only", children: value ? "Included" : "Not included" })] })) : (value) }, plans[i])))] }, row.feature)))] }, group.label)))] }) })] }));
|
|
31
|
+
}
|
|
32
|
+
export function PostList({ id, className, title, description, posts, action, headingLevel = 2, itemHeadingLevel, }) {
|
|
33
|
+
const PostHeading = headingTag(itemHeadingLevel ?? childLevel(headingLevel));
|
|
34
|
+
return (_jsxs("section", { id: id, className: cx("m-marketing-section", className), ...componentRoot("PostList"), children: [_jsxs("div", { className: "m-section-heading-row", children: [_jsx("div", { children: _jsx(TitledIntro, { title: title, description: description, headingLevel: headingLevel }) }), action && _jsx(ActionLink, { ...action, variant: "secondary" })] }), _jsx("ul", { className: "m-post-list", children: posts.map((post, index) => (_jsx("li", { "data-featured": post.featured ? "true" : undefined, children: _jsxs("article", { children: [post.image && _jsx("div", { className: "m-post-image", children: post.image }), _jsxs("p", { className: "m-post-meta", children: [post.category && _jsx("span", { children: post.category }), _jsx("time", { dateTime: post.date, children: post.dateLabel })] }), _jsx(PostHeading, { className: "m-post-title", children: _jsx(KitLink, { href: post.href, children: post.title }) }), _jsx("p", { children: post.excerpt })] }) }, `${post.title}:${post.href}:${index}`))) })] }));
|
|
35
|
+
}
|
|
36
|
+
export function TeamGrid({ id, className, title, description, members, headingLevel = 2, layout = "grid", }) {
|
|
37
|
+
return (_jsxs("section", { id: id, className: cx("m-marketing-section", className), ...componentRoot("TeamGrid"), children: [_jsx(TitledIntro, { title: title, description: description, headingLevel: headingLevel }), _jsx("ul", { className: "m-team-grid", "data-layout": layout === "grid" ? undefined : layout, children: members.map((member) => (_jsxs("li", { children: [_jsx("div", { className: "m-team-portrait", children: member.image || (_jsx(Avatar, { decorative: true, name: member.name, size: "lg", shape: "square" })) }), _jsx("strong", { children: member.name }), _jsx("span", { children: member.role }), member.bio && _jsx("p", { className: "m-team-bio", children: member.bio }), member.links?.length ? (_jsx(IconLinks, { label: `${member.name} links`, links: member.links })) : null] }, member.name))) })] }));
|
|
38
|
+
}
|
|
39
|
+
export function ContactSection({ id, className, title, description, details = [], headingLevel = 2, media, offices = [], children, }) {
|
|
40
|
+
const OfficeHeading = headingTag(childLevel(headingLevel));
|
|
41
|
+
const intro = (_jsxs(_Fragment, { children: [_jsx(TitledIntro, { title: title, description: description, headingLevel: headingLevel }), details.length > 0 && (_jsx(TermList, { items: details.map((d) => ({ term: d.label, details: d.value })) }))] }));
|
|
42
|
+
const form = children && _jsx("div", { className: "m-contact-form", children: children });
|
|
43
|
+
return (_jsxs("section", { id: id, className: cx("m-contact", className), ...componentRoot("ContactSection"), "data-media": media ? "true" : undefined, children: [media ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "m-contact-media", children: media }), _jsxs("div", { className: "m-contact-body", children: [_jsx("div", { children: intro }), form] })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { children: intro }), form] })), offices.length > 0 && (_jsx("ul", { className: "m-contact-offices", children: offices.map((office, index) => (_jsxs("li", { children: [_jsx(OfficeHeading, { children: office.name }), _jsx("address", { children: office.address })] }, `${office.name}:${index}`))) }))] }));
|
|
44
|
+
}
|
|
45
|
+
export function Newsletter({ id, className, title, description, onSubmit, submitLabel = "Subscribe", status = "idle", message, headingLevel = 2, layout = "split", variant = "plain", }) {
|
|
46
|
+
const [email, setEmail] = useState("");
|
|
47
|
+
const submitting = status === "submitting";
|
|
48
|
+
const submit = (e) => {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
if (!submitting)
|
|
51
|
+
onSubmit(email);
|
|
52
|
+
};
|
|
53
|
+
return (_jsxs("section", { id: id, className: cx("m-newsletter", className), ...componentRoot("Newsletter"), "data-layout": layout === "split" ? undefined : layout, "data-variant": variant === "plain" ? undefined : variant, children: [_jsx("div", { children: _jsx(TitledIntro, { title: title, description: description, descriptionClassName: null, headingLevel: headingLevel }) }), _jsxs("form", { onSubmit: submit, "aria-busy": submitting || undefined, children: [_jsx(Input, { label: "Email address", type: "email", autoComplete: "email", required: true, value: email, onChange: (e) => setEmail(e.target.value), error: status === "error" ? message : undefined, disabled: submitting }), _jsx(Button, { type: "submit", loading: submitting, children: submitLabel }), _jsx("p", { role: "status", className: "m-newsletter-status", children: status === "success" ? message || "Thanks for subscribing." : "" })] })] }));
|
|
54
|
+
}
|
|
55
|
+
export function NotFound({ id, className, code = "404", title, description, actions, illustration, background, headingLevel = 1, }) {
|
|
56
|
+
const Heading = headingTag(headingLevel);
|
|
57
|
+
return (_jsxs("section", { id: id, className: cx("m-not-found", className), ...componentRoot("NotFound"), "data-background": background ? true : undefined, "data-tone": background ? "inverse" : undefined, children: [background && (_jsx("div", { className: "m-cover-background", "aria-hidden": "true", children: background })), illustration && (_jsx("div", { className: "m-not-found-art", "aria-hidden": "true", children: illustration })), _jsx("p", { className: "m-eyebrow", children: code }), _jsx(Heading, { className: "m-section-title", children: title }), _jsx("p", { children: description }), _jsx(ActionLinks, { actions: actions })] }));
|
|
58
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { type IconLink, type ItemGroup, type LinkItem } from "./navigation.js";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import { type LinkAction } from "./primitives.js";
|
|
4
|
+
import { type SectionTone } from "./layout.js";
|
|
5
|
+
import { type HeadingLevel } from "./heading.js";
|
|
6
|
+
export type FlyoutItem = Pick<LinkItem, "label" | "href" | "description" | "icon">;
|
|
7
|
+
/** A header link, or a labelled group that opens a flyout panel. */
|
|
8
|
+
export type HeaderLink = LinkAction | (ItemGroup<FlyoutItem> & {
|
|
9
|
+
footer?: LinkAction[];
|
|
10
|
+
/** list: one column. columns: a wider panel in two columns. full: as wide as the viewport allows, up to four columns. */
|
|
11
|
+
layout?: "list" | "columns" | "full";
|
|
12
|
+
});
|
|
13
|
+
export declare function MarketingHeader({ brand, tagline, homeHref, logo, links, action, controls, layout, mobileMenu, className, id, }: {
|
|
14
|
+
brand: string;
|
|
15
|
+
/** A short descriptor beside the wordmark, outside the home link. Hidden on narrow screens. */
|
|
16
|
+
tagline?: string;
|
|
17
|
+
logo?: ReactNode;
|
|
18
|
+
homeHref?: string;
|
|
19
|
+
/** Links, or groups of items that open a flyout panel. */
|
|
20
|
+
links: HeaderLink[];
|
|
21
|
+
action?: LinkAction;
|
|
22
|
+
controls?: ReactNode;
|
|
23
|
+
/** inline: brand, links and actions in one row. center: links at the start, the brand centred, actions at the end. */
|
|
24
|
+
layout?: "inline" | "center";
|
|
25
|
+
/** How narrow screens show the links. inline: the menu button expands them under the header. panel: it opens a full-screen dialog. */
|
|
26
|
+
mobileMenu?: "inline" | "panel";
|
|
27
|
+
className?: string;
|
|
28
|
+
id?: string;
|
|
29
|
+
}): import("react").JSX.Element;
|
|
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, }: {
|
|
46
|
+
id?: string;
|
|
47
|
+
className?: string;
|
|
48
|
+
eyebrow?: string;
|
|
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;
|
|
52
|
+
description: string;
|
|
53
|
+
actions?: LinkAction[];
|
|
54
|
+
/** Short supporting content below the actions, such as a note or a Notice. */
|
|
55
|
+
note?: ReactNode;
|
|
56
|
+
children?: ReactNode;
|
|
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). */
|
|
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";
|
|
61
|
+
headingLevel?: HeadingLevel;
|
|
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. */
|
|
63
|
+
background?: ReactNode;
|
|
64
|
+
/** Image tiles shown as a staggered collage beside (split) or below (center) the copy. Use instead of children; give each image its own alt text, or alt="" when decorative. */
|
|
65
|
+
images?: ReactNode[];
|
|
66
|
+
/** default: the page. accent: a full-bleed band of the brand's accent fill (its accent gradient when it defines one) with on-accent copy and actions; the preview keeps the page's colours. A background takes precedence. */
|
|
67
|
+
tone?: "default" | "accent";
|
|
68
|
+
}): import("react").JSX.Element;
|
|
69
|
+
export declare function FeatureGrid({ id, className, eyebrow, title, description, items, marker, columns, tone, headingLevel, }: {
|
|
70
|
+
id?: string;
|
|
71
|
+
className?: string;
|
|
72
|
+
eyebrow?: string;
|
|
73
|
+
title: string;
|
|
74
|
+
description?: string;
|
|
75
|
+
items: {
|
|
76
|
+
title: string;
|
|
77
|
+
description: string;
|
|
78
|
+
icon?: ReactNode;
|
|
79
|
+
}[];
|
|
80
|
+
/** auto: show icons when supplied, otherwise no marker. number: for sequences. */
|
|
81
|
+
marker?: "auto" | "icon" | "number" | "none";
|
|
82
|
+
/** Defaults to a balanced count for the number of items. */
|
|
83
|
+
columns?: 2 | 3 | 4;
|
|
84
|
+
tone?: SectionTone;
|
|
85
|
+
/** The section title's level; item titles sit one level below. */
|
|
86
|
+
headingLevel?: HeadingLevel;
|
|
87
|
+
}): import("react").JSX.Element;
|
|
88
|
+
export declare function FeatureSection({ id, className, eyebrow, title, description, details, children, reverse, tone, framed, offset, headingLevel, }: {
|
|
89
|
+
id?: string;
|
|
90
|
+
className?: string;
|
|
91
|
+
eyebrow?: string;
|
|
92
|
+
title: string;
|
|
93
|
+
description: string;
|
|
94
|
+
/** Supporting content under the description, such as a CheckList or actions. */
|
|
95
|
+
details?: ReactNode;
|
|
96
|
+
children: ReactNode;
|
|
97
|
+
reverse?: boolean;
|
|
98
|
+
tone?: SectionTone;
|
|
99
|
+
/** Wrap the preview in the sunken frame. Disable when the child brings its own frame. */
|
|
100
|
+
framed?: boolean;
|
|
101
|
+
/** Let a wide screenshot run past the section's end edge and crop there (wide screens only). */
|
|
102
|
+
offset?: boolean;
|
|
103
|
+
headingLevel?: HeadingLevel;
|
|
104
|
+
}): import("react").JSX.Element;
|
|
105
|
+
/** Numbered, connected sequence for marketing "how it works" sections. */
|
|
106
|
+
export declare function ProcessSteps({ id, className, eyebrow, title, description, items, tone, headingLevel, }: {
|
|
107
|
+
id?: string;
|
|
108
|
+
className?: string;
|
|
109
|
+
eyebrow?: string;
|
|
110
|
+
title: string;
|
|
111
|
+
description?: string;
|
|
112
|
+
items: {
|
|
113
|
+
title: string;
|
|
114
|
+
description: string;
|
|
115
|
+
}[];
|
|
116
|
+
tone?: SectionTone;
|
|
117
|
+
headingLevel?: HeadingLevel;
|
|
118
|
+
}): import("react").JSX.Element;
|
|
119
|
+
export declare function Testimonial({ id, className, quote, name, role, logo, avatar, }: {
|
|
120
|
+
id?: string;
|
|
121
|
+
className?: string;
|
|
122
|
+
quote: string;
|
|
123
|
+
name: string;
|
|
124
|
+
role?: string;
|
|
125
|
+
/** Company logo above the quote; give the image its company name as alt text. */
|
|
126
|
+
logo?: ReactNode;
|
|
127
|
+
/** Photo or avatar beside the name (decorative). */
|
|
128
|
+
avatar?: ReactNode;
|
|
129
|
+
}): import("react").JSX.Element;
|
|
130
|
+
export declare function LogoCloud({ id, className, label, items, layout, title, description, actions, headingLevel, }: {
|
|
131
|
+
id?: string;
|
|
132
|
+
className?: string;
|
|
133
|
+
label: string;
|
|
134
|
+
items: {
|
|
135
|
+
name: string;
|
|
136
|
+
image?: string;
|
|
137
|
+
}[];
|
|
138
|
+
/** inline: a centred row. grid: logos in bordered tiles. split: copy beside a logo grid. */
|
|
139
|
+
layout?: "inline" | "grid" | "split";
|
|
140
|
+
/** Split layout heading; the label still names the section. */
|
|
141
|
+
title?: string;
|
|
142
|
+
/** Split layout supporting copy. */
|
|
143
|
+
description?: string;
|
|
144
|
+
/** Split layout links, e.g. a case-studies link. */
|
|
145
|
+
actions?: LinkAction[];
|
|
146
|
+
/** Level of the split layout's title. */
|
|
147
|
+
headingLevel?: HeadingLevel;
|
|
148
|
+
}): import("react").JSX.Element;
|
|
149
|
+
export interface PricingPlan {
|
|
150
|
+
id: string;
|
|
151
|
+
name: string;
|
|
152
|
+
description: string;
|
|
153
|
+
monthly: string;
|
|
154
|
+
annual?: string;
|
|
155
|
+
unit?: string;
|
|
156
|
+
annualUnit?: string;
|
|
157
|
+
features: string[];
|
|
158
|
+
action: LinkAction;
|
|
159
|
+
featured?: boolean;
|
|
160
|
+
availability?: "available" | "coming-soon" | "unavailable";
|
|
161
|
+
availabilityLabel?: string;
|
|
162
|
+
}
|
|
163
|
+
/** A full-width tier band under the pricing plans. */
|
|
164
|
+
export interface PricingExtra {
|
|
165
|
+
name: string;
|
|
166
|
+
description: string;
|
|
167
|
+
/** Optional price, e.g. "$9" or "Custom". */
|
|
168
|
+
price?: string;
|
|
169
|
+
unit?: string;
|
|
170
|
+
action: LinkAction;
|
|
171
|
+
}
|
|
172
|
+
export declare function Pricing({ id, className, title, description, plans, tone, billingToggle, layout, featuresLabel, headingLevel, extra, }: {
|
|
173
|
+
id?: string;
|
|
174
|
+
className?: string;
|
|
175
|
+
title: string;
|
|
176
|
+
description?: string;
|
|
177
|
+
plans: PricingPlan[];
|
|
178
|
+
/** Applied only to featured cards. Other plans keep the page surface. */
|
|
179
|
+
tone?: SectionTone;
|
|
180
|
+
billingToggle?: boolean;
|
|
181
|
+
/** cards: side-by-side tiers. detailed: one full-width row per plan, features beside a price panel (single-price pages). */
|
|
182
|
+
layout?: "cards" | "detailed";
|
|
183
|
+
/** Heading above the feature list in the detailed layout. */
|
|
184
|
+
featuresLabel?: string;
|
|
185
|
+
/** The section title's level; plan names sit one level below. */
|
|
186
|
+
headingLevel?: HeadingLevel;
|
|
187
|
+
/** A full-width tier band under the plans, such as an enterprise or discount row. */
|
|
188
|
+
extra?: PricingExtra;
|
|
189
|
+
}): import("react").JSX.Element;
|
|
190
|
+
export declare function FAQ({ id, className, title, description, items, layout, headingLevel, }: {
|
|
191
|
+
id?: string;
|
|
192
|
+
className?: string;
|
|
193
|
+
title: string;
|
|
194
|
+
description?: ReactNode;
|
|
195
|
+
items: {
|
|
196
|
+
question: string;
|
|
197
|
+
answer: ReactNode;
|
|
198
|
+
}[];
|
|
199
|
+
/** split: heading beside an accordion. center: heading above a narrow accordion. grid: every answer visible in columns. */
|
|
200
|
+
layout?: "split" | "center" | "grid";
|
|
201
|
+
headingLevel?: HeadingLevel;
|
|
202
|
+
}): import("react").JSX.Element;
|
|
203
|
+
export declare function CallToAction({ id, className, title, description, actions, tone, media, headingLevel, }: {
|
|
204
|
+
id?: string;
|
|
205
|
+
className?: string;
|
|
206
|
+
title: string;
|
|
207
|
+
description?: string;
|
|
208
|
+
actions: LinkAction[];
|
|
209
|
+
/** default: the page surface. muted: the sunken band. inverse: the inverse token remap. accent: the brand's accent fill (its accent gradient when it defines one) with on-accent copy and actions. */
|
|
210
|
+
tone?: SectionTone | "accent";
|
|
211
|
+
/** Image or screenshot beside the copy (promo layout). */
|
|
212
|
+
media?: ReactNode;
|
|
213
|
+
headingLevel?: HeadingLevel;
|
|
214
|
+
}): import("react").JSX.Element;
|
|
215
|
+
export declare function Footer({ id, className, brand, logo, description, note, columns, copyright, legal, itemHeadingLevel, social, layout, newsletter, }: {
|
|
216
|
+
id?: string;
|
|
217
|
+
className?: string;
|
|
218
|
+
brand: string;
|
|
219
|
+
logo?: ReactNode;
|
|
220
|
+
description?: string;
|
|
221
|
+
note?: ReactNode;
|
|
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>[];
|
|
224
|
+
copyright: string;
|
|
225
|
+
legal?: LinkAction[];
|
|
226
|
+
/** Level of the column headings. */
|
|
227
|
+
itemHeadingLevel?: HeadingLevel;
|
|
228
|
+
/** Social profiles, usually icon-only; each label names its link. */
|
|
229
|
+
social?: IconLink[];
|
|
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";
|
|
232
|
+
/** A signup row above the copyright, usually a Newsletter; the footer removes its section padding. */
|
|
233
|
+
newsletter?: ReactNode;
|
|
234
|
+
}): import("react").JSX.Element;
|
|
235
|
+
/** Intro band for inner pages (about, pricing, careers), with optional links and stats. */
|
|
236
|
+
export declare function HeaderSection({ id, className, eyebrow, title, description, links, stats, tone, align, headingLevel, }: {
|
|
237
|
+
id?: string;
|
|
238
|
+
className?: string;
|
|
239
|
+
eyebrow?: string;
|
|
240
|
+
title: string;
|
|
241
|
+
description?: string;
|
|
242
|
+
links?: LinkAction[];
|
|
243
|
+
stats?: {
|
|
244
|
+
label: string;
|
|
245
|
+
value: string;
|
|
246
|
+
}[];
|
|
247
|
+
tone?: SectionTone;
|
|
248
|
+
align?: "start" | "center";
|
|
249
|
+
headingLevel?: HeadingLevel;
|
|
250
|
+
}): import("react").JSX.Element;
|
|
251
|
+
/** Long-form copy with optional media beside it, e.g. an about-page story. */
|
|
252
|
+
export declare function ContentSection({ id, className, eyebrow, title, description, media, mediaPosition, tone, headingLevel, stats, testimonial, children, }: {
|
|
253
|
+
id?: string;
|
|
254
|
+
className?: string;
|
|
255
|
+
eyebrow?: string;
|
|
256
|
+
title: string;
|
|
257
|
+
description?: string;
|
|
258
|
+
/** Image, figure or illustration shown beside the copy. */
|
|
259
|
+
media?: ReactNode;
|
|
260
|
+
mediaPosition?: "start" | "end";
|
|
261
|
+
tone?: SectionTone;
|
|
262
|
+
headingLevel?: HeadingLevel;
|
|
263
|
+
/** Figures under the copy, shown as a StatGroup named by the title. */
|
|
264
|
+
stats?: {
|
|
265
|
+
label: string;
|
|
266
|
+
value: string;
|
|
267
|
+
description?: string;
|
|
268
|
+
}[];
|
|
269
|
+
/** A customer quote under the copy, shown as a Testimonial. */
|
|
270
|
+
testimonial?: {
|
|
271
|
+
quote: string;
|
|
272
|
+
name: string;
|
|
273
|
+
role?: string;
|
|
274
|
+
avatar?: ReactNode;
|
|
275
|
+
};
|
|
276
|
+
children?: ReactNode;
|
|
277
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { IconLinks, KitLink, } from "./navigation.js";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { CloseButton, DialogPanel, DialogTitle, Popover, PopoverButton, PopoverPanel, } from "@headlessui/react";
|
|
5
|
+
import { ActionLink, Button } from "./primitives.js";
|
|
6
|
+
import { BrandMark, Prose } from "./layout.js";
|
|
7
|
+
import { SegmentedControl } from "./inspection.js";
|
|
8
|
+
import { ChevronDown } from "./icons.js";
|
|
9
|
+
import { StatGroup } from "./marketing-sections.js";
|
|
10
|
+
import { childLevel, headingTag } from "./heading.js";
|
|
11
|
+
import { ActionLinks, DialogFrame, TermList, TitledIntro, cx, componentRoot, titledIntroSlots, sectionTone, useDomId, } from "./internal.js";
|
|
12
|
+
function Flyout({ label, items, footer = [], layout = "list", onNavigate, }) {
|
|
13
|
+
const dataLayout = layout === "list" ? undefined : layout;
|
|
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
|
+
}
|
|
16
|
+
export function MarketingHeader({ brand, tagline, homeHref = "/", logo, links, action, controls, layout = "inline", mobileMenu = "inline", className, id, }) {
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
// Generated so two headers on one page keep distinct, valid references.
|
|
19
|
+
const navId = useDomId("m-marketing-nav");
|
|
20
|
+
const panel = mobileMenu === "panel";
|
|
21
|
+
const close = () => setOpen(false);
|
|
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);
|
|
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" }));
|
|
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}`))) }));
|
|
27
|
+
const actions = (controls || action) && (_jsxs(_Fragment, { children: [controls, action && _jsx(ActionLink, { ...action })] }));
|
|
28
|
+
return (_jsxs("header", { id: id, className: cx("m-marketing-header", className), ...componentRoot("MarketingHeader"), "data-layout": layout === "center" ? "center" : undefined, "data-menu": panel ? "panel" : undefined, children: [layout === "center" ? (_jsxs(_Fragment, { children: [menuButton, nav, brandLink] })) : (_jsxs(_Fragment, { children: [brandLink, menuButton, nav] })), _jsx("div", { className: "m-actions", children: actions }), panel && (_jsx(DialogFrame, { open: open, onClose: close, children: _jsxs(DialogPanel, { className: "m-marketing-panel", children: [_jsxs("div", { className: "m-dialog-heading", children: [_jsxs(DialogTitle, { className: "m-brand", children: [_jsx(BrandMark, { logo: logo }), brand] }), _jsx(Button, { variant: "ghost", onClick: close, children: "Close menu" })] }), _jsx(PanelLinks, { links: links, onNavigate: close }), actions && _jsx("div", { className: "m-actions", children: actions })] }) }))] }));
|
|
29
|
+
}
|
|
30
|
+
/** The header's links in the full-screen mobile panel; flyout groups become disclosures. */
|
|
31
|
+
function PanelLinks({ links, onNavigate, }) {
|
|
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}`))) }));
|
|
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 })] }));
|
|
43
|
+
}
|
|
44
|
+
export function FeatureGrid({ id, className, eyebrow, title, description, items, marker = "auto", columns, tone = "default", headingLevel = 2, }) {
|
|
45
|
+
const ItemHeading = headingTag(childLevel(headingLevel));
|
|
46
|
+
const cols = columns ??
|
|
47
|
+
(items.length === 2 || items.length === 4
|
|
48
|
+
? 2
|
|
49
|
+
: items.length % 3 === 0 || items.length > 4
|
|
50
|
+
? 3
|
|
51
|
+
: Math.min(items.length, 3));
|
|
52
|
+
return (_jsxs("section", { id: id, className: cx("m-marketing-section", className), "data-tone": sectionTone(tone), ...componentRoot("FeatureGrid"), children: [...titledIntroSlots({ eyebrow, title, description, headingLevel }), _jsx("div", { className: "m-feature-grid", "data-columns": cols, children: items.map((item, i) => {
|
|
53
|
+
const content = marker === "number"
|
|
54
|
+
? String(i + 1).padStart(2, "0")
|
|
55
|
+
: marker !== "none"
|
|
56
|
+
? item.icon
|
|
57
|
+
: null;
|
|
58
|
+
return (_jsxs("article", { children: [content && (_jsx("span", { className: "m-feature-icon", "data-marker": marker === "number" ? "number" : "icon", "aria-hidden": "true", children: content })), _jsx(ItemHeading, { children: item.title }), _jsx("p", { children: item.description })] }, item.title));
|
|
59
|
+
}) })] }));
|
|
60
|
+
}
|
|
61
|
+
export function FeatureSection({ id, className, eyebrow, title, description, details, children, reverse = false, tone = "default", framed = true, offset = false, headingLevel = 2, }) {
|
|
62
|
+
const [eyebrowSlot, headingSlot] = titledIntroSlots({
|
|
63
|
+
eyebrow,
|
|
64
|
+
title,
|
|
65
|
+
headingLevel,
|
|
66
|
+
});
|
|
67
|
+
return (_jsxs("section", { id: id, className: cx("m-feature-section", className), "data-reverse": reverse || undefined, "data-offset": offset || undefined, "data-tone": sectionTone(tone), ...componentRoot("FeatureSection"), children: [_jsxs("div", { children: [eyebrowSlot, headingSlot, _jsx("p", { children: description }), details && _jsx("div", { className: "m-feature-details", children: details })] }), _jsx("div", { className: "m-feature-preview", "data-framed": framed || undefined, children: children })] }));
|
|
68
|
+
}
|
|
69
|
+
/** Numbered, connected sequence for marketing "how it works" sections. */
|
|
70
|
+
export function ProcessSteps({ id, className, eyebrow, title, description, items, tone = "default", headingLevel = 2, }) {
|
|
71
|
+
const ItemHeading = headingTag(childLevel(headingLevel));
|
|
72
|
+
return (_jsxs("section", { id: id, className: cx("m-marketing-section", className), "data-tone": sectionTone(tone), ...componentRoot("ProcessSteps"), children: [...titledIntroSlots({ eyebrow, title, description, headingLevel }), _jsx("ol", { className: "m-process-steps", style: { "--m-steps": items.length }, children: items.map((item, i) => (_jsxs("li", { children: [_jsx("span", { className: "m-process-number", "aria-hidden": "true", children: i + 1 }), _jsx(ItemHeading, { children: item.title }), _jsx("p", { children: item.description })] }, `${item.title}:${i}`))) })] }));
|
|
73
|
+
}
|
|
74
|
+
export function Testimonial({ id, className, quote, name, role, logo, avatar, }) {
|
|
75
|
+
const person = (_jsxs(_Fragment, { children: [_jsx("strong", { children: name }), role && _jsx("span", { children: role })] }));
|
|
76
|
+
return (_jsxs("figure", { id: id, className: cx("m-testimonial", className), ...componentRoot("Testimonial"), children: [logo && _jsx("div", { className: "m-testimonial-logo", children: logo }), _jsx("blockquote", { children: quote }), _jsx("figcaption", { children: avatar ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "m-testimonial-avatar", "aria-hidden": "true", children: avatar }), _jsx("span", { className: "m-testimonial-person", children: person })] })) : (person) })] }));
|
|
77
|
+
}
|
|
78
|
+
export function LogoCloud({ id, className, label, items, layout = "inline", title, description, actions = [], headingLevel = 2, }) {
|
|
79
|
+
const Heading = headingTag(headingLevel);
|
|
80
|
+
const list = (_jsx("ul", { children: items.map((item) => (_jsx("li", { children: item.image ? (_jsx("img", { src: item.image, alt: item.name })) : (_jsx("span", { children: item.name })) }, item.name))) }));
|
|
81
|
+
if (layout === "split") {
|
|
82
|
+
return (_jsxs("section", { id: id, className: cx("m-logo-cloud", className), "aria-label": label, ...componentRoot("LogoCloud"), "data-layout": "split", children: [_jsxs("div", { className: "m-logo-cloud-copy", children: [title ? (_jsx(Heading, { className: "m-section-title", children: title })) : (_jsx("p", { children: label })), description && (_jsx("p", { className: "m-section-description", children: description })), actions.length > 0 && _jsx(ActionLinks, { actions: actions })] }), list] }));
|
|
83
|
+
}
|
|
84
|
+
return (_jsxs("section", { id: id, className: cx("m-logo-cloud", className), "aria-label": label, ...componentRoot("LogoCloud"), "data-layout": layout === "grid" ? "grid" : undefined, children: [_jsx("p", { children: label }), list] }));
|
|
85
|
+
}
|
|
86
|
+
export function Pricing({ id, className, title, description, plans, tone = "default", billingToggle, layout = "cards", featuresLabel = "What’s included", headingLevel = 2, extra, }) {
|
|
87
|
+
const [annualChoice, setAnnual] = useState(true);
|
|
88
|
+
const showToggle = billingToggle ??
|
|
89
|
+
(plans.length > 0 && plans.every((plan) => Boolean(plan.annual)));
|
|
90
|
+
// Annual prices show only while the toggle that switches them is visible,
|
|
91
|
+
// so the page never mixes units the visitor cannot change.
|
|
92
|
+
const annual = showToggle && annualChoice;
|
|
93
|
+
const PlanHeading = headingTag(childLevel(headingLevel));
|
|
94
|
+
return (_jsxs("section", { id: id, className: cx("m-marketing-section m-pricing", className), ...componentRoot("Pricing"), children: [_jsx(TitledIntro, { title: title, description: description, headingLevel: headingLevel }), showToggle && (_jsx(SegmentedControl, { label: "Billing interval", value: annual ? "annual" : "monthly", options: [
|
|
95
|
+
{ value: "annual", label: "Annual" },
|
|
96
|
+
{ value: "monthly", label: "Monthly" },
|
|
97
|
+
], onChange: (value) => setAnnual(value === "annual") })), _jsxs("div", { className: "m-pricing-grid", "data-layout": layout === "detailed" ? "detailed" : undefined, "data-columns": layout === "cards" && plans.length === 4 ? 4 : undefined, children: [plans.map((plan) => {
|
|
98
|
+
const availability = plan.availability ?? "available";
|
|
99
|
+
const blocked = availability !== "available";
|
|
100
|
+
const showAnnual = annual && Boolean(plan.annual);
|
|
101
|
+
const suffix = showAnnual ? plan.annualUnit : plan.unit;
|
|
102
|
+
const price = (_jsxs("div", { className: "m-price", children: [_jsx("strong", { children: showAnnual ? plan.annual : plan.monthly }), suffix && _jsx("span", { children: suffix })] }));
|
|
103
|
+
const features = (_jsx("ul", { children: plan.features.map((feature) => (_jsx("li", { children: feature }, feature))) }));
|
|
104
|
+
// Static text, not a live region: nothing changes after render.
|
|
105
|
+
const purchase = blocked ? (_jsx("p", { className: "m-pricing-availability", children: plan.availabilityLabel ??
|
|
106
|
+
(availability === "coming-soon"
|
|
107
|
+
? "Coming soon"
|
|
108
|
+
: "Unavailable") })) : (_jsx(ActionLink, { ...plan.action, variant: layout === "detailed" ? "primary" : "secondary" }));
|
|
109
|
+
return (_jsx("article", { "data-featured": plan.featured || undefined, "data-tone": plan.featured ? sectionTone(tone) : undefined, "data-availability": blocked ? availability : undefined, children: layout === "detailed" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "m-pricing-detail", children: [_jsx(PlanHeading, { children: plan.name }), _jsx("p", { children: plan.description }), _jsx("p", { className: "m-pricing-features-label", children: featuresLabel }), features] }), _jsxs("div", { className: "m-pricing-offer", children: [price, purchase] })] })) : (_jsxs(_Fragment, { children: [_jsx(PlanHeading, { children: plan.name }), _jsx("p", { children: plan.description }), price, features, purchase] })) }, plan.id));
|
|
110
|
+
}), extra && (_jsxs("article", { className: "m-pricing-extra", children: [_jsxs("div", { children: [_jsx(PlanHeading, { children: extra.name }), _jsx("p", { children: extra.description })] }), extra.price && (_jsxs("div", { className: "m-price", children: [_jsx("strong", { children: extra.price }), extra.unit && _jsx("span", { children: extra.unit })] })), _jsx(ActionLink, { ...extra.action, variant: "secondary" })] }))] })] }));
|
|
111
|
+
}
|
|
112
|
+
export function FAQ({ id, className, title, description, items, layout = "split", headingLevel = 2, }) {
|
|
113
|
+
const Heading = headingTag(headingLevel);
|
|
114
|
+
const head = (_jsxs("div", { children: [_jsx(Heading, { className: "m-section-title", children: title }), description && _jsx("div", { children: description })] }));
|
|
115
|
+
if (layout === "grid") {
|
|
116
|
+
return (_jsxs("section", { id: id, className: cx("m-faq", className), ...componentRoot("FAQ"), "data-layout": "grid", children: [head, _jsx(TermList, { className: "m-faq-grid", items: items.map((item) => ({
|
|
117
|
+
term: item.question,
|
|
118
|
+
details: item.answer,
|
|
119
|
+
})) })] }));
|
|
120
|
+
}
|
|
121
|
+
return (_jsxs("section", { id: id, className: cx("m-faq", className), ...componentRoot("FAQ"), "data-layout": layout === "center" ? "center" : undefined, children: [head, _jsx("div", { children: items.map((item) => (_jsxs("details", { children: [_jsxs("summary", { children: [item.question, _jsx("span", { "aria-hidden": "true", children: "+" })] }), _jsx("div", { children: item.answer })] }, item.question))) })] }));
|
|
122
|
+
}
|
|
123
|
+
export function CallToAction({ id, className, title, description, actions, tone = "default", media, headingLevel = 2, }) {
|
|
124
|
+
const copy = (_jsxs(_Fragment, { children: [_jsx(TitledIntro, { title: title, description: description, descriptionClassName: null, headingLevel: headingLevel }), _jsx(ActionLinks, { actions: actions })] }));
|
|
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) }));
|
|
126
|
+
}
|
|
127
|
+
export function Footer({ id, className, brand, logo, description, note, columns = [], copyright, legal = [], itemHeadingLevel = 2, social = [], layout = "columns", newsletter, }) {
|
|
128
|
+
const Heading = headingTag(itemHeadingLevel);
|
|
129
|
+
const center = layout === "center";
|
|
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 })] }));
|
|
132
|
+
const socialLinks = social.length > 0 && (_jsx(IconLinks, { label: "Social media", links: social }));
|
|
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] })] }));
|
|
138
|
+
}
|
|
139
|
+
/** Intro band for inner pages (about, pricing, careers), with optional links and stats. */
|
|
140
|
+
export function HeaderSection({ id, className, eyebrow, title, description, links = [], stats = [], tone = "default", align = "start", headingLevel = 1, }) {
|
|
141
|
+
const headingId = useId();
|
|
142
|
+
return (_jsxs("section", { id: id, className: cx("m-marketing-section m-header-section", className), "data-tone": sectionTone(tone), "data-align": align === "start" ? undefined : align, "aria-labelledby": headingId, ...componentRoot("HeaderSection"), children: [...titledIntroSlots({
|
|
143
|
+
eyebrow,
|
|
144
|
+
title,
|
|
145
|
+
description,
|
|
146
|
+
id: headingId,
|
|
147
|
+
headingLevel,
|
|
148
|
+
}), links.length > 0 && (_jsx("div", { className: "m-header-links", children: links.map((link, index) => (_jsxs(KitLink, { href: link.href, children: [link.label, _jsx("span", { "aria-hidden": "true", children: " \u2192" })] }, `${link.label}:${link.href}:${index}`))) })), stats.length > 0 && (_jsx(TermList, { className: "m-header-stats", items: stats.map((stat) => ({
|
|
149
|
+
term: stat.label,
|
|
150
|
+
details: stat.value,
|
|
151
|
+
})) }))] }));
|
|
152
|
+
}
|
|
153
|
+
/** Long-form copy with optional media beside it, e.g. an about-page story. */
|
|
154
|
+
export function ContentSection({ id, className, eyebrow, title, description, media, mediaPosition = "end", tone = "default", headingLevel = 2, stats = [], testimonial, children, }) {
|
|
155
|
+
const headingId = useId();
|
|
156
|
+
return (_jsxs("section", { id: id, className: cx("m-marketing-section m-content-section", className), "data-tone": sectionTone(tone), "data-media": media ? mediaPosition : undefined, "aria-labelledby": headingId, ...componentRoot("ContentSection"), children: [_jsxs("div", { className: "m-content-copy", children: [...titledIntroSlots({
|
|
157
|
+
eyebrow,
|
|
158
|
+
title,
|
|
159
|
+
description,
|
|
160
|
+
id: headingId,
|
|
161
|
+
headingLevel,
|
|
162
|
+
}), children && _jsx(Prose, { children: children }), testimonial && (_jsx(Testimonial, { className: "m-content-quote", ...testimonial })), stats.length > 0 && (_jsx(StatGroup, { className: "m-content-stats", label: title, items: stats }))] }), media && _jsx("figure", { className: "m-content-media", children: media })] }));
|
|
163
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ReactNode, type AnchorHTMLAttributes, type MouseEvent } from "react";
|
|
2
|
+
/** Return false to leave a route to the browser; otherwise the app owns navigation. */
|
|
3
|
+
export type Navigate = (href: string) => void | false;
|
|
4
|
+
export declare const NavigationContext: import("react").Context<Navigate | undefined>;
|
|
5
|
+
export declare function followLink(event: MouseEvent<HTMLAnchorElement>, navigate?: Navigate): void;
|
|
6
|
+
/** Internal shared anchor: retains native link semantics unless an app router handles it. */
|
|
7
|
+
export declare function KitLink({ onClick, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>): import("react").JSX.Element;
|
|
8
|
+
/** A navigation link: sidebar and navbar items, page tabs and flyout entries. */
|
|
9
|
+
export interface LinkItem {
|
|
10
|
+
label: string;
|
|
11
|
+
href: string;
|
|
12
|
+
/** The page being shown; marked aria-current="page". */
|
|
13
|
+
current?: boolean;
|
|
14
|
+
/** Icon shown before the label. */
|
|
15
|
+
icon?: ReactNode;
|
|
16
|
+
/** Shown after the label, e.g. a number of items. */
|
|
17
|
+
count?: number | string;
|
|
18
|
+
description?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Items gathered under a visible group label. */
|
|
21
|
+
export interface ItemGroup<T> {
|
|
22
|
+
label: string;
|
|
23
|
+
items: T[];
|
|
24
|
+
}
|
|
25
|
+
export interface IconLink {
|
|
26
|
+
/** Accessible name; also the visible text when there is no icon. */
|
|
27
|
+
label: string;
|
|
28
|
+
href: string;
|
|
29
|
+
icon?: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
/** Labelled list of profile or social links; icon links are named by their label. */
|
|
32
|
+
export declare function IconLinks({ label, links, }: {
|
|
33
|
+
label: string;
|
|
34
|
+
links: IconLink[];
|
|
35
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, } from "react";
|
|
3
|
+
export const NavigationContext = createContext(undefined);
|
|
4
|
+
export function followLink(event, navigate) {
|
|
5
|
+
const anchor = event.currentTarget;
|
|
6
|
+
if (!navigate ||
|
|
7
|
+
event.defaultPrevented ||
|
|
8
|
+
event.button !== 0 ||
|
|
9
|
+
event.metaKey ||
|
|
10
|
+
event.ctrlKey ||
|
|
11
|
+
event.shiftKey ||
|
|
12
|
+
event.altKey ||
|
|
13
|
+
anchor.hasAttribute("download") ||
|
|
14
|
+
(anchor.target && anchor.target !== "_self") ||
|
|
15
|
+
anchor.rel.split(/\s+/).includes("external"))
|
|
16
|
+
return;
|
|
17
|
+
const raw = anchor.getAttribute("href");
|
|
18
|
+
if (!raw || raw.startsWith("#"))
|
|
19
|
+
return;
|
|
20
|
+
const destination = new URL(anchor.href);
|
|
21
|
+
const current = new URL(anchor.ownerDocument.URL);
|
|
22
|
+
if (!["http:", "https:"].includes(destination.protocol) ||
|
|
23
|
+
destination.origin !== current.origin)
|
|
24
|
+
return;
|
|
25
|
+
if (destination.pathname === current.pathname &&
|
|
26
|
+
destination.search === current.search &&
|
|
27
|
+
destination.hash)
|
|
28
|
+
return;
|
|
29
|
+
if (navigate(destination.pathname + destination.search + destination.hash) !==
|
|
30
|
+
false)
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
}
|
|
33
|
+
/** Internal shared anchor: retains native link semantics unless an app router handles it. */
|
|
34
|
+
export function KitLink({ onClick, ...props }) {
|
|
35
|
+
const navigate = useContext(NavigationContext);
|
|
36
|
+
return (_jsx("a", { ...props, onClick: (event) => {
|
|
37
|
+
onClick?.(event);
|
|
38
|
+
followLink(event, navigate);
|
|
39
|
+
} }));
|
|
40
|
+
}
|
|
41
|
+
/** Labelled list of profile or social links; icon links are named by their label. */
|
|
42
|
+
export function IconLinks({ label, links, }) {
|
|
43
|
+
return (_jsx("ul", { className: "m-icon-links", "aria-label": label, children: links.map((link, index) => (_jsx("li", { children: link.icon ? (_jsx(KitLink, { href: link.href, "aria-label": link.label, children: _jsx("span", { "aria-hidden": "true", children: link.icon }) })) : (_jsx(KitLink, { href: link.href, children: link.label })) }, `${link.label}:${link.href}:${index}`))) }));
|
|
44
|
+
}
|