@sproutsocial/seeds-react-narrative-kit 0.1.0 → 0.3.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.
Files changed (60) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +28 -0
  3. package/dist/esm/index.js +324 -2
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/eyebrow-token.css +37 -0
  6. package/dist/index.d.mts +304 -1
  7. package/dist/index.d.ts +304 -1
  8. package/dist/index.js +333 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/metric-highlight.css +168 -0
  11. package/dist/narrative-container.css +1 -1
  12. package/dist/narrative-divider.css +27 -0
  13. package/dist/narrative-headline.css +132 -0
  14. package/dist/narrative-summary.css +126 -0
  15. package/dist/pull-quote.css +89 -0
  16. package/package.json +13 -3
  17. package/src/EyebrowToken/EyebrowToken.stories.tsx +25 -0
  18. package/src/EyebrowToken/EyebrowToken.tsx +25 -0
  19. package/src/EyebrowToken/EyebrowTokenTypes.ts +7 -0
  20. package/src/EyebrowToken/__tests__/EyebrowToken.test.tsx +35 -0
  21. package/src/EyebrowToken/index.ts +2 -0
  22. package/src/MetricHighlight/MetricHighlight.stories.tsx +110 -0
  23. package/src/MetricHighlight/MetricHighlight.tsx +116 -0
  24. package/src/MetricHighlight/MetricHighlightTypes.ts +58 -0
  25. package/src/MetricHighlight/__tests__/MetricHighlight.test.tsx +116 -0
  26. package/src/MetricHighlight/index.ts +5 -0
  27. package/src/NarrativeContainer/NarrativeContainer.stories.tsx +3 -3
  28. package/src/NarrativeContainer/NarrativeContainer.tsx +1 -23
  29. package/src/NarrativeDivider/NarrativeDivider.stories.tsx +33 -0
  30. package/src/NarrativeDivider/NarrativeDivider.tsx +30 -0
  31. package/src/NarrativeDivider/NarrativeDividerTypes.ts +4 -0
  32. package/src/NarrativeDivider/__tests__/NarrativeDivider.test.tsx +35 -0
  33. package/src/NarrativeDivider/index.ts +2 -0
  34. package/src/NarrativeHeadline/NarrativeHeadline.stories.tsx +68 -0
  35. package/src/NarrativeHeadline/NarrativeHeadline.tsx +45 -0
  36. package/src/NarrativeHeadline/NarrativeHeadlineHighlight.tsx +31 -0
  37. package/src/NarrativeHeadline/NarrativeHeadlineRoll.tsx +103 -0
  38. package/src/NarrativeHeadline/NarrativeHeadlineTypes.ts +63 -0
  39. package/src/NarrativeHeadline/__tests__/NarrativeHeadline.test.tsx +133 -0
  40. package/src/NarrativeHeadline/index.ts +10 -0
  41. package/src/NarrativeSummary/NarrativeSummary.stories.tsx +79 -0
  42. package/src/NarrativeSummary/NarrativeSummary.tsx +98 -0
  43. package/src/NarrativeSummary/NarrativeSummaryTypes.ts +59 -0
  44. package/src/NarrativeSummary/__tests__/NarrativeSummary.test.tsx +99 -0
  45. package/src/NarrativeSummary/index.ts +5 -0
  46. package/src/Playground/Playground.stories.tsx +163 -0
  47. package/src/PullQuote/PullQuote.stories.tsx +45 -0
  48. package/src/PullQuote/PullQuote.tsx +42 -0
  49. package/src/PullQuote/PullQuoteTypes.ts +18 -0
  50. package/src/PullQuote/__tests__/PullQuote.test.tsx +48 -0
  51. package/src/PullQuote/index.ts +2 -0
  52. package/src/_internal/cn.ts +22 -0
  53. package/src/eyebrow-token.css +37 -0
  54. package/src/index.ts +31 -0
  55. package/src/metric-highlight.css +168 -0
  56. package/src/narrative-container.css +1 -1
  57. package/src/narrative-divider.css +27 -0
  58. package/src/narrative-headline.css +132 -0
  59. package/src/narrative-summary.css +126 -0
  60. package/src/pull-quote.css +89 -0
