@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.
- package/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +28 -0
- package/dist/esm/index.js +324 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/eyebrow-token.css +37 -0
- package/dist/index.d.mts +304 -1
- package/dist/index.d.ts +304 -1
- package/dist/index.js +333 -3
- package/dist/index.js.map +1 -1
- package/dist/metric-highlight.css +168 -0
- package/dist/narrative-container.css +1 -1
- package/dist/narrative-divider.css +27 -0
- package/dist/narrative-headline.css +132 -0
- package/dist/narrative-summary.css +126 -0
- package/dist/pull-quote.css +89 -0
- package/package.json +13 -3
- package/src/EyebrowToken/EyebrowToken.stories.tsx +25 -0
- package/src/EyebrowToken/EyebrowToken.tsx +25 -0
- package/src/EyebrowToken/EyebrowTokenTypes.ts +7 -0
- package/src/EyebrowToken/__tests__/EyebrowToken.test.tsx +35 -0
- package/src/EyebrowToken/index.ts +2 -0
- package/src/MetricHighlight/MetricHighlight.stories.tsx +110 -0
- package/src/MetricHighlight/MetricHighlight.tsx +116 -0
- package/src/MetricHighlight/MetricHighlightTypes.ts +58 -0
- package/src/MetricHighlight/__tests__/MetricHighlight.test.tsx +116 -0
- package/src/MetricHighlight/index.ts +5 -0
- package/src/NarrativeContainer/NarrativeContainer.stories.tsx +3 -3
- package/src/NarrativeContainer/NarrativeContainer.tsx +1 -23
- package/src/NarrativeDivider/NarrativeDivider.stories.tsx +33 -0
- package/src/NarrativeDivider/NarrativeDivider.tsx +30 -0
- package/src/NarrativeDivider/NarrativeDividerTypes.ts +4 -0
- package/src/NarrativeDivider/__tests__/NarrativeDivider.test.tsx +35 -0
- package/src/NarrativeDivider/index.ts +2 -0
- package/src/NarrativeHeadline/NarrativeHeadline.stories.tsx +68 -0
- package/src/NarrativeHeadline/NarrativeHeadline.tsx +45 -0
- package/src/NarrativeHeadline/NarrativeHeadlineHighlight.tsx +31 -0
- package/src/NarrativeHeadline/NarrativeHeadlineRoll.tsx +103 -0
- package/src/NarrativeHeadline/NarrativeHeadlineTypes.ts +63 -0
- package/src/NarrativeHeadline/__tests__/NarrativeHeadline.test.tsx +133 -0
- package/src/NarrativeHeadline/index.ts +10 -0
- package/src/NarrativeSummary/NarrativeSummary.stories.tsx +79 -0
- package/src/NarrativeSummary/NarrativeSummary.tsx +98 -0
- package/src/NarrativeSummary/NarrativeSummaryTypes.ts +59 -0
- package/src/NarrativeSummary/__tests__/NarrativeSummary.test.tsx +99 -0
- package/src/NarrativeSummary/index.ts +5 -0
- package/src/Playground/Playground.stories.tsx +163 -0
- package/src/PullQuote/PullQuote.stories.tsx +45 -0
- package/src/PullQuote/PullQuote.tsx +42 -0
- package/src/PullQuote/PullQuoteTypes.ts +18 -0
- package/src/PullQuote/__tests__/PullQuote.test.tsx +48 -0
- package/src/PullQuote/index.ts +2 -0
- package/src/_internal/cn.ts +22 -0
- package/src/eyebrow-token.css +37 -0
- package/src/index.ts +31 -0
- package/src/metric-highlight.css +168 -0
- package/src/narrative-container.css +1 -1
- package/src/narrative-divider.css +27 -0
- package/src/narrative-headline.css +132 -0
- package/src/narrative-summary.css +126 -0
- 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
|
+
});
|
|
@@ -12,7 +12,7 @@ export default meta;
|
|
|
12
12
|
type Story = StoryObj<typeof meta>;
|
|
13
13
|
|
|
14
14
|
const SlotContent = () => (
|
|
15
|
-
<div
|
|
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
|
-
|
|
26
|
+
className: "w-96",
|
|
27
27
|
children: <SlotContent />,
|
|
28
28
|
},
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
export const WithoutAccent: Story = {
|
|
32
32
|
args: {
|
|
33
|
-
|
|
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,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,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;
|