@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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@sproutsocial/seeds-react-testing-library";
|
|
3
|
+
import NarrativeSummary from "../NarrativeSummary";
|
|
4
|
+
|
|
5
|
+
const baseProps = {
|
|
6
|
+
headline: "Headline goes here",
|
|
7
|
+
summary: "A short summary.",
|
|
8
|
+
keyThemes: ["Theme one", "Theme two"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe("NarrativeSummary", () => {
|
|
12
|
+
it("renders the headline, eyebrow, summary, and key themes", () => {
|
|
13
|
+
render(<NarrativeSummary {...baseProps} eyebrow="June 10th" />);
|
|
14
|
+
|
|
15
|
+
expect(screen.getByText("June 10th")).toBeInTheDocument();
|
|
16
|
+
expect(
|
|
17
|
+
screen.getByRole("heading", { name: "Headline goes here" })
|
|
18
|
+
).toBeInTheDocument();
|
|
19
|
+
expect(screen.getByText("A short summary.")).toBeInTheDocument();
|
|
20
|
+
expect(screen.getByText("Theme one")).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByText("Theme two")).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders the headline at the small size", () => {
|
|
25
|
+
const { container } = render(<NarrativeSummary {...baseProps} />);
|
|
26
|
+
|
|
27
|
+
expect(
|
|
28
|
+
container.querySelector(".seeds-narrative-headline-small")
|
|
29
|
+
).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("uses the default section labels and allows overrides", () => {
|
|
33
|
+
const { rerender } = render(<NarrativeSummary {...baseProps} />);
|
|
34
|
+
expect(screen.getByText("Summary")).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText("Key themes")).toBeInTheDocument();
|
|
36
|
+
|
|
37
|
+
rerender(
|
|
38
|
+
<NarrativeSummary
|
|
39
|
+
{...baseProps}
|
|
40
|
+
summaryLabel="Overview"
|
|
41
|
+
keyThemesLabel="Highlights"
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
expect(screen.getByText("Overview")).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByText("Highlights")).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders the key themes as list items", () => {
|
|
49
|
+
render(<NarrativeSummary {...baseProps} />);
|
|
50
|
+
|
|
51
|
+
expect(screen.getAllByRole("listitem")).toHaveLength(2);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("renders the action only when provided", () => {
|
|
55
|
+
const { rerender } = render(<NarrativeSummary {...baseProps} />);
|
|
56
|
+
expect(
|
|
57
|
+
screen.queryByRole("button", { name: "Action" })
|
|
58
|
+
).not.toBeInTheDocument();
|
|
59
|
+
|
|
60
|
+
rerender(
|
|
61
|
+
<NarrativeSummary {...baseProps} action={<button>Action</button>} />
|
|
62
|
+
);
|
|
63
|
+
expect(screen.getByRole("button", { name: "Action" })).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("omits a section when its content is empty", () => {
|
|
67
|
+
render(<NarrativeSummary headline="Only summary" summary="Just this." />);
|
|
68
|
+
|
|
69
|
+
expect(screen.getByText("Summary")).toBeInTheDocument();
|
|
70
|
+
expect(screen.queryByText("Key themes")).not.toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("applies the two-column modifier class", () => {
|
|
74
|
+
const { container } = render(
|
|
75
|
+
<NarrativeSummary {...baseProps} layout="two-column" />
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(
|
|
79
|
+
container.querySelector(".seeds-narrative-summary-two-column")
|
|
80
|
+
).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("merges a consumer className with the base class", () => {
|
|
84
|
+
const { container } = render(
|
|
85
|
+
<NarrativeSummary {...baseProps} className="custom" />
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const root = container.querySelector(".seeds-narrative-summary");
|
|
89
|
+
expect(root).toBeInTheDocument();
|
|
90
|
+
expect(root).toHaveClass("custom");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("forwards its ref to the underlying div element", () => {
|
|
94
|
+
const ref = React.createRef<HTMLDivElement>();
|
|
95
|
+
render(<NarrativeSummary {...baseProps} ref={ref} />);
|
|
96
|
+
|
|
97
|
+
expect(ref.current).toBeInstanceOf(HTMLDivElement);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -8,12 +8,34 @@ import NarrativeHeadlineRoll from "../NarrativeHeadline/NarrativeHeadlineRoll";
|
|
|
8
8
|
import PullQuote from "../PullQuote/PullQuote";
|
|
9
9
|
import MetricHighlight from "../MetricHighlight/MetricHighlight";
|
|
10
10
|
import EyebrowToken from "../EyebrowToken/EyebrowToken";
|
|
11
|
+
import NarrativeDivider from "../NarrativeDivider/NarrativeDivider";
|
|
12
|
+
import NarrativeSummary from "../NarrativeSummary/NarrativeSummary";
|
|
13
|
+
import NarrativeOrderedList from "../NarrativeOrderedList/NarrativeOrderedList";
|
|
14
|
+
import NarrativeDataCallout from "../NarrativeDataCallout/NarrativeDataCallout";
|
|
15
|
+
import Button from "@sproutsocial/seeds-react-button";
|
|
11
16
|
import Text from "@sproutsocial/seeds-react-text";
|
|
17
|
+
import { SparklineChart } from "@sproutsocial/seeds-react-data-viz/sparkline";
|
|
18
|
+
import { DonutChart } from "@sproutsocial/seeds-react-data-viz/donut";
|
|
12
19
|
import "../narrative-container.css";
|
|
13
20
|
import "../narrative-headline.css";
|
|
14
21
|
import "../pull-quote.css";
|
|
15
22
|
import "../metric-highlight.css";
|
|
16
23
|
import "../eyebrow-token.css";
|
|
24
|
+
import "../narrative-divider.css";
|
|
25
|
+
import "../narrative-summary.css";
|
|
26
|
+
import "../narrative-ordered-list.css";
|
|
27
|
+
import "../narrative-data-callout.css";
|
|
28
|
+
import "@sproutsocial/seeds-react-button/dist/button.css";
|
|
29
|
+
import "@sproutsocial/seeds-react-data-viz/sparkline.css";
|
|
30
|
+
|
|
31
|
+
const spark = (data: number[], variant: "line" | "bar" = "line") => (
|
|
32
|
+
<SparklineChart
|
|
33
|
+
variant={variant}
|
|
34
|
+
description="Trend"
|
|
35
|
+
data={data}
|
|
36
|
+
className="h-full w-full"
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
17
39
|
|
|
18
40
|
/**
|
|
19
41
|
* Narrative Kit — Playground (showcase story)
|
|
@@ -40,56 +62,172 @@ const Attribution = ({ name }: { name: string }) => (
|
|
|
40
62
|
const Brief = () => (
|
|
41
63
|
<div className="box-border min-h-screen p-600 bg-app-bg-base">
|
|
42
64
|
<div className="max-w-280 mx-auto grid gap-500">
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
<span>
|
|
66
|
+
<EyebrowToken className="mb-300 inline-flex">May 31, 2026</EyebrowToken>
|
|
67
|
+
<NarrativeHeadline headingLevel={1}>
|
|
68
|
+
The social suite built for{" "}
|
|
69
|
+
<NarrativeHeadlineRoll
|
|
70
|
+
words={["marketers", "agencies", "enterprise teams", "founders"]}
|
|
71
|
+
/>
|
|
72
|
+
</NarrativeHeadline>
|
|
73
|
+
<Text.BodyCopy mt={400} display="block" width="50ch">
|
|
74
|
+
Empowering cross-functional stakeholders to drive engagement at scale
|
|
75
|
+
and accelerate time-to-value through data-driven content strategies
|
|
76
|
+
aligned with your brand's north-star KPIs.
|
|
77
|
+
</Text.BodyCopy>
|
|
78
|
+
</span>
|
|
57
79
|
|
|
58
80
|
{/* Pull quotes ------------------------------------------------------- */}
|
|
59
81
|
<NarrativeContainer className="w-full">
|
|
60
|
-
<Grid columns={{ sm: 1, md: 1 }} gap={
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
<Grid columns={{ sm: 1, md: 1 }} gap={500}>
|
|
83
|
+
{/* Metric highlights — a row of 3 -------------------------------- */}
|
|
84
|
+
<Grid columns={{ sm: 1, md: 2, lg: 3 }} gap={400}>
|
|
85
|
+
<MetricHighlight
|
|
86
|
+
label="Impressions"
|
|
87
|
+
value="1.2M"
|
|
88
|
+
trend={{ direction: "up", value: "18%", "aria-label": "up 18%" }}
|
|
89
|
+
sparkline={spark([8, 12, 9, 15, 13, 19])}
|
|
90
|
+
/>
|
|
91
|
+
<MetricHighlight
|
|
92
|
+
label="Engagement rate"
|
|
93
|
+
value="6.4%"
|
|
94
|
+
trend={{
|
|
95
|
+
direction: "up",
|
|
96
|
+
value: "240%",
|
|
97
|
+
"aria-label": "up 240%",
|
|
98
|
+
}}
|
|
99
|
+
sparkline={spark([3, 5, 4, 7, 6, 9])}
|
|
100
|
+
/>
|
|
101
|
+
<MetricHighlight
|
|
102
|
+
label="Response time"
|
|
103
|
+
value="2h"
|
|
104
|
+
trend={{
|
|
105
|
+
direction: "down",
|
|
106
|
+
value: "12%",
|
|
107
|
+
"aria-label": "down 12%",
|
|
108
|
+
}}
|
|
109
|
+
sparkline={spark([18, 16, 17, 13, 14, 10], "bar")}
|
|
110
|
+
/>
|
|
111
|
+
</Grid>
|
|
112
|
+
|
|
113
|
+
{/* Divider — separates the metric row from the narrative below --- */}
|
|
114
|
+
<NarrativeDivider />
|
|
115
|
+
|
|
116
|
+
{/* Narrative summary (2-column) — between the pull quotes -------- */}
|
|
117
|
+
<NarrativeSummary
|
|
118
|
+
layout="two-column"
|
|
119
|
+
eyebrow="June 10th"
|
|
120
|
+
headline="Engagement climbed across every owned channel this month"
|
|
121
|
+
summary="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio leo, gravida vitae sodales vitae, tempor eget ligula. Vestibulum nec enim eget mi placerat finibus. Donec tempor tincidunt elit."
|
|
122
|
+
keyThemes={[
|
|
123
|
+
"Reply threads outperformed paid pushes for the week.",
|
|
124
|
+
"Average response time dropped nearly in half.",
|
|
125
|
+
"Enterprise segment drove the largest share of new engagement.",
|
|
126
|
+
]}
|
|
127
|
+
/>
|
|
128
|
+
|
|
82
129
|
<PullQuote
|
|
83
130
|
quote="The reply thread on the 18th outperformed our entire paid push for the week."
|
|
84
131
|
attribution={<Attribution name="Jordan Lee" />}
|
|
85
132
|
/>
|
|
133
|
+
|
|
134
|
+
<NarrativeDivider />
|
|
135
|
+
|
|
136
|
+
<NarrativeOrderedList
|
|
137
|
+
items={[
|
|
138
|
+
{
|
|
139
|
+
title: "Double down on reply threads",
|
|
140
|
+
description:
|
|
141
|
+
"Shift a slice of paid budget toward the organic reply threads that outperformed this week.",
|
|
142
|
+
action: (
|
|
143
|
+
<Button appearance="ai-secondary">Suggested Action</Button>
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
title: "Protect response time at peak",
|
|
148
|
+
description:
|
|
149
|
+
"Add coverage during the two peak windows to hold response time under two hours.",
|
|
150
|
+
action: (
|
|
151
|
+
<Button appearance="ai-secondary">Suggested Action</Button>
|
|
152
|
+
),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
title: "Lean into the enterprise segment",
|
|
156
|
+
description:
|
|
157
|
+
"Build a follow-up sequence for the enterprise cohort that drove the most new engagement.",
|
|
158
|
+
action: (
|
|
159
|
+
<Button appearance="ai-secondary">Suggested Action</Button>
|
|
160
|
+
),
|
|
161
|
+
},
|
|
162
|
+
]}
|
|
163
|
+
/>
|
|
164
|
+
|
|
165
|
+
<NarrativeDivider />
|
|
166
|
+
|
|
167
|
+
{/* Narrative summary (3-column) — below the pull quotes --------- */}
|
|
168
|
+
<NarrativeSummary
|
|
169
|
+
eyebrow="Quarterly review"
|
|
170
|
+
headline="Three themes defined the quarter for the brand"
|
|
171
|
+
action={<Button appearance="primary">View report</Button>}
|
|
172
|
+
summary="Praesent pretium libero vitae arcu blandit pellentesque. Aenean fermentum, nisi eu blandit placerat, ligula ligula euismod ante, id semper nibh diam ac ipsum."
|
|
173
|
+
keyThemes={[
|
|
174
|
+
"Owned channels outpaced paid across every region.",
|
|
175
|
+
"Response time held under two hours through peak weeks.",
|
|
176
|
+
"Sentiment trended positive after the product launch.",
|
|
177
|
+
]}
|
|
178
|
+
/>
|
|
179
|
+
|
|
86
180
|
<PullQuote
|
|
87
181
|
appearance="vivid"
|
|
88
182
|
quote="This rollout cut our average response time nearly in half."
|
|
89
183
|
attribution={<Attribution name="Priya Nair" />}
|
|
90
184
|
/>
|
|
185
|
+
|
|
186
|
+
<NarrativeDivider />
|
|
187
|
+
|
|
188
|
+
{/* Data callout — a data-viz visual paired with narrative copy ---- */}
|
|
189
|
+
<NarrativeDataCallout
|
|
190
|
+
visual={
|
|
191
|
+
<DonutChart
|
|
192
|
+
description="Goal completion: 80% complete"
|
|
193
|
+
data={[
|
|
194
|
+
{ name: "Complete", value: 80 },
|
|
195
|
+
{ name: "Remaining", value: 20 },
|
|
196
|
+
]}
|
|
197
|
+
/>
|
|
198
|
+
}
|
|
199
|
+
>
|
|
200
|
+
We're 80% of the way to closing out this quarter's goal — a clear
|
|
201
|
+
signal that the cross-functional alignment we drove upstream is now
|
|
202
|
+
compounding into measurable downstream velocity. With the remaining
|
|
203
|
+
20% squarely in view, we're doubling down on the workstreams that
|
|
204
|
+
move the needle and sunsetting the low-leverage noise to land this
|
|
205
|
+
initiative ahead of the curve.
|
|
206
|
+
</NarrativeDataCallout>
|
|
91
207
|
</Grid>
|
|
92
208
|
</NarrativeContainer>
|
|
209
|
+
<NarrativeContainer>
|
|
210
|
+
<NarrativeOrderedList
|
|
211
|
+
orientation="horizontal"
|
|
212
|
+
items={[
|
|
213
|
+
{
|
|
214
|
+
title: "Unlock audience momentum",
|
|
215
|
+
description:
|
|
216
|
+
"Reallocate engagement capacity toward the content clusters generating outsized share-of-voice gains across high-intent segments.",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
title: "Operationalize your top performers",
|
|
220
|
+
description:
|
|
221
|
+
"Templatize the creative formats moving the needle this quarter and activate them across your highest-converting touchpoints.",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
title: "Accelerate pipeline velocity",
|
|
225
|
+
description:
|
|
226
|
+
"Trigger a nurture sequence for the buyer personas showing the strongest conversion propensity signals right now.",
|
|
227
|
+
},
|
|
228
|
+
]}
|
|
229
|
+
/>
|
|
230
|
+
</NarrativeContainer>
|
|
93
231
|
</div>
|
|
94
232
|
</div>
|
|
95
233
|
);
|
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import PullQuote from "./PullQuote";
|
|
4
4
|
import "../pull-quote.css";
|
|
5
|
+
import "../ai-container.css";
|
|
5
6
|
|
|
6
7
|
const meta = {
|
|
7
8
|
title: "Narrative Kit/PullQuote",
|
|
@@ -16,7 +17,9 @@ type Story = StoryObj<typeof meta>;
|
|
|
16
17
|
|
|
17
18
|
const Attribution = () => (
|
|
18
19
|
<>
|
|
19
|
-
<span aria-hidden className="text-400"
|
|
20
|
+
<span aria-hidden className="text-400">
|
|
21
|
+
🧵
|
|
22
|
+
</span>
|
|
20
23
|
<span>Display Name</span>
|
|
21
24
|
</>
|
|
22
25
|
);
|
|
@@ -8,9 +8,10 @@ import type { TypePullQuoteProps } from "./PullQuoteTypes";
|
|
|
8
8
|
* optional attribution row (author name, avatar, network).
|
|
9
9
|
*
|
|
10
10
|
* Rendered as semantic `<figure>` / `<blockquote>` / `<figcaption>` markup.
|
|
11
|
-
* Styled via `pull-quote.css` (Seeds CSS custom
|
|
12
|
-
* the classes through
|
|
13
|
-
* are done with
|
|
11
|
+
* Styled via `pull-quote.css` and `ai-container.css` (Seeds CSS custom
|
|
12
|
+
* properties); consumers import the classes through
|
|
13
|
+
* `@sproutsocial/racine/css/components`. Surface overrides are done with
|
|
14
|
+
* standard `className` / `style`.
|
|
14
15
|
*/
|
|
15
16
|
const PullQuote = React.forwardRef<HTMLElement, TypePullQuoteProps>(
|
|
16
17
|
({ quote, attribution, appearance = "subtle", className, ...props }, ref) => {
|
|
@@ -19,6 +20,8 @@ const PullQuote = React.forwardRef<HTMLElement, TypePullQuoteProps>(
|
|
|
19
20
|
ref={ref}
|
|
20
21
|
className={cn(
|
|
21
22
|
"seeds-pull-quote",
|
|
23
|
+
"seeds-ai-container",
|
|
24
|
+
{ "seeds-ai-container-vivid": appearance === "vivid" },
|
|
22
25
|
{ "seeds-pull-quote-vivid": appearance === "vivid" },
|
|
23
26
|
className
|
|
24
27
|
)}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type * as React from "react";
|
|
2
2
|
|
|
3
|
-
export interface TypePullQuoteProps
|
|
4
|
-
extends React.HTMLAttributes<HTMLElement> {
|
|
3
|
+
export interface TypePullQuoteProps extends React.HTMLAttributes<HTMLElement> {
|
|
5
4
|
/** The quotation text, rendered emphasized inside the accented bar. */
|
|
6
5
|
quote: React.ReactNode;
|
|
7
6
|
/**
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds AIContainer component classes.
|
|
3
|
+
* Use these instead of writing out individual Tailwind utility classes.
|
|
4
|
+
*
|
|
5
|
+
* Requires @sproutsocial/seeds-react-theme/dist/theme-all.css imported for
|
|
6
|
+
* CSS variable definitions and dark mode support.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <div class="seeds-ai-container">…</div> // subtle (default)
|
|
10
|
+
* <div class="seeds-ai-container seeds-ai-container-vivid">…</div>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
@layer components {
|
|
14
|
+
|
|
15
|
+
.seeds-ai-container {
|
|
16
|
+
position: relative;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
border-radius: var(--radius-800);
|
|
20
|
+
box-shadow: var(--shadow-low);
|
|
21
|
+
font-family: var(--font-family);
|
|
22
|
+
/* Subtle (default): the 20% AI gradient tint composited over the base
|
|
23
|
+
surface. --color-gradient-ai-subtle is already translucent, so layering it
|
|
24
|
+
over the opaque base color produces the soft wash. */
|
|
25
|
+
background-color: var(--color-container-bg-base);
|
|
26
|
+
background-image: var(--color-gradient-ai-subtle);
|
|
27
|
+
color: var(--color-text-headline);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Vivid: the full-saturation AI gradient with white content. White has no
|
|
31
|
+
semantic Seeds token; it is part of the AI gradient treatment (mirrors the
|
|
32
|
+
raw brand hex used in --color-gradient-ai). */
|
|
33
|
+
.seeds-ai-container-vivid {
|
|
34
|
+
background-color: transparent;
|
|
35
|
+
background-image: var(--color-gradient-ai);
|
|
36
|
+
color: #fff;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -19,10 +19,35 @@ export { PullQuote, type TypePullQuoteProps } from "./PullQuote";
|
|
|
19
19
|
|
|
20
20
|
export { EyebrowToken, type TypeEyebrowTokenProps } from "./EyebrowToken";
|
|
21
21
|
|
|
22
|
+
export {
|
|
23
|
+
NarrativeDivider,
|
|
24
|
+
type TypeNarrativeDividerProps,
|
|
25
|
+
} from "./NarrativeDivider";
|
|
26
|
+
|
|
22
27
|
export {
|
|
23
28
|
MetricHighlight,
|
|
24
29
|
type TypeMetricHighlightProps,
|
|
25
30
|
type TypeMetricHighlightTrend,
|
|
26
31
|
} from "./MetricHighlight";
|
|
27
32
|
|
|
33
|
+
export {
|
|
34
|
+
NarrativeSummary,
|
|
35
|
+
type TypeNarrativeSummaryProps,
|
|
36
|
+
type TypeNarrativeSummaryLayout,
|
|
37
|
+
} from "./NarrativeSummary";
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
NarrativeOrderedList,
|
|
41
|
+
type TypeNarrativeOrderedListProps,
|
|
42
|
+
type TypeNarrativeOrderedListItem,
|
|
43
|
+
type TypeNarrativeOrderedListOrientation,
|
|
44
|
+
} from "./NarrativeOrderedList";
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
NarrativeDataCallout,
|
|
48
|
+
type TypeNarrativeDataCalloutProps,
|
|
49
|
+
} from "./NarrativeDataCallout";
|
|
50
|
+
|
|
51
|
+
export { AIContainer, type TypeAIContainerProps } from "./AIContainer";
|
|
52
|
+
|
|
28
53
|
export type { TypeNarrativeTone } from "./_internal/types";
|
package/src/metric-highlight.css
CHANGED
|
@@ -131,47 +131,37 @@
|
|
|
131
131
|
stroke-width: 1.5;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/*
|
|
135
|
-
stroke
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
/* Slot for an injected chart (e.g. a data-viz SparklineChart). Layout only — no
|
|
135
|
+
stroke/background styling, so the injected chart controls its own look. Heights
|
|
136
|
+
are explicit (never percentage/stretch) so a Highcharts chart always measures a
|
|
137
|
+
concrete container instead of falling back to its 400px default. */
|
|
138
|
+
.seeds-metric-highlight-chart {
|
|
139
|
+
box-sizing: border-box;
|
|
138
140
|
flex: 0 0 auto;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
stroke: var(--color-red-700);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.seeds-metric-highlight-spark path {
|
|
149
|
-
stroke: inherit;
|
|
150
|
-
stroke-width: 2;
|
|
151
|
-
vector-effect: non-scaling-stroke;
|
|
141
|
+
align-self: center;
|
|
142
|
+
/* Center the injected chart as a flex item — an inline/inline-block child
|
|
143
|
+
(e.g. the data-viz SparklineChart) otherwise sits on the text baseline with
|
|
144
|
+
descender space below it, reading as vertically off-center in the slot. */
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
152
147
|
}
|
|
153
148
|
|
|
154
|
-
|
|
155
|
-
metric block's height. */
|
|
156
|
-
.seeds-metric-highlight:not(.seeds-metric-highlight-vertical) .seeds-metric-highlight-spark {
|
|
149
|
+
.seeds-metric-highlight:not(.seeds-metric-highlight-vertical) .seeds-metric-highlight-chart {
|
|
157
150
|
width: 104px;
|
|
158
|
-
|
|
159
|
-
min-height: 40px;
|
|
151
|
+
height: 48px;
|
|
160
152
|
}
|
|
161
153
|
|
|
162
|
-
.seeds-metric-highlight-small:not(.seeds-metric-highlight-vertical) .seeds-metric-highlight-
|
|
154
|
+
.seeds-metric-highlight-small:not(.seeds-metric-highlight-vertical) .seeds-metric-highlight-chart {
|
|
163
155
|
width: 88px;
|
|
164
156
|
height: 40px;
|
|
165
|
-
align-self: center;
|
|
166
157
|
}
|
|
167
158
|
|
|
168
|
-
|
|
169
|
-
.seeds-metric-highlight-vertical .seeds-metric-highlight-spark {
|
|
159
|
+
.seeds-metric-highlight-vertical .seeds-metric-highlight-chart {
|
|
170
160
|
width: 100%;
|
|
171
161
|
height: 96px;
|
|
172
162
|
}
|
|
173
163
|
|
|
174
|
-
.seeds-metric-highlight-vertical.seeds-metric-highlight-small .seeds-metric-highlight-
|
|
164
|
+
.seeds-metric-highlight-vertical.seeds-metric-highlight-small .seeds-metric-highlight-chart {
|
|
175
165
|
height: 56px;
|
|
176
166
|
}
|
|
177
167
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds NarrativeDataCallout component classes.
|
|
3
|
+
* Use these instead of writing out individual Tailwind utility classes.
|
|
4
|
+
*
|
|
5
|
+
* Requires @sproutsocial/seeds-react-theme/dist/theme-all.css imported for
|
|
6
|
+
* CSS variable definitions and dark mode support.
|
|
7
|
+
*
|
|
8
|
+
* The layout is a flex row driven by a container query on the root, so the
|
|
9
|
+
* visual/text columns stack based on the width the component is given, not the
|
|
10
|
+
* viewport.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* <div class="seeds-narrative-data-callout">
|
|
14
|
+
* <div class="seeds-narrative-data-callout-row">
|
|
15
|
+
* <div class="seeds-narrative-data-callout-visual">…chart…</div>
|
|
16
|
+
* <div class="seeds-narrative-data-callout-body">…copy…</div>
|
|
17
|
+
* </div>
|
|
18
|
+
* </div>
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
@layer components {
|
|
22
|
+
|
|
23
|
+
/* Establishes the query container. The row inside reacts to this element's
|
|
24
|
+
inline size. A container query only styles DESCENDANTS of the query container,
|
|
25
|
+
so the flex row lives on the inner element, not this one. */
|
|
26
|
+
.seeds-narrative-data-callout {
|
|
27
|
+
container-type: inline-size;
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
font-family: var(--font-family);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Stacked single column by default (narrow). The container query below turns it
|
|
33
|
+
into the horizontal 1/3 + 2/3 row once there is room. */
|
|
34
|
+
.seeds-narrative-data-callout-row {
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: var(--space-450); /* 24px */
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Visual slot — layout only, no stroke/background so the injected chart controls
|
|
43
|
+
its own look and its own size. No fixed height: charts like the data-viz
|
|
44
|
+
DonutChart carry their own intrinsic height (and a legend), so the slot sizes
|
|
45
|
+
to the chart rather than clipping it. Stacked full-width until the container
|
|
46
|
+
query expands the row. */
|
|
47
|
+
.seeds-narrative-data-callout-visual {
|
|
48
|
+
box-sizing: border-box;
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
width: 100%;
|
|
53
|
+
min-width: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Stretch the injected visual to the slot's full width. A fluid-width chart like
|
|
57
|
+
the v2 DonutChart otherwise shrinks to content inside this centered flex slot,
|
|
58
|
+
so Highcharts fills that shrunk width and the ring gets width-capped below its
|
|
59
|
+
fixed 270px height — leaving an empty vertical band. Filling the width lets the
|
|
60
|
+
ring become height-capped and fill its box top-to-bottom, like the standalone
|
|
61
|
+
chart story. */
|
|
62
|
+
.seeds-narrative-data-callout-visual > * {
|
|
63
|
+
width: 100%;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Body copy — fills the remaining space. */
|
|
67
|
+
.seeds-narrative-data-callout-body {
|
|
68
|
+
box-sizing: border-box;
|
|
69
|
+
min-width: 0;
|
|
70
|
+
font-size: var(--font-size-300); /* 16px */
|
|
71
|
+
line-height: var(--line-height-400); /* ~28px */
|
|
72
|
+
font-weight: var(--font-weight-normal);
|
|
73
|
+
color: var(--color-text-body);
|
|
74
|
+
overflow-wrap: break-word;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Expand to the horizontal 1/3 + 2/3 split once the component is wide enough.
|
|
78
|
+
The threshold is on the component's own width (container query), so a narrow
|
|
79
|
+
callout on a wide screen still stacks. 480px keeps the donut + copy readable
|
|
80
|
+
side by side. */
|
|
81
|
+
@container (min-width: 480px) {
|
|
82
|
+
.seeds-narrative-data-callout-row {
|
|
83
|
+
flex-direction: row;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.seeds-narrative-data-callout-visual {
|
|
87
|
+
/* Nominally one-third, but never narrower than the injected chart's fixed
|
|
88
|
+
height (the v2 DonutChart is 270px tall). Below that width a fluid-width,
|
|
89
|
+
fixed-height chart like the donut gets width-capped and leaves an empty
|
|
90
|
+
vertical band above/below the ring; the min-width keeps the visual square
|
|
91
|
+
enough that the ring fills its box top-to-bottom, matching the standalone
|
|
92
|
+
chart story. */
|
|
93
|
+
flex: 0 0 33.333%;
|
|
94
|
+
min-width: 270px;
|
|
95
|
+
width: auto;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.seeds-narrative-data-callout-body {
|
|
99
|
+
flex: 1 1 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
}
|