@@ -0,0 +1,116 @@
1
+ import * as React from "react";
2
+ import { cn } from "../_internal/cn";
3
+ import type {
4
+ TypeMetricHighlightProps,
5
+ TypeMetricHighlightTrend,
6
+ } from "./MetricHighlightTypes";
7
+
8
+ /** Directional arrow glyph for the trend badge (decorative; `aria-hidden`). */
9
+ const TrendArrow = ({ direction }: { direction: "up" | "down" }) => (
10
+ <svg
11
+ className="seeds-metric-highlight-trend-arrow"
12
+ viewBox="0 0 16 16"
13
+ aria-hidden
14
+ focusable={false}
15
+ >
16
+ {direction === "up" ? (
17
+ <path
18
+ d="M4.5 11.5 11.5 4.5M11.5 4.5H5.5M11.5 4.5V10.5"
19
+ fill="none"
20
+ strokeLinecap="round"
21
+ strokeLinejoin="round"
22
+ />
23
+ ) : (
24
+ <path
25
+ d="M4.5 4.5 11.5 11.5M11.5 11.5H5.5M11.5 11.5V5.5"
26
+ fill="none"
27
+ strokeLinecap="round"
28
+ strokeLinejoin="round"
29
+ />
30
+ )}
31
+ </svg>
32
+ );
33
+
34
+ const TrendBadge = ({
35
+ direction = "up",
36
+ value,
37
+ ...rest
38
+ }: TypeMetricHighlightTrend) => (
39
+ <span
40
+ className={cn("seeds-metric-highlight-trend", {
41
+ "seeds-metric-highlight-trend-down": direction === "down",
42
+ })}
43
+ {...rest}
44
+ >
45
+ <TrendArrow direction={direction} />
46
+ {value != null && (
47
+ <span className="seeds-metric-highlight-trend-value">{value}</span>
48
+ )}
49
+ </span>
50
+ );
51
+
52
+ /**
53
+ * MetricHighlight — a narrative primitive that spotlights a single metric: a
54
+ * label, a large monospace value, an optional trend badge (a directional arrow
55
+ * plus an optional number such as "240%"), and an optional chart supplied via
56
+ * the `sparkline` slot (e.g. a `<SparklineChart />` from
57
+ * `@sproutsocial/seeds-react-data-viz/sparkline`).
58
+ *
59
+ * Styled via `metric-highlight.css` (Seeds CSS custom properties); consumers
60
+ * import the classes through `@sproutsocial/racine/css/components`. Layout flexes
61
+ * across `appearance` (horizontal/vertical) and `size` (default/small); surface
62
+ * overrides are done with standard `className` / `style`.
63
+ */
64
+ const MetricHighlight = React.forwardRef<
65
+ HTMLDivElement,
66
+ TypeMetricHighlightProps
67
+ >(
68
+ (
69
+ {
70
+ label,
71
+ value,
72
+ trend,
73
+ sparkline,
74
+ appearance = "horizontal",
75
+ size = "default",
76
+ container = true,
77
+ className,
78
+ ...props
79
+ },
80
+ ref
81
+ ) => {
82
+ return (
83
+ <div
84
+ ref={ref}
85
+ className={cn(
86
+ "seeds-metric-highlight",
87
+ {
88
+ "seeds-metric-highlight-vertical": appearance === "vertical",
89
+ "seeds-metric-highlight-small": size === "small",
90
+ "seeds-metric-highlight-bare": !container,
91
+ },
92
+ className
93
+ )}
94
+ {...props}
95
+ >
96
+ <div className="seeds-metric-highlight-metric">
97
+ <p className="seeds-metric-highlight-label">{label}</p>
98
+ <div className="seeds-metric-highlight-value-row">
99
+ <p className="seeds-metric-highlight-value">{value}</p>
100
+ {trend != null && <TrendBadge {...trend} />}
101
+ </div>
102
+ </div>
103
+ {sparkline != null && (
104
+ // Layout-only slot (bounded dimensions per horizontal/vertical/small)
105
+ // for a consumer-supplied chart. It controls its own look; the slot
106
+ // only sizes and centers it.
107
+ <div className="seeds-metric-highlight-chart">{sparkline}</div>
108
+ )}
109
+ </div>
110
+ );
111
+ }
112
+ );
113
+
114
+ MetricHighlight.displayName = "MetricHighlight";
115
+
116
+ export default MetricHighlight;
@@ -0,0 +1,58 @@
1
+ import type * as React from "react";
2
+
3
+ export interface TypeMetricHighlightTrend {
4
+ /**
5
+ * Direction of movement. Selects the arrow glyph and the sentiment color of
6
+ * the badge (green for `up`, red for `down`).
7
+ * @default "up"
8
+ */
9
+ direction?: "up" | "down";
10
+ /**
11
+ * Optional value rendered next to the arrow (e.g. `"240%"`, `12`). When
12
+ * omitted the badge is icon-only.
13
+ */
14
+ value?: React.ReactNode;
15
+ /**
16
+ * Accessible label describing the trend (e.g. `"up 240%"`). The arrow glyph is
17
+ * decorative, so provide this whenever the trend conveys meaning on its own.
18
+ */
19
+ "aria-label"?: string;
20
+ }
21
+
22
+ export interface TypeMetricHighlightProps
23
+ extends React.HTMLAttributes<HTMLDivElement> {
24
+ /** Short metric label rendered above/beside the value (e.g. `"Insight metric"`). */
25
+ label: React.ReactNode;
26
+ /** The metric value, rendered as the emphasized monospace figure (e.g. `"123"`, `"1.2M"`). */
27
+ value: React.ReactNode;
28
+ /** Optional trend badge — a directional arrow plus an optional value. */
29
+ trend?: TypeMetricHighlightTrend;
30
+ /**
31
+ * Chart slot. When provided, this node renders in the chart area beside/below
32
+ * the metric — e.g. a `<SparklineChart />` from
33
+ * `@sproutsocial/seeds-react-data-viz/sparkline`. Omit it to render the metric
34
+ * on its own. The injected node controls its own look; the slot only sizes and
35
+ * centers it (pass `className="h-full w-full"` to fill).
36
+ */
37
+ sparkline?: React.ReactNode;
38
+ /**
39
+ * Layout direction.
40
+ * - `horizontal` — metric block beside the sparkline.
41
+ * - `vertical` — metric block stacked above a full-width sparkline.
42
+ * @default "horizontal"
43
+ */
44
+ appearance?: "horizontal" | "vertical";
45
+ /**
46
+ * Size of the value and overall density.
47
+ * - `default` — large value, label stacked above it.
48
+ * - `small` — condensed value, label and value on a single row.
49
+ * @default "default"
50
+ */
51
+ size?: "default" | "small";
52
+ /**
53
+ * Render the padded surface background. Set `false` to drop the surface and
54
+ * compose the metric inside another container.
55
+ * @default true
56
+ */
57
+ container?: boolean;
58
+ }
@@ -0,0 +1,116 @@
1
+ import React from "react";
2
+ import { render, screen } from "@sproutsocial/seeds-react-testing-library";
3
+ import MetricHighlight from "../MetricHighlight";
4
+
5
+ describe("MetricHighlight", () => {
6
+ it("renders the label and value", () => {
7
+ render(<MetricHighlight label="Engagement" value="123" />);
8
+
9
+ expect(screen.getByText("Engagement")).toBeInTheDocument();
10
+ expect(screen.getByText("123")).toBeInTheDocument();
11
+ });
12
+
13
+ it("renders the trend value when provided", () => {
14
+ render(
15
+ <MetricHighlight
16
+ label="Engagement"
17
+ value="123"
18
+ trend={{ direction: "up", value: "240%" }}
19
+ />
20
+ );
21
+
22
+ expect(screen.getByText("240%")).toBeInTheDocument();
23
+ });
24
+
25
+ it("renders an icon-only trend badge with no value text", () => {
26
+ const { container } = render(
27
+ <MetricHighlight
28
+ label="Engagement"
29
+ value="123"
30
+ trend={{ direction: "up", "aria-label": "trending up" }}
31
+ />
32
+ );
33
+
34
+ expect(
35
+ container.querySelector(".seeds-metric-highlight-trend")
36
+ ).toBeInTheDocument();
37
+ expect(
38
+ container.querySelector(".seeds-metric-highlight-trend-value")
39
+ ).not.toBeInTheDocument();
40
+ expect(screen.getByLabelText("trending up")).toBeInTheDocument();
41
+ });
42
+
43
+ it("applies the down modifier for a downward trend", () => {
44
+ const { container } = render(
45
+ <MetricHighlight
46
+ label="Engagement"
47
+ value="123"
48
+ trend={{ direction: "down", value: "12%" }}
49
+ />
50
+ );
51
+
52
+ expect(
53
+ container.querySelector(".seeds-metric-highlight-trend-down")
54
+ ).toBeInTheDocument();
55
+ });
56
+
57
+ it("omits the trend badge when no trend is provided", () => {
58
+ const { container } = render(
59
+ <MetricHighlight label="Engagement" value="123" />
60
+ );
61
+
62
+ expect(
63
+ container.querySelector(".seeds-metric-highlight-trend")
64
+ ).not.toBeInTheDocument();
65
+ });
66
+
67
+ it("renders the sparkline slot node in the chart area", () => {
68
+ const { container } = render(
69
+ <MetricHighlight
70
+ label="Engagement"
71
+ value="123"
72
+ sparkline={<div data-testid="custom-spark">chart</div>}
73
+ />
74
+ );
75
+
76
+ expect(screen.getByTestId("custom-spark")).toBeInTheDocument();
77
+ expect(
78
+ container.querySelector(".seeds-metric-highlight-chart")
79
+ ).toBeInTheDocument();
80
+ });
81
+
82
+ it("omits the chart area when no sparkline slot is provided", () => {
83
+ const { container } = render(
84
+ <MetricHighlight label="Engagement" value="123" />
85
+ );
86
+
87
+ expect(
88
+ container.querySelector(".seeds-metric-highlight-chart")
89
+ ).not.toBeInTheDocument();
90
+ });
91
+
92
+ it("applies the vertical, small, and bare modifiers per props", () => {
93
+ const { container } = render(
94
+ <MetricHighlight
95
+ label="Engagement"
96
+ value="123"
97
+ appearance="vertical"
98
+ size="small"
99
+ container={false}
100
+ />
101
+ );
102
+
103
+ const root = container.querySelector(".seeds-metric-highlight");
104
+ expect(root).toHaveClass("seeds-metric-highlight-vertical");
105
+ expect(root).toHaveClass("seeds-metric-highlight-small");
106
+ expect(root).toHaveClass("seeds-metric-highlight-bare");
107
+ });
108
+
109
+ it("forwards its ref to the underlying div element", () => {
110
+ const ref = React.createRef<HTMLDivElement>();
111
+ render(<MetricHighlight ref={ref} label="Engagement" value="123" />);
112
+
113
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
114
+ expect(ref.current?.tagName).toBe("DIV");
115
+ });
116
+ });
@@ -0,0 +1,5 @@
1
+ export { default as MetricHighlight } from "./MetricHighlight";
2
+ export type {
3
+ TypeMetricHighlightProps,
4
+ TypeMetricHighlightTrend,
5
+ } from "./MetricHighlightTypes";
@@ -12,7 +12,7 @@ export default meta;
12
12
  type Story = StoryObj<typeof meta>;
