@hraness/design-kit 0.34.0 → 0.35.1
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/AGENT_SETUP.md +4 -0
- package/ARTICLE_COPY.md +12 -6
- package/README.md +20 -2
- package/dist/{chunk-nwpcjm48.js → chunk-6bfr0g23.js} +1115 -441
- package/dist/{chunk-h4k7yv6x.js → chunk-6ts4955n.js} +36 -36
- package/dist/chunk-gmea6p0d.js +13 -0
- package/dist/{chunk-mr1vdcjq.js → chunk-tzd7177a.js} +142 -82
- package/dist/index.js +5 -1
- package/dist/mockups/client.js +50 -15
- package/dist/portfolio.js +586 -85
- package/dist/react/index.js +169 -105
- package/dist/react/platform-install.js +3 -2
- package/dist/react/server.js +17 -3
- package/dist/stylex-manifest.json +1 -1
- package/dist/stylex.css +376 -58
- package/package.json +16 -3
- package/portfolio-inventory.json +1 -1
- package/src/diagrams.ts +11 -0
- package/src/index.ts +2 -0
- package/src/marketing-comparison.css +162 -0
- package/src/marketing-diagram.css +24 -0
- package/src/mockups/client.tsx +25 -17
- package/src/mockups.css +19 -0
- package/src/plain-publication.css +9 -6
- package/src/portfolio.generated.json +578 -86
- package/src/portfolio.generated.ts +578 -86
- package/src/portfolio.ts +73 -0
- package/src/product-marketing-foundation.css +29 -0
- package/src/product-marketing.css +133 -38
- package/src/react/agent-setup-prompt.stylex.ts +5 -25
- package/src/react/agent-setup-prompt.tsx +9 -18
- package/src/react/article.tsx +2 -2
- package/src/react/design-gallery.tsx +34 -6
- package/src/react/index.ts +3 -0
- package/src/react/marketing-account.stylex.ts +74 -0
- package/src/react/marketing-account.tsx +69 -0
- package/src/react/marketing-comparison.stylex.ts +24 -0
- package/src/react/marketing-comparison.tsx +73 -0
- package/src/react/marketing-diagram.stylex.ts +8 -0
- package/src/react/marketing-diagram.tsx +33 -0
- package/src/react/platform-install.stylex.ts +9 -37
- package/src/react/platform-install.tsx +17 -24
- package/src/react/product-marketing.stylex.ts +138 -109
- package/src/react/product-marketing.tsx +69 -43
- package/src/react/server.ts +3 -0
- package/src/react/setup-copy-button.stylex.ts +39 -0
- package/src/react/setup-copy-button.tsx +20 -0
- package/src/styles.css +2 -0
- package/src/typography.css +32 -0
- /package/dist/{chunk-4d79eskk.js → chunk-c80g58j1.js} +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as stylex from "@stylexjs/stylex";
|
|
2
|
+
|
|
3
|
+
export const marketingAccountStyles = stylex.create({
|
|
4
|
+
section: {
|
|
5
|
+
alignItems: "center",
|
|
6
|
+
"border-top": "var(--hraness-marketing-rule, 1px solid var(--line))",
|
|
7
|
+
color: "var(--foreground)",
|
|
8
|
+
display: "grid",
|
|
9
|
+
gap: "2rem 4rem",
|
|
10
|
+
gridTemplateColumns: { default: "minmax(0, 1fr)", "@media (min-width: 50rem)": "minmax(0, 1fr) minmax(0, 1fr)" },
|
|
11
|
+
paddingBlock: "clamp(2.5rem, 6vw, 4rem)",
|
|
12
|
+
scrollMarginTop: "calc(var(--hraness-sticky-offset, 5rem) + 2rem)",
|
|
13
|
+
},
|
|
14
|
+
copy: { minWidth: 0 },
|
|
15
|
+
heading: {
|
|
16
|
+
fontFamily: "var(--font-text)",
|
|
17
|
+
fontSize: "clamp(1.625rem, 1.3rem + 1vw, 2.25rem)",
|
|
18
|
+
fontWeight: 550,
|
|
19
|
+
letterSpacing: "-0.02em",
|
|
20
|
+
lineHeight: 1.15,
|
|
21
|
+
margin: 0,
|
|
22
|
+
textWrap: "balance",
|
|
23
|
+
},
|
|
24
|
+
summary: {
|
|
25
|
+
color: "var(--muted)",
|
|
26
|
+
fontSize: "1rem",
|
|
27
|
+
lineHeight: 1.6,
|
|
28
|
+
marginTop: "1rem",
|
|
29
|
+
maxWidth: "42ch",
|
|
30
|
+
textWrap: "pretty",
|
|
31
|
+
},
|
|
32
|
+
content: { minWidth: 0 },
|
|
33
|
+
actions: { alignItems: "center", display: "flex", flexWrap: "wrap", gap: "0.75rem 1.5rem" },
|
|
34
|
+
primary: {
|
|
35
|
+
alignItems: "center",
|
|
36
|
+
backgroundColor: { default: "var(--primary)", ":hover": "color-mix(in oklch, var(--primary) 88%, var(--foreground))", "@media (forced-colors: active)": "ButtonFace" },
|
|
37
|
+
borderWidth: "1px",
|
|
38
|
+
borderStyle: "solid",
|
|
39
|
+
borderColor: { default: "transparent", "@media (forced-colors: active)": "ButtonText" },
|
|
40
|
+
borderRadius: "0.625rem",
|
|
41
|
+
color: { default: "var(--primary-foreground)", "@media (forced-colors: active)": "ButtonText" },
|
|
42
|
+
display: "inline-flex",
|
|
43
|
+
fontFamily: "var(--font-text)",
|
|
44
|
+
fontSize: "1.0625rem",
|
|
45
|
+
fontWeight: 550,
|
|
46
|
+
justifyContent: "center",
|
|
47
|
+
lineHeight: 1.3,
|
|
48
|
+
maxWidth: "100%",
|
|
49
|
+
minHeight: "3.5rem",
|
|
50
|
+
outline: { default: null, ":focus-visible": "2px solid var(--primary)" },
|
|
51
|
+
outlineOffset: { default: null, ":focus-visible": "3px" },
|
|
52
|
+
paddingBlock: "0.875rem",
|
|
53
|
+
paddingInline: "1.5rem",
|
|
54
|
+
textAlign: "center",
|
|
55
|
+
textDecoration: "none",
|
|
56
|
+
},
|
|
57
|
+
signIn: {
|
|
58
|
+
alignItems: "center",
|
|
59
|
+
color: "var(--foreground)",
|
|
60
|
+
display: "inline-flex",
|
|
61
|
+
fontFamily: "var(--font-text)",
|
|
62
|
+
fontSize: "1.0625rem",
|
|
63
|
+
fontWeight: 450,
|
|
64
|
+
lineHeight: 1.3,
|
|
65
|
+
minHeight: "3.5rem",
|
|
66
|
+
outline: { default: null, ":focus-visible": "2px solid var(--primary)" },
|
|
67
|
+
outlineOffset: { default: null, ":focus-visible": "3px" },
|
|
68
|
+
textDecorationLine: "underline",
|
|
69
|
+
textDecorationStyle: "dotted",
|
|
70
|
+
textDecorationColor: { default: "color-mix(in srgb, currentColor 45%, transparent)", ":hover": "currentColor", "@media (forced-colors: active)": "LinkText" },
|
|
71
|
+
textDecorationThickness: "1px",
|
|
72
|
+
textUnderlineOffset: "0.22em",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as stylex from "@stylexjs/stylex";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { marketingAccountStyles as styles } from "./marketing-account.stylex.js";
|
|
5
|
+
|
|
6
|
+
/** Optional account access near the end of a product's marketing page. */
|
|
7
|
+
export function MarketingAccount({
|
|
8
|
+
children,
|
|
9
|
+
className,
|
|
10
|
+
heading = "Your account",
|
|
11
|
+
id = "account",
|
|
12
|
+
summary,
|
|
13
|
+
}: Readonly<{
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
heading?: string;
|
|
17
|
+
id?: string;
|
|
18
|
+
summary: ReactNode;
|
|
19
|
+
}>) {
|
|
20
|
+
const presentation = stylex.props(styles.section);
|
|
21
|
+
return (
|
|
22
|
+
<section
|
|
23
|
+
{...presentation}
|
|
24
|
+
aria-labelledby={`${id}-heading`}
|
|
25
|
+
className={["hraness-marketing-account", presentation.className, className].filter(Boolean).join(" ")}
|
|
26
|
+
id={id}
|
|
27
|
+
>
|
|
28
|
+
<div {...stylex.props(styles.copy)}>
|
|
29
|
+
<h2 {...stylex.props(styles.heading)} id={`${id}-heading`}>{heading}</h2>
|
|
30
|
+
<div {...stylex.props(styles.summary)}>{summary}</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div {...stylex.props(styles.content)}>{children}</div>
|
|
33
|
+
</section>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MarketingAccountAction {
|
|
38
|
+
readonly href: string;
|
|
39
|
+
readonly analyticsEvent?: string;
|
|
40
|
+
readonly analyticsId?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One prominent account action and an optional, equally tall sign-in link. */
|
|
44
|
+
export function MarketingAccountActions({ primary, signIn }: Readonly<{
|
|
45
|
+
primary: MarketingAccountAction & Readonly<{ label: string }>;
|
|
46
|
+
signIn?: MarketingAccountAction;
|
|
47
|
+
}>) {
|
|
48
|
+
return (
|
|
49
|
+
<div {...stylex.props(styles.actions)} className={`hraness-marketing-account__actions ${stylex.props(styles.actions).className}`}>
|
|
50
|
+
<a
|
|
51
|
+
{...stylex.props(styles.primary)}
|
|
52
|
+
className={`hraness-marketing-account__primary ${stylex.props(styles.primary).className}`}
|
|
53
|
+
data-analytics-event={primary.analyticsEvent}
|
|
54
|
+
data-analytics-id={primary.analyticsId}
|
|
55
|
+
data-emphasis="primary"
|
|
56
|
+
href={primary.href}
|
|
57
|
+
>{primary.label}</a>
|
|
58
|
+
{signIn === undefined ? null : (
|
|
59
|
+
<a
|
|
60
|
+
{...stylex.props(styles.signIn)}
|
|
61
|
+
className={`hraness-marketing-account__sign-in ${stylex.props(styles.signIn).className}`}
|
|
62
|
+
data-analytics-event={signIn.analyticsEvent}
|
|
63
|
+
data-analytics-id={signIn.analyticsId}
|
|
64
|
+
href={signIn.href}
|
|
65
|
+
>Sign in</a>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as stylex from "@stylexjs/stylex";
|
|
2
|
+
|
|
3
|
+
export const comparisonStyles = stylex.create({
|
|
4
|
+
figure: { "margin": "0", "min-inline-size": "0", "color": "var(--hraness-marketing-ink, var(--foreground))", "font-family": "var(--font-text)" },
|
|
5
|
+
scroll: { "overflow-x": "auto", "max-inline-size": "100%", "outline": { "default": null, ":focus-visible": "2px solid var(--primary)" }, "outline-offset": { "default": null, ":focus-visible": "3px" } },
|
|
6
|
+
table: { "inline-size": "100%", "border-collapse": "collapse", "text-align": "start", "font-size": "0.9375rem", "line-height": "1.4" },
|
|
7
|
+
caption: { "padding-block-end": "1.25rem", "text-align": "start", "font-size": "1.125rem", "font-weight": "550", "color": "var(--foreground)" },
|
|
8
|
+
corner: { "min-inline-size": "9rem", "border-block-end": "1px solid var(--line)" },
|
|
9
|
+
option: { "padding": "1rem", "min-inline-size": "8.5rem", "border-block-end": "1px solid var(--line)", "vertical-align": "bottom", "font-weight": "600" },
|
|
10
|
+
optionLabel: { "display": "flex", "align-items": "center", "gap": "0.6rem" },
|
|
11
|
+
mark: { "object-fit": "contain", "flex-shrink": "0", "inline-size": "1.5rem", "block-size": "1.5rem" },
|
|
12
|
+
rowLabel: { "padding": "1rem 1rem 1rem 0", "min-inline-size": "9rem", "max-inline-size": "15rem", "border-block-end": "1px solid var(--line)", "vertical-align": "top", "font-weight": "500" },
|
|
13
|
+
cell: { "padding": "1rem", "border-block-end": "1px solid var(--line)", "vertical-align": "top" },
|
|
14
|
+
highlight: { "background-color": { "default": "color-mix(in srgb, var(--primary) 5%, transparent)", "@media (forced-colors: active)": "Canvas" }, "color": "var(--foreground)" },
|
|
15
|
+
value: { "display": "grid", "gap": "0.25rem" },
|
|
16
|
+
label: { "display": "flex", "align-items": "flex-start", "gap": "0.45rem" },
|
|
17
|
+
glyph: { "flex-shrink": "0", "margin-block-start": "0.1rem", "inline-size": "1.125rem", "block-size": "1.125rem" },
|
|
18
|
+
positive: { "color": { "default": "var(--hraness-studio-emerald)", "@media (forced-colors: active)": "CanvasText" } },
|
|
19
|
+
negative: { "color": { "default": "var(--muted)", "@media (forced-colors: active)": "CanvasText" } },
|
|
20
|
+
conditional: { "color": { "default": "var(--hraness-studio-amber)", "@media (forced-colors: active)": "CanvasText" } },
|
|
21
|
+
text: { "font-variant-numeric": "tabular-nums" },
|
|
22
|
+
detail: { "display": "block", "color": "var(--muted)", "font-size": "0.8125rem", "font-weight": "400", "line-height": "1.45", "margin-block-start": "0.2rem", "max-inline-size": "26ch" },
|
|
23
|
+
note: { "color": "var(--muted)", "font-size": "0.875rem", "line-height": "1.5", "margin-block-start": "1rem", "max-inline-size": "76ch" },
|
|
24
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as stylex from "@stylexjs/stylex";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { ComparisonGlyph } from "./article.js";
|
|
4
|
+
import { comparisonStyles as styles } from "./marketing-comparison.stylex.js";
|
|
5
|
+
|
|
6
|
+
export type MarketingComparisonStatus = "yes" | "no" | "partial" | "optional" | "depends";
|
|
7
|
+
export type MarketingComparisonValue = boolean | "partial" | string | Readonly<{
|
|
8
|
+
status: MarketingComparisonStatus;
|
|
9
|
+
label?: string;
|
|
10
|
+
detail?: string;
|
|
11
|
+
}>;
|
|
12
|
+
export interface MarketingComparisonOption { readonly name: string; readonly mark?: string }
|
|
13
|
+
export interface MarketingComparisonRow {
|
|
14
|
+
readonly label: string;
|
|
15
|
+
readonly values: readonly MarketingComparisonValue[];
|
|
16
|
+
readonly note?: string;
|
|
17
|
+
}
|
|
18
|
+
const labels = { yes: "Yes", no: "No", partial: "Partly", optional: "Optional", depends: "Depends" } as const;
|
|
19
|
+
|
|
20
|
+
function presentation(slot: string, props: ReturnType<typeof stylex.props>) {
|
|
21
|
+
const hook = slot.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`);
|
|
22
|
+
return { ...props, className: `hraness-marketing-comparison__${hook} ${props.className}` };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function Value({ value }: Readonly<{ value: MarketingComparisonValue }>) {
|
|
26
|
+
if (typeof value === "string" && value !== "partial") return <span {...presentation("text", stylex.props(styles.text))}>{value}</span>;
|
|
27
|
+
const status = typeof value === "object" ? value.status : value === true ? "yes" : value === false ? "no" : "partial";
|
|
28
|
+
const label = typeof value === "object" ? value.label ?? labels[status] : labels[status];
|
|
29
|
+
const detail = typeof value === "object" ? value.detail : undefined;
|
|
30
|
+
return <div {...presentation("value", stylex.props(styles.value))} data-comparison-status={status}>
|
|
31
|
+
<span {...presentation("label", stylex.props(styles.label))}>
|
|
32
|
+
<ComparisonGlyph className={`hraness-marketing-comparison__glyph ${stylex.props(styles.glyph, status === "yes" ? styles.positive : status === "no" ? styles.negative : styles.conditional).className}`}
|
|
33
|
+
kind={status === "yes" || status === "no" ? status : "partial"} />
|
|
34
|
+
<span>{label}</span>
|
|
35
|
+
</span>
|
|
36
|
+
{detail === undefined ? null : <small {...presentation("detail", stylex.props(styles.detail))}>{detail}</small>}
|
|
37
|
+
</div>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Concise product comparisons with visible words for every icon and conditional claim. */
|
|
41
|
+
export function MarketingComparison({ caption, className, highlight, id, note, options, rows }: Readonly<{
|
|
42
|
+
caption: string;
|
|
43
|
+
className?: string;
|
|
44
|
+
highlight?: number;
|
|
45
|
+
id?: string;
|
|
46
|
+
note?: ReactNode;
|
|
47
|
+
options: readonly MarketingComparisonOption[];
|
|
48
|
+
rows: readonly MarketingComparisonRow[];
|
|
49
|
+
}>) {
|
|
50
|
+
if (caption.trim() === "") throw new RangeError("Marketing comparison needs a caption.");
|
|
51
|
+
if (options.length === 0) throw new RangeError("Marketing comparison needs at least one option.");
|
|
52
|
+
if (highlight !== undefined && (!Number.isInteger(highlight) || highlight < 0 || highlight >= options.length)) throw new RangeError("Marketing comparison highlight must be an option index.");
|
|
53
|
+
for (const row of rows) if (row.values.length !== options.length) throw new RangeError(`Marketing comparison row ${JSON.stringify(row.label)} must have one value per option.`);
|
|
54
|
+
return <figure {...presentation("figure", stylex.props(styles.figure))} className={["hraness-marketing-comparison", stylex.props(styles.figure).className, className].filter(Boolean).join(" ")} id={id}>
|
|
55
|
+
<div {...presentation("scroll", stylex.props(styles.scroll))} aria-label={caption} role="region" tabIndex={0}>
|
|
56
|
+
<table {...presentation("table", stylex.props(styles.table))}>
|
|
57
|
+
<caption {...presentation("caption", stylex.props(styles.caption))}>{caption}</caption>
|
|
58
|
+
<thead><tr><td {...presentation("corner", stylex.props(styles.corner))} />{options.map((option, index) =>
|
|
59
|
+
<th {...presentation("option", stylex.props(styles.option, index === highlight && styles.highlight))} data-highlight={index === highlight ? "" : undefined} key={option.name} scope="col">
|
|
60
|
+
<span {...presentation("optionLabel", stylex.props(styles.optionLabel))}>
|
|
61
|
+
{option.mark === undefined ? null : <img alt="" {...presentation("mark", stylex.props(styles.mark))} height={24} src={option.mark} width={24} />}
|
|
62
|
+
<span>{option.name}</span>
|
|
63
|
+
</span>
|
|
64
|
+
</th>)}</tr></thead>
|
|
65
|
+
<tbody>{rows.map((row) => <tr key={row.label}>
|
|
66
|
+
<th {...presentation("rowLabel", stylex.props(styles.rowLabel))} scope="row">{row.label}{row.note === undefined ? null : <small {...presentation("detail", stylex.props(styles.detail))}>{row.note}</small>}</th>
|
|
67
|
+
{row.values.map((value, index) => <td {...presentation("cell", stylex.props(styles.cell, index === highlight && styles.highlight))} data-highlight={index === highlight ? "" : undefined} key={index}><Value value={value} /></td>)}
|
|
68
|
+
</tr>)}</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
</div>
|
|
71
|
+
{note === undefined || note === null ? null : <figcaption {...presentation("note", stylex.props(styles.note))}>{note}</figcaption>}
|
|
72
|
+
</figure>;
|
|
73
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as stylex from "@stylexjs/stylex";
|
|
2
|
+
|
|
3
|
+
export const diagramStyles = stylex.create({
|
|
4
|
+
figure: { "margin": "0", "min-inline-size": "0", "color": "var(--foreground)", "font-family": "var(--font-text)" },
|
|
5
|
+
scroll: { "overflow-x": "auto", "max-inline-size": "100%", "outline": { "default": null, ":focus-visible": "2px solid var(--primary)" }, "outline-offset": { "default": null, ":focus-visible": "3px" } },
|
|
6
|
+
canvas: { "display": "block", "inline-size": "100%", "min-inline-size": "40rem", "block-size": "auto", "font-family": "var(--font-text)", "font-size": "20px", "color": "var(--primary)" },
|
|
7
|
+
caption: { "margin-block-start": "1rem", "max-inline-size": "76ch", "color": "var(--muted)", "font-size": "0.875rem", "line-height": "1.5" },
|
|
8
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as stylex from "@stylexjs/stylex";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { diagramMetrics } from "../diagrams.js";
|
|
4
|
+
import { diagramStyles as styles } from "./marketing-diagram.stylex.js";
|
|
5
|
+
|
|
6
|
+
/** A bounded arrowhead whose paint follows its connector's stroke. IDs are caller-owned. */
|
|
7
|
+
export function DiagramArrowhead({ id }: Readonly<{ id: string }>) {
|
|
8
|
+
if (id.trim() === "" || /\s/u.test(id)) throw new RangeError("Diagram arrowhead needs one nonempty SVG ID.");
|
|
9
|
+
return <marker id={id} markerHeight={diagramMetrics.arrowheadSize} markerUnits="userSpaceOnUse" markerWidth={diagramMetrics.arrowheadSize} orient="auto-start-reverse" refX="5.5" refY="3" viewBox="0 0 6 6">
|
|
10
|
+
<path d="M0 0 L6 3 L0 6 Z" fill="context-stroke" />
|
|
11
|
+
</marker>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** An accessible SVG frame. Layout and facts stay with the consuming product. */
|
|
15
|
+
export function MarketingDiagram({ caption, children, className, height, label, width }: Readonly<{
|
|
16
|
+
caption?: ReactNode;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
height: number;
|
|
20
|
+
label: string;
|
|
21
|
+
width: number;
|
|
22
|
+
}>) {
|
|
23
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) throw new RangeError("Marketing diagram dimensions must be positive.");
|
|
24
|
+
if (label.trim() === "") throw new RangeError("Marketing diagram needs a useful accessible label.");
|
|
25
|
+
return <figure className={["hraness-diagram", stylex.props(styles.figure).className, className].filter(Boolean).join(" ")}>
|
|
26
|
+
<div {...stylex.props(styles.scroll)} className={`hraness-diagram__scroll ${stylex.props(styles.scroll).className}`} aria-label={label} role="region" tabIndex={0}>
|
|
27
|
+
<svg {...stylex.props(styles.canvas)} className={`hraness-diagram__canvas ${stylex.props(styles.canvas).className}`} aria-label={label} height={height} role="img" viewBox={`0 0 ${width} ${height}`} width={width}>
|
|
28
|
+
{children}
|
|
29
|
+
</svg>
|
|
30
|
+
</div>
|
|
31
|
+
{caption === undefined || caption === null ? null : <figcaption {...stylex.props(styles.caption)}>{caption}</figcaption>}
|
|
32
|
+
</figure>;
|
|
33
|
+
}
|
|
@@ -156,11 +156,12 @@ export const platformInstallStyles = stylex.create({
|
|
|
156
156
|
},
|
|
157
157
|
command: {
|
|
158
158
|
backgroundColor: { default: surface, [forced]: "Canvas" },
|
|
159
|
-
borderColor: { default:
|
|
159
|
+
borderColor: { default: `color-mix(in srgb, ${focusRing} 34%, ${line})`, [forced]: "CanvasText" },
|
|
160
160
|
borderRadius: "0.75rem",
|
|
161
161
|
borderStyle: "solid",
|
|
162
162
|
borderWidth: "1px",
|
|
163
163
|
boxSizing: "border-box",
|
|
164
|
+
boxShadow: { default: `0 3px 16px color-mix(in srgb, ${focusRing} 7%, transparent)`, [forced]: "none" },
|
|
164
165
|
display: "grid",
|
|
165
166
|
minInlineSize: 0,
|
|
166
167
|
overflow: "hidden",
|
|
@@ -220,40 +221,6 @@ export const platformInstallStyles = stylex.create({
|
|
|
220
221
|
fontSize: "inherit",
|
|
221
222
|
padding: 0,
|
|
222
223
|
},
|
|
223
|
-
copy: {
|
|
224
|
-
alignItems: "center",
|
|
225
|
-
appearance: "none",
|
|
226
|
-
backgroundColor: {
|
|
227
|
-
default: `color-mix(in srgb, ${ink} 4%, ${surface})`,
|
|
228
|
-
":hover": `color-mix(in srgb, ${ink} 9%, ${surface})`,
|
|
229
|
-
[forced]: "ButtonFace",
|
|
230
|
-
},
|
|
231
|
-
borderColor: { default: line, [forced]: "ButtonText" },
|
|
232
|
-
borderRadius: "0.5rem",
|
|
233
|
-
borderStyle: "solid",
|
|
234
|
-
borderWidth: "1px",
|
|
235
|
-
boxSizing: "border-box",
|
|
236
|
-
color: { default: ink, [forced]: "ButtonText" },
|
|
237
|
-
cursor: "pointer",
|
|
238
|
-
display: { default: "inline-flex", [noScript]: "none" },
|
|
239
|
-
flexShrink: 0,
|
|
240
|
-
fontFamily: textFont,
|
|
241
|
-
fontSize: "0.8125rem",
|
|
242
|
-
fontWeight: 500,
|
|
243
|
-
gap: "0.35rem",
|
|
244
|
-
lineHeight: 1,
|
|
245
|
-
margin: 0,
|
|
246
|
-
minBlockSize: { default: "2rem", "@media (pointer: coarse)": "2.75rem" },
|
|
247
|
-
outline: { default: "none", ":focus-visible": `2px solid ${focusRing}` },
|
|
248
|
-
outlineOffset: "1px",
|
|
249
|
-
paddingBlock: "0.35rem",
|
|
250
|
-
paddingInline: "0.65rem",
|
|
251
|
-
},
|
|
252
|
-
copyIcon: {
|
|
253
|
-
blockSize: "0.95rem",
|
|
254
|
-
flexShrink: 0,
|
|
255
|
-
inlineSize: "0.95rem",
|
|
256
|
-
},
|
|
257
224
|
alternatives: {
|
|
258
225
|
display: "grid",
|
|
259
226
|
gap: "0.6rem",
|
|
@@ -268,6 +235,13 @@ export const platformInstallStyles = stylex.create({
|
|
|
268
235
|
lineHeight: 1.5,
|
|
269
236
|
margin: 0,
|
|
270
237
|
},
|
|
238
|
+
commandNote: {
|
|
239
|
+
borderBlockStartColor: { default: line, [forced]: "CanvasText" },
|
|
240
|
+
borderBlockStartStyle: "solid",
|
|
241
|
+
borderBlockStartWidth: "1px",
|
|
242
|
+
paddingBlock: "0.65rem",
|
|
243
|
+
paddingInline: "0.95rem",
|
|
244
|
+
},
|
|
271
245
|
unavailable: {
|
|
272
246
|
borderColor: { default: line, [forced]: "CanvasText" },
|
|
273
247
|
borderRadius: "0.75rem",
|
|
@@ -358,8 +332,6 @@ const hooks: Partial<Record<Part, string>> = {
|
|
|
358
332
|
code: "hraness-platform-install__code",
|
|
359
333
|
command: "hraness-platform-install__command",
|
|
360
334
|
commandBar: "hraness-platform-install__command-bar",
|
|
361
|
-
copy: "hraness-platform-install__copy",
|
|
362
|
-
copyIcon: "hraness-platform-install__copy-icon",
|
|
363
335
|
icon: "hraness-platform-icon",
|
|
364
336
|
markSymbols: "hraness-platform-install__marks",
|
|
365
337
|
note: "hraness-platform-install__note",
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
platformMark,
|
|
12
12
|
type PlatformId,
|
|
13
13
|
} from "../platforms.js";
|
|
14
|
+
import { SetupCopyButton } from "./setup-copy-button.js";
|
|
15
|
+
import { SyntaxCode } from "./syntax-code.js";
|
|
14
16
|
import { platformInstallClassName as cx } from "./platform-install.stylex.js";
|
|
15
17
|
|
|
16
18
|
/** Another way to install on the same platform, such as npm or Homebrew. */
|
|
@@ -28,7 +30,7 @@ interface PlatformInstallTargetBase {
|
|
|
28
30
|
readonly label?: string;
|
|
29
31
|
/** Where the command runs, such as "Terminal" or "PowerShell". */
|
|
30
32
|
readonly shell?: string;
|
|
31
|
-
/** A short qualifier
|
|
33
|
+
/** A short qualifier in the primary command footer, such as "Apple silicon" or "Requires Bun 1.3.14+". */
|
|
32
34
|
readonly note?: ReactNode;
|
|
33
35
|
readonly alternatives?: readonly PlatformInstallAlternative[];
|
|
34
36
|
}
|
|
@@ -166,22 +168,12 @@ function PlatformMarkUse({ platform, symbolId }: Readonly<{ platform: PlatformId
|
|
|
166
168
|
);
|
|
167
169
|
}
|
|
168
170
|
|
|
169
|
-
function CopyGlyph({ copied }: Readonly<{ copied: boolean }>) {
|
|
170
|
-
return (
|
|
171
|
-
<svg aria-hidden="true" className={cx(["copyIcon"])} fill="none" focusable="false" stroke="currentColor"
|
|
172
|
-
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} viewBox="0 0 24 24">
|
|
173
|
-
{copied
|
|
174
|
-
? <path d="M5 12.5l4.5 4.5L19 7.5" />
|
|
175
|
-
: <><rect height="12" rx="2" width="12" x="8" y="8" /><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" /></>}
|
|
176
|
-
</svg>
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
171
|
function CommandBlock({
|
|
181
172
|
caption,
|
|
182
173
|
command,
|
|
183
174
|
copyKey,
|
|
184
175
|
onCopy,
|
|
176
|
+
note,
|
|
185
177
|
status,
|
|
186
178
|
subject,
|
|
187
179
|
}: Readonly<{
|
|
@@ -189,6 +181,7 @@ function CommandBlock({
|
|
|
189
181
|
command: string;
|
|
190
182
|
copyKey: string;
|
|
191
183
|
onCopy: (key: string, command: string, subject: string, pre: HTMLElement | null) => void;
|
|
184
|
+
note?: ReactNode;
|
|
192
185
|
status: CopyStatus | null;
|
|
193
186
|
subject: string;
|
|
194
187
|
}>) {
|
|
@@ -198,21 +191,20 @@ function CommandBlock({
|
|
|
198
191
|
<div className={cx(["command"])} data-copy-state={state}>
|
|
199
192
|
<div className={cx(["commandBar", (caption === undefined || caption === "") && "commandBarEmpty"])}>
|
|
200
193
|
<span className={cx(["shell"])}>{caption}</span>
|
|
201
|
-
<
|
|
202
|
-
className=
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
<span className={cx(["status"])}> {subject}</span>
|
|
210
|
-
</button>
|
|
194
|
+
<SetupCopyButton
|
|
195
|
+
className="hraness-platform-install__copy"
|
|
196
|
+
iconClassName="hraness-platform-install__copy-icon"
|
|
197
|
+
failedLabel="Select to copy"
|
|
198
|
+
onCopy={() => onCopy(copyKey, command, subject, preRef.current)}
|
|
199
|
+
state={state}
|
|
200
|
+
subject={subject}
|
|
201
|
+
/>
|
|
211
202
|
</div>
|
|
212
203
|
{/* Focusable so keyboard users can scroll a long command sideways. */}
|
|
213
204
|
<pre aria-label={subject} className={cx(["pre"])} ref={preRef} tabIndex={0}>
|
|
214
|
-
<
|
|
205
|
+
<SyntaxCode className={cx(["code"])} code={command} language="shell" styles="classes" />
|
|
215
206
|
</pre>
|
|
207
|
+
{note === undefined ? null : <div className={cx(["note", "commandNote"])}>{note}</div>}
|
|
216
208
|
</div>
|
|
217
209
|
);
|
|
218
210
|
}
|
|
@@ -356,6 +348,7 @@ export function PlatformInstall({
|
|
|
356
348
|
command={target.command}
|
|
357
349
|
copyKey={`${target.id}:primary`}
|
|
358
350
|
onCopy={copy}
|
|
351
|
+
note={target.note}
|
|
359
352
|
status={status}
|
|
360
353
|
subject={`${name} install command`}
|
|
361
354
|
/>
|
|
@@ -376,7 +369,7 @@ export function PlatformInstall({
|
|
|
376
369
|
))}
|
|
377
370
|
</ul>
|
|
378
371
|
)}
|
|
379
|
-
{target.note === undefined ? null : <div className={cx(["note"])}>{target.note}</div>}
|
|
372
|
+
{target.command !== undefined || target.note === undefined ? null : <div className={cx(["note"])}>{target.note}</div>}
|
|
380
373
|
</div>
|
|
381
374
|
</div>
|
|
382
375
|
);
|