@sproutsocial/seeds-react-narrative-kit 0.2.0 → 0.4.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 +46 -0
- package/dist/ai-container.css +39 -0
- package/dist/esm/index.js +217 -91
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +232 -14
- package/dist/index.d.ts +232 -14
- package/dist/index.js +224 -93
- package/dist/index.js.map +1 -1
- package/dist/metric-highlight.css +17 -27
- package/dist/narrative-data-callout.css +103 -0
- package/dist/narrative-divider.css +27 -0
- package/dist/narrative-headline.css +7 -0
- package/dist/narrative-ordered-list.css +112 -0
- package/dist/narrative-summary.css +126 -0
- package/dist/pull-quote.css +4 -25
- package/package.json +12 -5
- package/src/AIContainer/AIContainer.stories.tsx +54 -0
- package/src/AIContainer/AIContainer.tsx +35 -0
- package/src/AIContainer/AIContainerTypes.ts +14 -0
- package/src/AIContainer/__tests__/AIContainer.test.tsx +61 -0
- package/src/AIContainer/index.ts +2 -0
- package/src/EyebrowToken/EyebrowToken.tsx +1 -5
- package/src/MetricHighlight/MetricHighlight.stories.tsx +37 -14
- package/src/MetricHighlight/MetricHighlight.tsx +19 -99
- package/src/MetricHighlight/MetricHighlightTypes.ts +6 -9
- package/src/MetricHighlight/__tests__/MetricHighlight.test.tsx +13 -48
- package/src/NarrativeDataCallout/NarrativeDataCallout.stories.tsx +71 -0
- package/src/NarrativeDataCallout/NarrativeDataCallout.tsx +50 -0
- package/src/NarrativeDataCallout/NarrativeDataCalloutTypes.ts +18 -0
- package/src/NarrativeDataCallout/__tests__/NarrativeDataCallout.test.tsx +69 -0
- package/src/NarrativeDataCallout/index.ts +2 -0
- 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.tsx +21 -12
- package/src/NarrativeHeadline/NarrativeHeadlineRoll.tsx +4 -1
- package/src/NarrativeHeadline/NarrativeHeadlineTypes.ts +9 -0
- package/src/NarrativeHeadline/__tests__/NarrativeHeadline.test.tsx +2 -7
- package/src/NarrativeHeadline/index.ts +1 -0
- package/src/NarrativeOrderedList/NarrativeOrderedList.stories.tsx +69 -0
- package/src/NarrativeOrderedList/NarrativeOrderedList.tsx +74 -0
- package/src/NarrativeOrderedList/NarrativeOrderedListTypes.ts +39 -0
- package/src/NarrativeOrderedList/__tests__/NarrativeOrderedList.test.tsx +89 -0
- package/src/NarrativeOrderedList/index.ts +6 -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 +174 -36
- package/src/PullQuote/PullQuote.stories.tsx +4 -1
- package/src/PullQuote/PullQuote.tsx +6 -3
- package/src/PullQuote/PullQuoteTypes.ts +1 -2
- package/src/ai-container.css +39 -0
- package/src/index.ts +25 -0
- package/src/metric-highlight.css +17 -27
- package/src/narrative-data-callout.css +103 -0
- package/src/narrative-divider.css +27 -0
- package/src/narrative-headline.css +7 -0
- package/src/narrative-ordered-list.css +112 -0
- package/src/narrative-summary.css +126 -0
- package/src/pull-quote.css +4 -25
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { SparklineChart } from "@sproutsocial/seeds-react-data-viz/sparkline";
|
|
3
4
|
import MetricHighlight from "./MetricHighlight";
|
|
4
5
|
import "../metric-highlight.css";
|
|
6
|
+
import "@sproutsocial/seeds-react-data-viz/sparkline.css";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* MetricHighlight has no built-in chart — the `sparkline` slot accepts any node.
|
|
10
|
+
* These stories inject the real `SparklineChart` from
|
|
11
|
+
* `@sproutsocial/seeds-react-data-viz`; narrative-kit takes no runtime
|
|
12
|
+
* dependency on data-viz (only these stories do, via a devDependency). The
|
|
13
|
+
* injected chart fills the slot with `className="h-full w-full"`.
|
|
14
|
+
*/
|
|
15
|
+
const RISING = [4, 6, 5, 8, 7, 10];
|
|
16
|
+
const FALLING = [18, 16, 17, 13, 14, 10];
|
|
17
|
+
|
|
18
|
+
const spark = (
|
|
19
|
+
data: number[],
|
|
20
|
+
variant: "line" | "bar" = "line"
|
|
21
|
+
): React.ReactNode => (
|
|
22
|
+
<SparklineChart
|
|
23
|
+
variant={variant}
|
|
24
|
+
description="Weekly trend"
|
|
25
|
+
data={data}
|
|
26
|
+
className="h-full w-full"
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
5
29
|
|
|
6
30
|
const meta = {
|
|
7
31
|
title: "Narrative Kit/MetricHighlight",
|
|
@@ -10,16 +34,14 @@ const meta = {
|
|
|
10
34
|
label: "Insight metric",
|
|
11
35
|
value: "123",
|
|
12
36
|
trend: { direction: "up", value: "240%", "aria-label": "up 240%" },
|
|
37
|
+
sparkline: spark(RISING),
|
|
13
38
|
},
|
|
14
39
|
} satisfies Meta<typeof MetricHighlight>;
|
|
15
40
|
|
|
16
41
|
export default meta;
|
|
17
42
|
type Story = StoryObj<typeof meta>;
|
|
18
43
|
|
|
19
|
-
/**
|
|
20
|
-
* Horizontal, default size, padded surface, numeric trend badge, and the
|
|
21
|
-
* default sparkline (no `data` passed).
|
|
22
|
-
*/
|
|
44
|
+
/** Horizontal, default size, padded surface, numeric trend badge, and a sparkline. */
|
|
23
45
|
export const Default: Story = {
|
|
24
46
|
args: {
|
|
25
47
|
className: "w-80",
|
|
@@ -50,38 +72,39 @@ export const WithoutContainer: Story = {
|
|
|
50
72
|
},
|
|
51
73
|
};
|
|
52
74
|
|
|
53
|
-
/** Metric
|
|
75
|
+
/** Metric on its own — omit the `sparkline` slot to drop the chart. */
|
|
54
76
|
export const WithoutChart: Story = {
|
|
55
77
|
args: {
|
|
56
78
|
className: "w-80",
|
|
57
|
-
|
|
79
|
+
sparkline: undefined,
|
|
58
80
|
},
|
|
59
81
|
};
|
|
60
82
|
|
|
61
83
|
/**
|
|
62
|
-
* Negative trend — the badge swaps to the red treatment and the
|
|
63
|
-
*
|
|
84
|
+
* Negative trend — the badge swaps to the red treatment and the sparkline (which
|
|
85
|
+
* auto-colors from its data) slopes downward in red.
|
|
64
86
|
*/
|
|
65
87
|
export const TrendDown: Story = {
|
|
66
88
|
args: {
|
|
67
89
|
className: "w-80",
|
|
68
90
|
value: "87",
|
|
69
91
|
trend: { direction: "down", value: "12%", "aria-label": "down 12%" },
|
|
92
|
+
sparkline: spark(FALLING),
|
|
70
93
|
},
|
|
71
94
|
};
|
|
72
95
|
|
|
73
|
-
/**
|
|
74
|
-
export const
|
|
96
|
+
/** Icon-only trend badge (no number). */
|
|
97
|
+
export const TrendIconOnly: Story = {
|
|
75
98
|
args: {
|
|
76
99
|
className: "w-80",
|
|
77
|
-
|
|
100
|
+
trend: { direction: "up", "aria-label": "trending up" },
|
|
78
101
|
},
|
|
79
102
|
};
|
|
80
103
|
|
|
81
|
-
/**
|
|
82
|
-
export const
|
|
104
|
+
/** The slot accepts the bar variant of the sparkline just as well. */
|
|
105
|
+
export const BarSparkline: Story = {
|
|
83
106
|
args: {
|
|
84
107
|
className: "w-80",
|
|
85
|
-
|
|
108
|
+
sparkline: spark(RISING, "bar"),
|
|
86
109
|
},
|
|
87
110
|
};
|
|
@@ -5,91 +5,6 @@ import type {
|
|
|
5
5
|
TypeMetricHighlightTrend,
|
|
6
6
|
} from "./MetricHighlightTypes";
|
|
7
7
|
|
|
8
|
-
/** Fixed viewBox the sparkline normalizes into; CSS stretches it to fit. */
|
|
9
|
-
const SPARK_VIEWBOX_WIDTH = 100;
|
|
10
|
-
const SPARK_VIEWBOX_HEIGHT = 32;
|
|
11
|
-
/** Inset so the stroke at the min/max points isn't clipped by the viewBox edge. */
|
|
12
|
-
const SPARK_PADDING = 2;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Default sparkline series used when the consumer doesn't pass `data` — a gently
|
|
16
|
-
* rising wave. It's reversed for a downward trend so the line slopes to match.
|
|
17
|
-
*/
|
|
18
|
-
const DEFAULT_SPARK = [6, 10, 8, 13, 11, 16];
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Build a smooth SVG path through `points` using a Catmull-Rom spline converted
|
|
22
|
-
* to cubic béziers. Each segment's control points are derived from its
|
|
23
|
-
* neighbors, so the curve passes through every data point with soft, rounded
|
|
24
|
-
* transitions (matching the design) rather than sharp polyline corners.
|
|
25
|
-
*/
|
|
26
|
-
const smoothPath = (points: { x: number; y: number }[]) => {
|
|
27
|
-
if (points.length < 2) {
|
|
28
|
-
const p = points[0];
|
|
29
|
-
return p ? `M ${p.x},${p.y}` : "";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let d = `M ${points[0]!.x.toFixed(2)},${points[0]!.y.toFixed(2)}`;
|
|
33
|
-
for (let i = 0; i < points.length - 1; i++) {
|
|
34
|
-
// The loop bound guarantees p1 and p2; p0/p3 clamp to the endpoints.
|
|
35
|
-
const p1 = points[i]!;
|
|
36
|
-
const p2 = points[i + 1]!;
|
|
37
|
-
const p0 = points[i - 1] ?? p1;
|
|
38
|
-
const p3 = points[i + 2] ?? p2;
|
|
39
|
-
// Catmull-Rom → bézier control points (tension 1/6).
|
|
40
|
-
const cp1x = p1.x + (p2.x - p0.x) / 6;
|
|
41
|
-
const cp1y = p1.y + (p2.y - p0.y) / 6;
|
|
42
|
-
const cp2x = p2.x - (p3.x - p1.x) / 6;
|
|
43
|
-
const cp2y = p2.y - (p3.y - p1.y) / 6;
|
|
44
|
-
d += ` C ${cp1x.toFixed(2)},${cp1y.toFixed(2)} ${cp2x.toFixed(
|
|
45
|
-
2
|
|
46
|
-
)},${cp2y.toFixed(2)} ${p2.x.toFixed(2)},${p2.y.toFixed(2)}`;
|
|
47
|
-
}
|
|
48
|
-
return d;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Sparkline — an inline SVG path normalized from `data` into a fixed viewBox and
|
|
53
|
-
* smoothed into a soft curve. `preserveAspectRatio="none"` plus a non-scaling
|
|
54
|
-
* stroke (set in CSS) lets a single path stretch to any width/height while
|
|
55
|
-
* keeping a crisp line.
|
|
56
|
-
*/
|
|
57
|
-
const Sparkline = ({
|
|
58
|
-
data,
|
|
59
|
-
negative,
|
|
60
|
-
}: {
|
|
61
|
-
data: number[];
|
|
62
|
-
negative?: boolean;
|
|
63
|
-
}) => {
|
|
64
|
-
const min = Math.min(...data);
|
|
65
|
-
const max = Math.max(...data);
|
|
66
|
-
const range = max - min || 1;
|
|
67
|
-
const usableHeight = SPARK_VIEWBOX_HEIGHT - SPARK_PADDING * 2;
|
|
68
|
-
|
|
69
|
-
const points = data.map((value, i) => ({
|
|
70
|
-
x:
|
|
71
|
-
data.length === 1
|
|
72
|
-
? SPARK_VIEWBOX_WIDTH / 2
|
|
73
|
-
: (i / (data.length - 1)) * SPARK_VIEWBOX_WIDTH,
|
|
74
|
-
// Invert y so larger values sit higher in the chart.
|
|
75
|
-
y: SPARK_PADDING + (1 - (value - min) / range) * usableHeight,
|
|
76
|
-
}));
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<svg
|
|
80
|
-
className={cn("seeds-metric-highlight-spark", {
|
|
81
|
-
"seeds-metric-highlight-spark-negative": Boolean(negative),
|
|
82
|
-
})}
|
|
83
|
-
viewBox={`0 0 ${SPARK_VIEWBOX_WIDTH} ${SPARK_VIEWBOX_HEIGHT}`}
|
|
84
|
-
preserveAspectRatio="none"
|
|
85
|
-
aria-hidden
|
|
86
|
-
focusable={false}
|
|
87
|
-
>
|
|
88
|
-
<path d={smoothPath(points)} fill="none" strokeLinecap="round" />
|
|
89
|
-
</svg>
|
|
90
|
-
);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
8
|
/** Directional arrow glyph for the trend badge (decorative; `aria-hidden`). */
|
|
94
9
|
const TrendArrow = ({ direction }: { direction: "up" | "down" }) => (
|
|
95
10
|
<svg
|
|
@@ -116,7 +31,11 @@ const TrendArrow = ({ direction }: { direction: "up" | "down" }) => (
|
|
|
116
31
|
</svg>
|
|
117
32
|
);
|
|
118
33
|
|
|
119
|
-
const TrendBadge = ({
|
|
34
|
+
const TrendBadge = ({
|
|
35
|
+
direction = "up",
|
|
36
|
+
value,
|
|
37
|
+
...rest
|
|
38
|
+
}: TypeMetricHighlightTrend) => (
|
|
120
39
|
<span
|
|
121
40
|
className={cn("seeds-metric-highlight-trend", {
|
|
122
41
|
"seeds-metric-highlight-trend-down": direction === "down",
|
|
@@ -133,21 +52,25 @@ const TrendBadge = ({ direction = "up", value, ...rest }: TypeMetricHighlightTre
|
|
|
133
52
|
/**
|
|
134
53
|
* MetricHighlight — a narrative primitive that spotlights a single metric: a
|
|
135
54
|
* label, a large monospace value, an optional trend badge (a directional arrow
|
|
136
|
-
* plus an optional number such as "240%"), and an optional
|
|
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`).
|
|
137
58
|
*
|
|
138
59
|
* Styled via `metric-highlight.css` (Seeds CSS custom properties); consumers
|
|
139
60
|
* import the classes through `@sproutsocial/racine/css/components`. Layout flexes
|
|
140
61
|
* across `appearance` (horizontal/vertical) and `size` (default/small); surface
|
|
141
62
|
* overrides are done with standard `className` / `style`.
|
|
142
63
|
*/
|
|
143
|
-
const MetricHighlight = React.forwardRef<
|
|
64
|
+
const MetricHighlight = React.forwardRef<
|
|
65
|
+
HTMLDivElement,
|
|
66
|
+
TypeMetricHighlightProps
|
|
67
|
+
>(
|
|
144
68
|
(
|
|
145
69
|
{
|
|
146
70
|
label,
|
|
147
71
|
value,
|
|
148
72
|
trend,
|
|
149
|
-
|
|
150
|
-
chart = true,
|
|
73
|
+
sparkline,
|
|
151
74
|
appearance = "horizontal",
|
|
152
75
|
size = "default",
|
|
153
76
|
container = true,
|
|
@@ -156,14 +79,6 @@ const MetricHighlight = React.forwardRef<HTMLDivElement, TypeMetricHighlightProp
|
|
|
156
79
|
},
|
|
157
80
|
ref
|
|
158
81
|
) => {
|
|
159
|
-
const isDown = trend?.direction === "down";
|
|
160
|
-
// Fall back to a sensible default series, sloped to match the trend, so the
|
|
161
|
-
// chart renders without the consumer wiring up data. An explicit `data`
|
|
162
|
-
// (including an empty array to opt out) always wins.
|
|
163
|
-
const resolvedData =
|
|
164
|
-
data ?? (isDown ? [...DEFAULT_SPARK].reverse() : DEFAULT_SPARK);
|
|
165
|
-
const showChart = chart && resolvedData.length > 0;
|
|
166
|
-
|
|
167
82
|
return (
|
|
168
83
|
<div
|
|
169
84
|
ref={ref}
|
|
@@ -185,7 +100,12 @@ const MetricHighlight = React.forwardRef<HTMLDivElement, TypeMetricHighlightProp
|
|
|
185
100
|
{trend != null && <TrendBadge {...trend} />}
|
|
186
101
|
</div>
|
|
187
102
|
</div>
|
|
188
|
-
{
|
|
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
|
+
)}
|
|
189
109
|
</div>
|
|
190
110
|
);
|
|
191
111
|
}
|
|
@@ -28,16 +28,13 @@ export interface TypeMetricHighlightProps
|
|
|
28
28
|
/** Optional trend badge — a directional arrow plus an optional value. */
|
|
29
29
|
trend?: TypeMetricHighlightTrend;
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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).
|
|
34
36
|
*/
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Render the sparkline.
|
|
38
|
-
* @default true
|
|
39
|
-
*/
|
|
40
|
-
chart?: boolean;
|
|
37
|
+
sparkline?: React.ReactNode;
|
|
41
38
|
/**
|
|
42
39
|
* Layout direction.
|
|
43
40
|
* - `horizontal` — metric block beside the sparkline.
|
|
@@ -2,8 +2,6 @@ import React from "react";
|
|
|
2
2
|
import { render, screen } from "@sproutsocial/seeds-react-testing-library";
|
|
3
3
|
import MetricHighlight from "../MetricHighlight";
|
|
4
4
|
|
|
5
|
-
const DATA = [1, 4, 2, 6, 3];
|
|
6
|
-
|
|
7
5
|
describe("MetricHighlight", () => {
|
|
8
6
|
it("renders the label and value", () => {
|
|
9
7
|
render(<MetricHighlight label="Engagement" value="123" />);
|
|
@@ -33,7 +31,9 @@ describe("MetricHighlight", () => {
|
|
|
33
31
|
/>
|
|
34
32
|
);
|
|
35
33
|
|
|
36
|
-
expect(
|
|
34
|
+
expect(
|
|
35
|
+
container.querySelector(".seeds-metric-highlight-trend")
|
|
36
|
+
).toBeInTheDocument();
|
|
37
37
|
expect(
|
|
38
38
|
container.querySelector(".seeds-metric-highlight-trend-value")
|
|
39
39
|
).not.toBeInTheDocument();
|
|
@@ -54,32 +54,6 @@ describe("MetricHighlight", () => {
|
|
|
54
54
|
).toBeInTheDocument();
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
it("marks the sparkline negative for a downward trend", () => {
|
|
58
|
-
const { container, rerender } = render(
|
|
59
|
-
<MetricHighlight
|
|
60
|
-
label="Engagement"
|
|
61
|
-
value="123"
|
|
62
|
-
data={DATA}
|
|
63
|
-
trend={{ direction: "down", value: "12%" }}
|
|
64
|
-
/>
|
|
65
|
-
);
|
|
66
|
-
expect(
|
|
67
|
-
container.querySelector(".seeds-metric-highlight-spark-negative")
|
|
68
|
-
).toBeInTheDocument();
|
|
69
|
-
|
|
70
|
-
rerender(
|
|
71
|
-
<MetricHighlight
|
|
72
|
-
label="Engagement"
|
|
73
|
-
value="123"
|
|
74
|
-
data={DATA}
|
|
75
|
-
trend={{ direction: "up", value: "12%" }}
|
|
76
|
-
/>
|
|
77
|
-
);
|
|
78
|
-
expect(
|
|
79
|
-
container.querySelector(".seeds-metric-highlight-spark-negative")
|
|
80
|
-
).not.toBeInTheDocument();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
57
|
it("omits the trend badge when no trend is provided", () => {
|
|
84
58
|
const { container } = render(
|
|
85
59
|
<MetricHighlight label="Engagement" value="123" />
|
|
@@ -90,37 +64,28 @@ describe("MetricHighlight", () => {
|
|
|
90
64
|
).not.toBeInTheDocument();
|
|
91
65
|
});
|
|
92
66
|
|
|
93
|
-
it("renders the sparkline
|
|
67
|
+
it("renders the sparkline slot node in the chart area", () => {
|
|
94
68
|
const { container } = render(
|
|
95
|
-
<MetricHighlight
|
|
69
|
+
<MetricHighlight
|
|
70
|
+
label="Engagement"
|
|
71
|
+
value="123"
|
|
72
|
+
sparkline={<div data-testid="custom-spark">chart</div>}
|
|
73
|
+
/>
|
|
96
74
|
);
|
|
97
75
|
|
|
76
|
+
expect(screen.getByTestId("custom-spark")).toBeInTheDocument();
|
|
98
77
|
expect(
|
|
99
|
-
container.querySelector(".seeds-metric-highlight-
|
|
78
|
+
container.querySelector(".seeds-metric-highlight-chart")
|
|
100
79
|
).toBeInTheDocument();
|
|
101
80
|
});
|
|
102
81
|
|
|
103
|
-
it("
|
|
82
|
+
it("omits the chart area when no sparkline slot is provided", () => {
|
|
104
83
|
const { container } = render(
|
|
105
84
|
<MetricHighlight label="Engagement" value="123" />
|
|
106
85
|
);
|
|
107
86
|
|
|
108
87
|
expect(
|
|
109
|
-
container.querySelector(".seeds-metric-highlight-
|
|
110
|
-
).toBeInTheDocument();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("omits the sparkline when chart is false or data is empty", () => {
|
|
114
|
-
const { container, rerender } = render(
|
|
115
|
-
<MetricHighlight label="Engagement" value="123" data={DATA} chart={false} />
|
|
116
|
-
);
|
|
117
|
-
expect(
|
|
118
|
-
container.querySelector(".seeds-metric-highlight-spark")
|
|
119
|
-
).not.toBeInTheDocument();
|
|
120
|
-
|
|
121
|
-
rerender(<MetricHighlight label="Engagement" value="123" data={[]} />);
|
|
122
|
-
expect(
|
|
123
|
-
container.querySelector(".seeds-metric-highlight-spark")
|
|
88
|
+
container.querySelector(".seeds-metric-highlight-chart")
|
|
124
89
|
).not.toBeInTheDocument();
|
|
125
90
|
});
|
|
126
91
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { DonutChart } from "@sproutsocial/seeds-react-data-viz/donut";
|
|
4
|
+
import NarrativeDataCallout from "./NarrativeDataCallout";
|
|
5
|
+
import NarrativeContainer from "../NarrativeContainer/NarrativeContainer";
|
|
6
|
+
import "../narrative-data-callout.css";
|
|
7
|
+
import "../narrative-container.css";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* NarrativeDataCallout has no built-in chart — the `visual` slot accepts any
|
|
11
|
+
* node. These stories inject the real v2 `DonutChart` from
|
|
12
|
+
* `@sproutsocial/seeds-react-data-viz/donut`; narrative-kit takes no runtime
|
|
13
|
+
* dependency on data-viz (only these stories do, via a devDependency). The
|
|
14
|
+
* injected chart fills the left third; the copy fills the right two-thirds.
|
|
15
|
+
*/
|
|
16
|
+
const donut = (): React.ReactNode => (
|
|
17
|
+
<DonutChart
|
|
18
|
+
description="Completion rate: 80% complete"
|
|
19
|
+
data={[
|
|
20
|
+
{ name: "Complete", value: 80 },
|
|
21
|
+
{ name: "Remaining", value: 20 },
|
|
22
|
+
]}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const BODY =
|
|
27
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio leo, " +
|
|
28
|
+
"gravida vitae sodales vitae, tempor eget ligula. Vestibulum nec enim eget " +
|
|
29
|
+
"mi placerat finibus. Donec tempor tincidunt elit.";
|
|
30
|
+
|
|
31
|
+
const meta = {
|
|
32
|
+
title: "Narrative Kit/NarrativeDataCallout",
|
|
33
|
+
component: NarrativeDataCallout,
|
|
34
|
+
args: {
|
|
35
|
+
visual: donut(),
|
|
36
|
+
children: BODY,
|
|
37
|
+
},
|
|
38
|
+
} satisfies Meta<typeof NarrativeDataCallout>;
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
type Story = StoryObj<typeof meta>;
|
|
42
|
+
|
|
43
|
+
/** Bare inner content — the visual on the left third, copy on the right. */
|
|
44
|
+
export const Default: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
className: "w-full max-w-240",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Dropped into a `NarrativeContainer` to reproduce the gradient-accented card
|
|
52
|
+
* from the design. This is the intended composition — the callout itself carries
|
|
53
|
+
* no surface.
|
|
54
|
+
*/
|
|
55
|
+
export const InContainer: Story = {
|
|
56
|
+
render: (args) => (
|
|
57
|
+
<NarrativeContainer className="w-full max-w-240">
|
|
58
|
+
<NarrativeDataCallout {...args} />
|
|
59
|
+
</NarrativeContainer>
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Narrow width — the container query collapses the row to a stacked column
|
|
65
|
+
* (visual on top, copy below). Resize the canvas to watch it reflow.
|
|
66
|
+
*/
|
|
67
|
+
export const Stacked: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
className: "w-80",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../_internal/cn";
|
|
3
|
+
import type { TypeNarrativeDataCalloutProps } from "./NarrativeDataCalloutTypes";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* NarrativeDataCallout — a horizontally oriented callout that pairs an injected
|
|
7
|
+
* data-viz visual (left, ~1/3 width) with a block of body copy (right, ~2/3).
|
|
8
|
+
*
|
|
9
|
+
* The kit ships no chart of its own; the `visual` slot accepts any node — the
|
|
10
|
+
* stories drop in a v2 `DonutChart` from `@sproutsocial/seeds-react-data-viz`.
|
|
11
|
+
* narrative-kit takes no runtime dependency on data-viz (mirrors MetricHighlight's
|
|
12
|
+
* `sparkline` slot).
|
|
13
|
+
*
|
|
14
|
+
* Renders inner content only — it carries no surface, shadow, or accent of its
|
|
15
|
+
* own. Drop it into a `NarrativeContainer` (see the stories) to reproduce the
|
|
16
|
+
* card look.
|
|
17
|
+
*
|
|
18
|
+
* Layout is a flex row driven by a container query, so it collapses to a stacked
|
|
19
|
+
* column (visual on top, text below) based on the width the component is actually
|
|
20
|
+
* given, not the viewport.
|
|
21
|
+
*
|
|
22
|
+
* Styled via `narrative-data-callout.css` (Seeds CSS custom properties); consumers
|
|
23
|
+
* import the classes through `@sproutsocial/racine/css/components`. Overrides come
|
|
24
|
+
* via standard `className` / `style`.
|
|
25
|
+
*/
|
|
26
|
+
const NarrativeDataCallout = React.forwardRef<
|
|
27
|
+
HTMLDivElement,
|
|
28
|
+
TypeNarrativeDataCalloutProps
|
|
29
|
+
>(({ visual, children, visualLabel, className, ...props }, ref) => (
|
|
30
|
+
<div
|
|
31
|
+
ref={ref}
|
|
32
|
+
className={cn("seeds-narrative-data-callout", className)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
<div className="seeds-narrative-data-callout-row">
|
|
36
|
+
<div
|
|
37
|
+
className="seeds-narrative-data-callout-visual"
|
|
38
|
+
role={visualLabel != null ? "img" : undefined}
|
|
39
|
+
aria-label={visualLabel}
|
|
40
|
+
>
|
|
41
|
+
{visual}
|
|
42
|
+
</div>
|
|
43
|
+
<div className="seeds-narrative-data-callout-body">{children}</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
));
|
|
47
|
+
|
|
48
|
+
NarrativeDataCallout.displayName = "NarrativeDataCallout";
|
|
49
|
+
|
|
50
|
+
export default NarrativeDataCallout;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
|
|
3
|
+
export interface TypeNarrativeDataCalloutProps
|
|
4
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/**
|
|
6
|
+
* Data-viz component rendered in the left slot (occupies ~1/3 of the width).
|
|
7
|
+
* The kit ships no chart of its own — drop in any data-viz node, e.g. a v2
|
|
8
|
+
* `DonutChart` from `@sproutsocial/seeds-react-data-viz`.
|
|
9
|
+
*/
|
|
10
|
+
visual: React.ReactNode;
|
|
11
|
+
/** Body copy / description occupying the right two-thirds. */
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Accessible label for the visual slot region. Omit when the injected visual
|
|
15
|
+
* already carries its own description/label.
|
|
16
|
+
*/
|
|
17
|
+
visualLabel?: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@sproutsocial/seeds-react-testing-library";
|
|
3
|
+
import NarrativeDataCallout from "../NarrativeDataCallout";
|
|
4
|
+
|
|
5
|
+
describe("NarrativeDataCallout", () => {
|
|
6
|
+
it("renders the body copy", () => {
|
|
7
|
+
render(
|
|
8
|
+
<NarrativeDataCallout visual={<div>chart</div>}>
|
|
9
|
+
Engagement climbed this month.
|
|
10
|
+
</NarrativeDataCallout>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
expect(
|
|
14
|
+
screen.getByText("Engagement climbed this month.")
|
|
15
|
+
).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("renders the injected visual node in the visual slot", () => {
|
|
19
|
+
const { container } = render(
|
|
20
|
+
<NarrativeDataCallout visual={<div data-testid="custom-viz">chart</div>}>
|
|
21
|
+
Body
|
|
22
|
+
</NarrativeDataCallout>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(screen.getByTestId("custom-viz")).toBeInTheDocument();
|
|
26
|
+
expect(
|
|
27
|
+
container.querySelector(".seeds-narrative-data-callout-visual")
|
|
28
|
+
).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("applies the root class and merges a custom className", () => {
|
|
32
|
+
const { container } = render(
|
|
33
|
+
<NarrativeDataCallout visual={<div>chart</div>} className="custom">
|
|
34
|
+
Body
|
|
35
|
+
</NarrativeDataCallout>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const root = container.querySelector(".seeds-narrative-data-callout");
|
|
39
|
+
expect(root).toBeInTheDocument();
|
|
40
|
+
expect(root).toHaveClass("custom");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("labels the visual region when visualLabel is provided", () => {
|
|
44
|
+
render(
|
|
45
|
+
<NarrativeDataCallout
|
|
46
|
+
visual={<div>chart</div>}
|
|
47
|
+
visualLabel="Completion rate"
|
|
48
|
+
>
|
|
49
|
+
Body
|
|
50
|
+
</NarrativeDataCallout>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(
|
|
54
|
+
screen.getByRole("img", { name: "Completion rate" })
|
|
55
|
+
).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("forwards its ref to the underlying div element", () => {
|
|
59
|
+
const ref = React.createRef<HTMLDivElement>();
|
|
60
|
+
render(
|
|
61
|
+
<NarrativeDataCallout ref={ref} visual={<div>chart</div>}>
|
|
62
|
+
Body
|
|
63
|
+
</NarrativeDataCallout>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(ref.current).toBeInstanceOf(HTMLDivElement);
|
|
67
|
+
expect(ref.current?.tagName).toBe("DIV");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -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;
|