13
13
 
14
14
  const SlotContent = () => (
15
- <div style={{ display: "grid", gap: 8 }}>
15
+ <div className="grid gap-300">
16
16
  <strong>Engagement is up 24% this month</strong>
17
17
  <span>
18
18
  Driven by a strong week on LinkedIn and a viral reply thread. Audience
@@ -23,14 +23,14 @@ const SlotContent = () => (
23
23
 
24
24
  export const Default: Story = {
25
25
  args: {
26
- style: { width: 384 },
26
+ className: "w-96",
27
27
  children: <SlotContent />,
28
28
  },
29
29
  };
30
30
 
31
31
  export const WithoutAccent: Story = {
32
32
  args: {
33
- style: { width: 384 },
33
+ className: "w-96",
34
34
  accent: false,
35
35
  children: <SlotContent />,
36
36
  },
@@ -1,29 +1,7 @@
1
1
  import * as React from "react";
2
+ import { cn } from "../_internal/cn";
2
3
  import type { TypeNarrativeContainerProps } from "./NarrativeContainerTypes";
3
4
 
4
- /** Merge class names, supporting `{ "class": boolean }` toggles. */
5
- function cn(
6
- ...inputs: (string | undefined | null | false | Record<string, boolean>)[]
7
- ): string {
8
- const classes: string[] = [];
9
-
10
- for (const input of inputs) {
11
- if (!input) continue;
12
-
13
- if (typeof input === "string") {
14
- classes.push(input);
15
- } else if (typeof input === "object") {
16
- for (const [key, value] of Object.entries(input)) {
17
- if (value) {
18
- classes.push(key);
19
- }
20
- }
21
- }
22
- }
23
-
24
- return classes.join(" ");
25
- }
26
-
27
5
  /**
28
6
  * NarrativeContainer — the foundational surface that narrative primitives are
29
7
  * composed inside. A padded, rounded, elevated card with a content slot and an
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import NarrativeDivider from "./NarrativeDivider";
4
+ import "../narrative-divider.css";
5
+
6
+ const meta = {
7
+ title: "Narrative Kit/NarrativeDivider",
8
+ component: NarrativeDivider,
9
+ decorators: [
10
+ (Story) => (
11
+ <div style={{ width: 480 }}>
12
+ <Story />
13
+ </div>
14
+ ),
15
+ ],
16
+ } satisfies Meta<typeof NarrativeDivider>;
17
+
18
+ export default meta;
19
+ type Story = StoryObj<typeof meta>;
20
+
21
+ /** A full-width 1px rule spanning its container. */
22
+ export const Default: Story = {};
23
+
24
+ /** Sitting between two blocks of content, as it would in a brief. */
25
+ export const BetweenSections: Story = {
26
+ render: () => (
27
+ <div style={{ width: 480 }}>
28
+ <p>Insight metrics</p>
29
+ <NarrativeDivider />
30
+ <p>Headline and summary</p>
31
+ </div>
32
+ ),
33
+ };
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+ import { cn } from "../_internal/cn";
3
+ import type { TypeNarrativeDividerProps } from "./NarrativeDividerTypes";
4
+
5
+ /**
6
+ * NarrativeDivider — a full-width, 1px horizontal rule that separates sections
7
+ * within a narrative brief (e.g. a row of metrics from the headline/summary
8
+ * block below it).
9
+ *
10
+ * Renders a semantic <hr> stripped of its user-agent margins and borders, then
11
+ * draws a single 1px line with the container-border-base token (#dee1e1, which
12
+ * also tracks dark mode). Styled via narrative-divider.css (Seeds CSS custom
13
+ * properties); consumers import the classes through
14
+ * @sproutsocial/racine/css/components. Overrides come via standard className /
15
+ * style.
16
+ */
17
+ const NarrativeDivider = React.forwardRef<
18
+ HTMLHRElement,
19
+ TypeNarrativeDividerProps
20
+ >(({ className, ...props }, ref) => (
21
+ <hr
22
+ ref={ref}
23
+ className={cn("seeds-narrative-divider", className)}
24
+ {...props}
25
+ />
26
+ ));
27
+
28
+ NarrativeDivider.displayName = "NarrativeDivider";
29
+
30
+ export default NarrativeDivider;
@@ -0,0 +1,4 @@
1
+ import type * as React from "react";
2
+
3
+ /** NarrativeDivider takes no extra props — just standard <hr> attributes. */
4
+ export type TypeNarrativeDividerProps = React.HTMLAttributes<HTMLHRElement>;
@@ -0,0 +1,35 @@
1
+ import React from "react";
2
+ import { render, screen } from "@sproutsocial/seeds-react-testing-library";
3
+ import NarrativeDivider from "../NarrativeDivider";
4
+
5
+ describe("NarrativeDivider", () => {
6
+ it("renders a horizontal rule with the base class", () => {
7
+ const { container } = render(<NarrativeDivider />);
8
+
9
+ const root = container.querySelector(".seeds-narrative-divider");
10
+ expect(root).toBeInTheDocument();
11
+ expect(root?.tagName).toBe("HR");
12
+ });
13
+
14
+ it("merges a consumer className with the base class", () => {
15
+ const { container } = render(<NarrativeDivider className="custom" />);
16
+
17
+ const root = container.querySelector(".seeds-narrative-divider");
18
+ expect(root).toBeInTheDocument();
19
+ expect(root).toHaveClass("custom");
20
+ });
21
+
22
+ it("forwards extra props to the underlying element", () => {
23
+ render(<NarrativeDivider data-testid="divider" />);
24
+
25
+ expect(screen.getByTestId("divider")).toBeInTheDocument();
26
+ });
27
+
28
+ it("forwards its ref to the underlying hr element", () => {
29
+ const ref = React.createRef<HTMLHRElement>();
30
+ render(<NarrativeDivider ref={ref} />);
31
+
32
+ expect(ref.current).toBeInstanceOf(HTMLHRElement);
33
+ expect(ref.current?.tagName).toBe("HR");
34
+ });
35
+ });
@@ -0,0 +1,2 @@
1
+ export { default as NarrativeDivider } from "./NarrativeDivider";
2
+ export type { TypeNarrativeDividerProps } from "./NarrativeDividerTypes";
@@ -0,0 +1,68 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import NarrativeHeadline from "./NarrativeHeadline";
4
+ import NarrativeHeadlineHighlight from "./NarrativeHeadlineHighlight";
5
+ import NarrativeHeadlineRoll from "./NarrativeHeadlineRoll";
6
+ import "../narrative-headline.css";
7
+
8
+ const meta = {
9
+ title: "Narrative Kit/NarrativeHeadline",
10
+ component: NarrativeHeadline,
11
+ } satisfies Meta<typeof NarrativeHeadline>;
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ export const Default: Story = {
17
+ args: {
18
+ className: "w-150",
19
+ children: "Engagement is up 24% this month",
20
+ },
21
+ };
22
+
23
+ export const Highlight: Story = {
24
+ name: "With highlight",
25
+ args: {
26
+ className: "w-150",
27
+ children: (
28
+ <>
29
+ Headline with a{" "}
30
+ <NarrativeHeadlineHighlight>highlighted</NarrativeHeadlineHighlight>{" "}
31
+ word
32
+ </>
33
+ ),
34
+ },
35
+ };
36
+
37
+ export const Roll: Story = {
38
+ name: "Roll variant",
39
+ args: {
40
+ className: "w-150",
41
+ children: (
42
+ <>
43
+ The social suite for{" "}
44
+ <NarrativeHeadlineRoll
45
+ words={["marketers", "agencies", "enterprise teams", "founders"]}
46
+ />
47
+ </>
48
+ ),
49
+ },
50
+ };
51
+
52
+ export const RollDownNoGradient: Story = {
53
+ name: "Roll variant — downward, no gradient",
54
+ args: {
55
+ className: "w-150",
56
+ children: (
57
+ <>
58
+ Now tracking{" "}
59
+ <NarrativeHeadlineRoll
60
+ words={["mentions", "reach", "sentiment"]}
61
+ direction="down"
62
+ gradient={false}
63
+ intervalMs={1600}
64
+ />
65
+ </>
66
+ ),
67
+ },
68
+ };
@@ -0,0 +1,45 @@
1
+ import * as React from "react";
2
+ import { cn } from "../_internal/cn";
3
+ import type { TypeNarrativeHeadlineProps } from "./NarrativeHeadlineTypes";
4
+
5
+ /**
6
+ * NarrativeHeadline — the titling primitive for a narrative block. A single
7
+ * heading element, nothing more: supporting copy and tags/tokens belong to
8
+ * sibling primitives that compose alongside it, not baked into the title.
9
+ *
10
+ * Wrap part of the headline in `<NarrativeHeadlineHighlight>` for a static
11
+ * gradient accent, or `<NarrativeHeadlineRoll>` for the rolling "roll" variant.
12
+ *
13
+ * Styled via `narrative-headline.css` (Seeds CSS custom properties). Consumers
14
+ * import the classes through `@sproutsocial/racine/css/components`. Overrides
15
+ * are done with standard `className` / `style`.
16
+ */
17
+ const NarrativeHeadline = React.forwardRef<
18
+ HTMLHeadingElement,
19
+ TypeNarrativeHeadlineProps
20
+ >(
21
+ (
22
+ { children, headingLevel = 2, size = "default", className, ...props },
23
+ ref
24
+ ) => {
25
+ const Heading = `h${headingLevel}` as const;
26
+
27
+ return (
28
+ <Heading
29
+ ref={ref}
30
+ className={cn(
31
+ "seeds-narrative-headline",
32
+ { "seeds-narrative-headline-small": size === "small" },
33
+ className
34
+ )}
35
+ {...props}
36
+ >
37
+ {children}
38
+ </Heading>
39
+ );
40
+ }
41
+ );
42
+
43
+ NarrativeHeadline.displayName = "NarrativeHeadline";
44
+
45
+ export default NarrativeHeadline;
@@ -0,0 +1,31 @@
1
+ import * as React from "react";
2
+ import { cn } from "../_internal/cn";
3
+ import type { TypeNarrativeHeadlineHighlightProps } from "./NarrativeHeadlineTypes";
4
+
5
+ /**
6
+ * NarrativeHeadlineHighlight — paints a span of headline text with the AI
7
+ * brand gradient (the "highlighted word" treatment). Drop it inline inside a
8
+ * `<NarrativeHeadline>`:
9
+ *
10
+ * <NarrativeHeadline>
11
+ * Headline with a <NarrativeHeadlineHighlight>highlighted</NarrativeHeadlineHighlight> word
12
+ * </NarrativeHeadline>
13
+ */
14
+ const NarrativeHeadlineHighlight = React.forwardRef<
15
+ HTMLSpanElement,
16
+ TypeNarrativeHeadlineHighlightProps
17
+ >(({ children, className, ...props }, ref) => {
18
+ return (
19
+ <span
20
+ ref={ref}
21
+ className={cn("seeds-narrative-headline-highlight", className)}
22
+ {...props}
23
+ >
24
+ {children}
25
+ </span>
26
+ );
27
+ });
28
+
29
+ NarrativeHeadlineHighlight.displayName = "NarrativeHeadlineHighlight";
30
+
31
+ export default NarrativeHeadlineHighlight;