@sproutsocial/seeds-react-narrative-kit 0.1.0 → 0.2.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 +9 -0
- package/dist/esm/index.js +296 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/eyebrow-token.css +37 -0
- package/dist/index.d.mts +205 -1
- package/dist/index.d.ts +205 -1
- package/dist/index.js +303 -3
- package/dist/index.js.map +1 -1
- package/dist/metric-highlight.css +178 -0
- package/dist/narrative-container.css +1 -1
- package/dist/narrative-headline.css +125 -0
- package/dist/pull-quote.css +89 -0
- package/package.json +9 -3
- package/src/EyebrowToken/EyebrowToken.stories.tsx +25 -0
- package/src/EyebrowToken/EyebrowToken.tsx +29 -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 +87 -0
- package/src/MetricHighlight/MetricHighlight.tsx +196 -0
- package/src/MetricHighlight/MetricHighlightTypes.ts +61 -0
- package/src/MetricHighlight/__tests__/MetricHighlight.test.tsx +151 -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/NarrativeHeadline/NarrativeHeadline.stories.tsx +68 -0
- package/src/NarrativeHeadline/NarrativeHeadline.tsx +36 -0
- package/src/NarrativeHeadline/NarrativeHeadlineHighlight.tsx +31 -0
- package/src/NarrativeHeadline/NarrativeHeadlineRoll.tsx +100 -0
- package/src/NarrativeHeadline/NarrativeHeadlineTypes.ts +54 -0
- package/src/NarrativeHeadline/__tests__/NarrativeHeadline.test.tsx +138 -0
- package/src/NarrativeHeadline/index.ts +9 -0
- package/src/Playground/Playground.stories.tsx +113 -0
- package/src/PullQuote/PullQuote.stories.tsx +43 -0
- package/src/PullQuote/PullQuote.tsx +42 -0
- package/src/PullQuote/PullQuoteTypes.ts +19 -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 +20 -0
- package/src/metric-highlight.css +178 -0
- package/src/narrative-container.css +1 -1
- package/src/narrative-headline.css +125 -0
- package/src/pull-quote.css +89 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { act, render, screen } from "@sproutsocial/seeds-react-testing-library";
|
|
3
|
+
import NarrativeHeadline from "../NarrativeHeadline";
|
|
4
|
+
import NarrativeHeadlineHighlight from "../NarrativeHeadlineHighlight";
|
|
5
|
+
import NarrativeHeadlineRoll from "../NarrativeHeadlineRoll";
|
|
6
|
+
|
|
7
|
+
describe("NarrativeHeadline", () => {
|
|
8
|
+
it("renders its headline content", () => {
|
|
9
|
+
render(<NarrativeHeadline>Engagement is up</NarrativeHeadline>);
|
|
10
|
+
|
|
11
|
+
expect(screen.getByText("Engagement is up")).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("renders the headline as an h2 by default and honors headingLevel", () => {
|
|
15
|
+
const { rerender } = render(
|
|
16
|
+
<NarrativeHeadline>Title</NarrativeHeadline>
|
|
17
|
+
);
|
|
18
|
+
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
|
|
19
|
+
"Title"
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
rerender(<NarrativeHeadline headingLevel={1}>Title</NarrativeHeadline>);
|
|
23
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
|
|
24
|
+
"Title"
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("forwards its ref to the underlying heading element", () => {
|
|
29
|
+
const ref = React.createRef<HTMLHeadingElement>();
|
|
30
|
+
render(<NarrativeHeadline ref={ref}>Content</NarrativeHeadline>);
|
|
31
|
+
|
|
32
|
+
expect(ref.current).toBeInstanceOf(HTMLHeadingElement);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("NarrativeHeadlineHighlight", () => {
|
|
37
|
+
it("paints its children with the gradient accent class", () => {
|
|
38
|
+
const { container } = render(
|
|
39
|
+
<NarrativeHeadlineHighlight>boosted</NarrativeHeadlineHighlight>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(screen.getByText("boosted")).toBeInTheDocument();
|
|
43
|
+
expect(
|
|
44
|
+
container.querySelector(".seeds-narrative-headline-highlight")
|
|
45
|
+
).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("NarrativeHeadlineRoll", () => {
|
|
50
|
+
it("renders every word and marks the first one active", () => {
|
|
51
|
+
const { container } = render(
|
|
52
|
+
<NarrativeHeadlineRoll words={["marketers", "agencies", "teams"]} />
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const words = container.querySelectorAll(
|
|
56
|
+
".seeds-narrative-headline-roll-word"
|
|
57
|
+
);
|
|
58
|
+
expect(words).toHaveLength(3);
|
|
59
|
+
expect(words[0]).toHaveClass("seeds-narrative-headline-roll-word-active");
|
|
60
|
+
expect(words[1]).not.toHaveClass(
|
|
61
|
+
"seeds-narrative-headline-roll-word-active"
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("renders a sizer matched to the active word", () => {
|
|
66
|
+
const { container } = render(
|
|
67
|
+
<NarrativeHeadlineRoll words={["alpha", "beta"]} />
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const sizer = container.querySelector(
|
|
71
|
+
".seeds-narrative-headline-roll-sizer"
|
|
72
|
+
);
|
|
73
|
+
expect(sizer).toHaveTextContent("alpha");
|
|
74
|
+
expect(sizer).toHaveAttribute("aria-hidden", "true");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("applies the gradient modifier by default and omits it when disabled", () => {
|
|
78
|
+
const { container, rerender } = render(
|
|
79
|
+
<NarrativeHeadlineRoll words={["a", "b"]} />
|
|
80
|
+
);
|
|
81
|
+
expect(
|
|
82
|
+
container.querySelector(".seeds-narrative-headline-roll-gradient")
|
|
83
|
+
).toBeInTheDocument();
|
|
84
|
+
|
|
85
|
+
rerender(<NarrativeHeadlineRoll words={["a", "b"]} gradient={false} />);
|
|
86
|
+
expect(
|
|
87
|
+
container.querySelector(".seeds-narrative-headline-roll-gradient")
|
|
88
|
+
).not.toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("applies the direction modifier when rolling down", () => {
|
|
92
|
+
const { container } = render(
|
|
93
|
+
<NarrativeHeadlineRoll words={["a", "b"]} direction="down" />
|
|
94
|
+
);
|
|
95
|
+
expect(
|
|
96
|
+
container.querySelector(".seeds-narrative-headline-roll-down")
|
|
97
|
+
).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("advances to the next word after the interval elapses", () => {
|
|
101
|
+
jest.useFakeTimers();
|
|
102
|
+
try {
|
|
103
|
+
const { container } = render(
|
|
104
|
+
<NarrativeHeadlineRoll
|
|
105
|
+
words={["first", "second"]}
|
|
106
|
+
intervalMs={1000}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const wordsBefore = container.querySelectorAll(
|
|
111
|
+
".seeds-narrative-headline-roll-word"
|
|
112
|
+
);
|
|
113
|
+
expect(wordsBefore[0]).toHaveClass(
|
|
114
|
+
"seeds-narrative-headline-roll-word-active"
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
act(() => {
|
|
118
|
+
jest.advanceTimersByTime(1000);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const wordsAfter = container.querySelectorAll(
|
|
122
|
+
".seeds-narrative-headline-roll-word"
|
|
123
|
+
);
|
|
124
|
+
expect(wordsAfter[1]).toHaveClass(
|
|
125
|
+
"seeds-narrative-headline-roll-word-active"
|
|
126
|
+
);
|
|
127
|
+
} finally {
|
|
128
|
+
jest.useRealTimers();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("forwards its ref to the underlying element", () => {
|
|
133
|
+
const ref = React.createRef<HTMLSpanElement>();
|
|
134
|
+
render(<NarrativeHeadlineRoll ref={ref} words={["x"]} />);
|
|
135
|
+
|
|
136
|
+
expect(ref.current).toBeInstanceOf(HTMLSpanElement);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { default as NarrativeHeadline } from "./NarrativeHeadline";
|
|
2
|
+
export { default as NarrativeHeadlineHighlight } from "./NarrativeHeadlineHighlight";
|
|
3
|
+
export { default as NarrativeHeadlineRoll } from "./NarrativeHeadlineRoll";
|
|
4
|
+
export type {
|
|
5
|
+
TypeNarrativeHeadlineProps,
|
|
6
|
+
TypeNarrativeHeadlineLevel,
|
|
7
|
+
TypeNarrativeHeadlineHighlightProps,
|
|
8
|
+
TypeNarrativeHeadlineRollProps,
|
|
9
|
+
} from "./NarrativeHeadlineTypes";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import Grid from "@sproutsocial/seeds-react-grid";
|
|
4
|
+
import NarrativeContainer from "../NarrativeContainer/NarrativeContainer";
|
|
5
|
+
import NarrativeHeadline from "../NarrativeHeadline/NarrativeHeadline";
|
|
6
|
+
import NarrativeHeadlineHighlight from "../NarrativeHeadline/NarrativeHeadlineHighlight";
|
|
7
|
+
import NarrativeHeadlineRoll from "../NarrativeHeadline/NarrativeHeadlineRoll";
|
|
8
|
+
import PullQuote from "../PullQuote/PullQuote";
|
|
9
|
+
import MetricHighlight from "../MetricHighlight/MetricHighlight";
|
|
10
|
+
import EyebrowToken from "../EyebrowToken/EyebrowToken";
|
|
11
|
+
import Text from "@sproutsocial/seeds-react-text";
|
|
12
|
+
import "../narrative-container.css";
|
|
13
|
+
import "../narrative-headline.css";
|
|
14
|
+
import "../pull-quote.css";
|
|
15
|
+
import "../metric-highlight.css";
|
|
16
|
+
import "../eyebrow-token.css";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Narrative Kit — Playground (showcase story)
|
|
20
|
+
* ============================================
|
|
21
|
+
* A living showcase that composes the narrative primitives exactly as an agent
|
|
22
|
+
* would assemble them in production. It only ever uses primitives that have
|
|
23
|
+
* actually shipped in the kit — as new ones land, drop them in here and the
|
|
24
|
+
* showcase grows with the ecosystem.
|
|
25
|
+
*
|
|
26
|
+
* Today's shipped primitives: NarrativeContainer, NarrativeHeadline
|
|
27
|
+
* (+ Highlight / + Roll), PullQuote, and MetricHighlight. Layout uses
|
|
28
|
+
* `@sproutsocial/seeds-react-grid` to arrange the containers.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const Attribution = ({ name }: { name: string }) => (
|
|
32
|
+
<>
|
|
33
|
+
<span aria-hidden className="text-400">
|
|
34
|
+
🧵
|
|
35
|
+
</span>
|
|
36
|
+
<span>{name}</span>
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const Brief = () => (
|
|
41
|
+
<div className="box-border min-h-screen p-600 bg-app-bg-base">
|
|
42
|
+
<div className="max-w-280 mx-auto grid gap-500">
|
|
43
|
+
<span>
|
|
44
|
+
<EyebrowToken className="mb-300 inline-flex">May 31, 2026</EyebrowToken>
|
|
45
|
+
<NarrativeHeadline headingLevel={1}>
|
|
46
|
+
The social suite built for{" "}
|
|
47
|
+
<NarrativeHeadlineRoll
|
|
48
|
+
words={["marketers", "agencies", "enterprise teams", "founders"]}
|
|
49
|
+
/>
|
|
50
|
+
</NarrativeHeadline>
|
|
51
|
+
<Text.BodyCopy mt={400} display="block" width="50ch">
|
|
52
|
+
Empowering cross-functional stakeholders to drive engagement at scale and accelerate time-to-value through data-driven content strategies aligned with your brand's north-star KPIs.
|
|
53
|
+
</Text.BodyCopy>
|
|
54
|
+
</span>
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
{/* Pull quotes ------------------------------------------------------- */}
|
|
59
|
+
<NarrativeContainer className="w-full">
|
|
60
|
+
<Grid columns={{ sm: 1, md: 1 }} gap={400}>
|
|
61
|
+
{/* Metric highlights — a row of 3 -------------------------------- */}
|
|
62
|
+
<Grid columns={{ sm: 1, md: 2, lg: 3 }} gap={400}>
|
|
63
|
+
<MetricHighlight
|
|
64
|
+
label="Impressions"
|
|
65
|
+
value="1.2M"
|
|
66
|
+
trend={{ direction: "up", value: "18%", "aria-label": "up 18%" }}
|
|
67
|
+
data={[8, 12, 9, 15, 13, 19]}
|
|
68
|
+
/>
|
|
69
|
+
<MetricHighlight
|
|
70
|
+
label="Engagement rate"
|
|
71
|
+
value="6.4%"
|
|
72
|
+
trend={{ direction: "up", value: "240%", "aria-label": "up 240%" }}
|
|
73
|
+
data={[3, 5, 4, 7, 6, 9]}
|
|
74
|
+
/>
|
|
75
|
+
<MetricHighlight
|
|
76
|
+
label="Response time"
|
|
77
|
+
value="2h"
|
|
78
|
+
trend={{ direction: "down", value: "12%", "aria-label": "down 12%" }}
|
|
79
|
+
data={[18, 16, 17, 13, 14, 10]}
|
|
80
|
+
/>
|
|
81
|
+
</Grid>
|
|
82
|
+
<PullQuote
|
|
83
|
+
quote="The reply thread on the 18th outperformed our entire paid push for the week."
|
|
84
|
+
attribution={<Attribution name="Jordan Lee" />}
|
|
85
|
+
/>
|
|
86
|
+
<PullQuote
|
|
87
|
+
appearance="vivid"
|
|
88
|
+
quote="This rollout cut our average response time nearly in half."
|
|
89
|
+
attribution={<Attribution name="Priya Nair" />}
|
|
90
|
+
/>
|
|
91
|
+
</Grid>
|
|
92
|
+
</NarrativeContainer>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const meta: Meta = {
|
|
98
|
+
title: "Narrative Kit/Playground",
|
|
99
|
+
parameters: {
|
|
100
|
+
layout: "fullscreen",
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export default meta;
|
|
105
|
+
type Story = StoryObj<typeof meta>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The showcase brief. Resize the canvas to watch the Grid reflow from 2 → 1
|
|
109
|
+
* column. Toggle dark mode (Storybook toolbar) to verify token-driven theming.
|
|
110
|
+
*/
|
|
111
|
+
export const ExecutiveBrief: Story = {
|
|
112
|
+
render: () => <Brief />,
|
|
113
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import PullQuote from "./PullQuote";
|
|
4
|
+
import "../pull-quote.css";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Narrative Kit/PullQuote",
|
|
8
|
+
component: PullQuote,
|
|
9
|
+
args: {
|
|
10
|
+
quote: "This rollout cut our average response time nearly in half.",
|
|
11
|
+
},
|
|
12
|
+
} satisfies Meta<typeof PullQuote>;
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj<typeof meta>;
|
|
16
|
+
|
|
17
|
+
const Attribution = () => (
|
|
18
|
+
<>
|
|
19
|
+
<span aria-hidden className="text-400">🧵</span>
|
|
20
|
+
<span>Display Name</span>
|
|
21
|
+
</>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const Subtle: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
className: "w-80",
|
|
27
|
+
attribution: <Attribution />,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const Vivid: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
className: "w-80",
|
|
34
|
+
appearance: "vivid",
|
|
35
|
+
attribution: <Attribution />,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const WithoutAttribution: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
className: "w-80",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../_internal/cn";
|
|
3
|
+
import type { TypePullQuoteProps } from "./PullQuoteTypes";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* PullQuote — a narrative primitive that spotlights a quotation on an AI
|
|
7
|
+
* gradient surface. The quote sits behind an accented vertical bar with an
|
|
8
|
+
* optional attribution row (author name, avatar, network).
|
|
9
|
+
*
|
|
10
|
+
* Rendered as semantic `<figure>` / `<blockquote>` / `<figcaption>` markup.
|
|
11
|
+
* Styled via `pull-quote.css` (Seeds CSS custom properties); consumers import
|
|
12
|
+
* the classes through `@sproutsocial/racine/css/components`. Surface overrides
|
|
13
|
+
* are done with standard `className` / `style`.
|
|
14
|
+
*/
|
|
15
|
+
const PullQuote = React.forwardRef<HTMLElement, TypePullQuoteProps>(
|
|
16
|
+
({ quote, attribution, appearance = "subtle", className, ...props }, ref) => {
|
|
17
|
+
return (
|
|
18
|
+
<figure
|
|
19
|
+
ref={ref}
|
|
20
|
+
className={cn(
|
|
21
|
+
"seeds-pull-quote",
|
|
22
|
+
{ "seeds-pull-quote-vivid": appearance === "vivid" },
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
>
|
|
27
|
+
<div className="seeds-pull-quote-bar">
|
|
28
|
+
<blockquote className="seeds-pull-quote-text">{quote}</blockquote>
|
|
29
|
+
{attribution != null && (
|
|
30
|
+
<figcaption className="seeds-pull-quote-attribution">
|
|
31
|
+
{attribution}
|
|
32
|
+
</figcaption>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
</figure>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
PullQuote.displayName = "PullQuote";
|
|
41
|
+
|
|
42
|
+
export default PullQuote;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
|
|
3
|
+
export interface TypePullQuoteProps
|
|
4
|
+
extends React.HTMLAttributes<HTMLElement> {
|
|
5
|
+
/** The quotation text, rendered emphasized inside the accented bar. */
|
|
6
|
+
quote: React.ReactNode;
|
|
7
|
+
/**
|
|
8
|
+
* Optional attribution slot rendered beneath the quote (e.g. an author name,
|
|
9
|
+
* avatar, and network). Composed by the consumer; omitted entirely when absent.
|
|
10
|
+
*/
|
|
11
|
+
attribution?: React.ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Visual treatment of the AI gradient surface.
|
|
14
|
+
* - `subtle` — a light 20% gradient tint over the base surface with dark text.
|
|
15
|
+
* - `vivid` — the full-saturation AI gradient with white text.
|
|
16
|
+
* @default "subtle"
|
|
17
|
+
*/
|
|
18
|
+
appearance?: "subtle" | "vivid";
|
|
19
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@sproutsocial/seeds-react-testing-library";
|
|
3
|
+
import PullQuote from "../PullQuote";
|
|
4
|
+
|
|
5
|
+
describe("PullQuote", () => {
|
|
6
|
+
it("renders the quote text in a blockquote", () => {
|
|
7
|
+
render(<PullQuote quote="A memorable line" />);
|
|
8
|
+
|
|
9
|
+
const quote = screen.getByText("A memorable line");
|
|
10
|
+
expect(quote).toBeInTheDocument();
|
|
11
|
+
expect(quote.tagName).toBe("BLOCKQUOTE");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("renders the attribution in a figcaption when provided", () => {
|
|
15
|
+
const { container } = render(
|
|
16
|
+
<PullQuote quote="A line" attribution={<span>Jane Doe</span>} />
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
expect(screen.getByText("Jane Doe")).toBeInTheDocument();
|
|
20
|
+
expect(container.querySelector("figcaption")).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("omits the figcaption when no attribution is provided", () => {
|
|
24
|
+
const { container } = render(<PullQuote quote="A line" />);
|
|
25
|
+
|
|
26
|
+
expect(container.querySelector("figcaption")).not.toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("applies the vivid modifier only for the vivid appearance", () => {
|
|
30
|
+
const { container, rerender } = render(<PullQuote quote="A line" />);
|
|
31
|
+
expect(
|
|
32
|
+
container.querySelector(".seeds-pull-quote-vivid")
|
|
33
|
+
).not.toBeInTheDocument();
|
|
34
|
+
|
|
35
|
+
rerender(<PullQuote quote="A line" appearance="vivid" />);
|
|
36
|
+
expect(
|
|
37
|
+
container.querySelector(".seeds-pull-quote-vivid")
|
|
38
|
+
).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("forwards its ref to the underlying figure element", () => {
|
|
42
|
+
const ref = React.createRef<HTMLElement>();
|
|
43
|
+
render(<PullQuote ref={ref} quote="A line" />);
|
|
44
|
+
|
|
45
|
+
expect(ref.current).toBeInstanceOf(HTMLElement);
|
|
46
|
+
expect(ref.current?.tagName).toBe("FIGURE");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Merge class names, supporting `{ "class": boolean }` toggles. */
|
|
2
|
+
export function cn(
|
|
3
|
+
...inputs: (string | undefined | null | false | Record<string, boolean>)[]
|
|
4
|
+
): string {
|
|
5
|
+
const classes: string[] = [];
|
|
6
|
+
|
|
7
|
+
for (const input of inputs) {
|
|
8
|
+
if (!input) continue;
|
|
9
|
+
|
|
10
|
+
if (typeof input === "string") {
|
|
11
|
+
classes.push(input);
|
|
12
|
+
} else if (typeof input === "object") {
|
|
13
|
+
for (const [key, value] of Object.entries(input)) {
|
|
14
|
+
if (value) {
|
|
15
|
+
classes.push(key);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return classes.join(" ");
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds EyebrowToken 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
|
+
* <span class="seeds-eyebrow-token">May 31, 2026</span>
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
@layer components {
|
|
13
|
+
|
|
14
|
+
/* A solid gray pill matching the design: background and border are both the
|
|
15
|
+
#dee1e1 container-border-base token, which also tracks dark mode. The two
|
|
16
|
+
pixel literals below have no exact theme token — the --space scale skips 6px
|
|
17
|
+
(4px→8px) and the --font-size scale skips 12px (11px→13px) — so the design's
|
|
18
|
+
values are kept verbatim. All colors use tokens. */
|
|
19
|
+
.seeds-eyebrow-token {
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
gap: var(--space-100); /* 2px */
|
|
25
|
+
padding: var(--space-100) var(--space-300); /* 2px / 8px */
|
|
26
|
+
border: 1px solid var(--color-container-border-base); /* #dee1e1 */
|
|
27
|
+
border-radius: var(--radius-500); /* 6px */
|
|
28
|
+
background-color: var(--color-container-border-base); /* #dee1e1 */
|
|
29
|
+
font-family: var(--font-family);
|
|
30
|
+
font-size: 12px; /* text-sm */
|
|
31
|
+
line-height: 20px;
|
|
32
|
+
font-weight: var(--font-weight-normal);
|
|
33
|
+
color: var(--color-text-body);
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,4 +5,24 @@ export {
|
|
|
5
5
|
type TypeNarrativeContainerProps,
|
|
6
6
|
} from "./NarrativeContainer";
|
|
7
7
|
|
|
8
|
+
export {
|
|
9
|
+
NarrativeHeadline,
|
|
10
|
+
NarrativeHeadlineHighlight,
|
|
11
|
+
NarrativeHeadlineRoll,
|
|
12
|
+
type TypeNarrativeHeadlineProps,
|
|
13
|
+
type TypeNarrativeHeadlineLevel,
|
|
14
|
+
type TypeNarrativeHeadlineHighlightProps,
|
|
15
|
+
type TypeNarrativeHeadlineRollProps,
|
|
16
|
+
} from "./NarrativeHeadline";
|
|
17
|
+
|
|
18
|
+
export { PullQuote, type TypePullQuoteProps } from "./PullQuote";
|
|
19
|
+
|
|
20
|
+
export { EyebrowToken, type TypeEyebrowTokenProps } from "./EyebrowToken";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
MetricHighlight,
|
|
24
|
+
type TypeMetricHighlightProps,
|
|
25
|
+
type TypeMetricHighlightTrend,
|
|
26
|
+
} from "./MetricHighlight";
|
|
27
|
+
|
|
8
28
|
export type { TypeNarrativeTone } from "./_internal/types";
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds MetricHighlight 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-metric-highlight">…</div> // horizontal, default
|
|
10
|
+
* <div class="seeds-metric-highlight seeds-metric-highlight-vertical">…</div>
|
|
11
|
+
* <div class="seeds-metric-highlight seeds-metric-highlight-small">…</div>
|
|
12
|
+
* <div class="seeds-metric-highlight seeds-metric-highlight-bare">…</div> // no surface
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
@layer components {
|
|
16
|
+
|
|
17
|
+
.seeds-metric-highlight {
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: flex-start;
|
|
21
|
+
gap: var(--space-350); /* 12px */
|
|
22
|
+
border-radius: var(--radius-800); /* 12px */
|
|
23
|
+
font-family: var(--font-family);
|
|
24
|
+
/* Container (default): a padded gray app surface (matches the design's
|
|
25
|
+
#f3f4f4 box). `-bare` strips it. */
|
|
26
|
+
padding: var(--space-400) var(--space-450); /* 16px / 24px */
|
|
27
|
+
background-color: var(--color-app-bg-base);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.seeds-metric-highlight-bare {
|
|
31
|
+
padding: 0;
|
|
32
|
+
background-color: transparent;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.seeds-metric-highlight-vertical {
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* The metric block: label + value/trend. Grows to fill the row in horizontal. */
|
|
40
|
+
.seeds-metric-highlight-metric {
|
|
41
|
+
box-sizing: border-box;
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
align-items: flex-start;
|
|
45
|
+
gap: var(--space-200); /* 4px */
|
|
46
|
+
flex: 1 0 0;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.seeds-metric-highlight-vertical .seeds-metric-highlight-metric {
|
|
51
|
+
width: 100%;
|
|
52
|
+
flex: 0 0 auto;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Small: label and value collapse onto a single centered row. */
|
|
56
|
+
.seeds-metric-highlight-small .seeds-metric-highlight-metric {
|
|
57
|
+
flex-direction: row;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: var(--space-300); /* 8px */
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.seeds-metric-highlight-label {
|
|
63
|
+
margin: 0;
|
|
64
|
+
font-size: var(--font-size-300); /* 16px */
|
|
65
|
+
line-height: var(--line-height-400); /* ~28px */
|
|
66
|
+
font-weight: var(--font-weight-normal);
|
|
67
|
+
color: var(--color-text-body);
|
|
68
|
+
overflow-wrap: break-word;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* In small, the label takes the slack so the value/trend align to the end. */
|
|
72
|
+
.seeds-metric-highlight-small .seeds-metric-highlight-label {
|
|
73
|
+
flex: 1 0 0;
|
|
74
|
+
min-width: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.seeds-metric-highlight-value-row {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: var(--space-300); /* 8px */
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.seeds-metric-highlight-small .seeds-metric-highlight-value-row {
|
|
84
|
+
justify-content: flex-end;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.seeds-metric-highlight-value {
|
|
88
|
+
margin: 0;
|
|
89
|
+
/* No mono token exists yet; fall back to a system monospace stack. Mirrors the
|
|
90
|
+
font/font-mono treatment in the design. */
|
|
91
|
+
font-family: var(--font-family-mono, ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace);
|
|
92
|
+
font-size: var(--font-size-700); /* 32px */
|
|
93
|
+
line-height: var(--line-height-700); /* 40px */
|
|
94
|
+
font-weight: var(--font-weight-bold);
|
|
95
|
+
color: var(--color-text-headline);
|
|
96
|
+
white-space: nowrap;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.seeds-metric-highlight-small .seeds-metric-highlight-value {
|
|
100
|
+
font-size: var(--font-size-600); /* 24px */
|
|
101
|
+
line-height: var(--line-height-600); /* 32px */
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Trend badge — a rounded pill. Up is the positive (green) treatment; down
|
|
105
|
+
swaps to the negative (red) treatment. Color flows to the arrow + value via
|
|
106
|
+
currentColor. */
|
|
107
|
+
.seeds-metric-highlight-trend {
|
|
108
|
+
display: inline-flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
gap: var(--space-200); /* 4px */
|
|
111
|
+
padding: var(--space-100) var(--space-300); /* 2px / 8px */
|
|
112
|
+
border-radius: 999px;
|
|
113
|
+
font-size: var(--font-size-200); /* 13px */
|
|
114
|
+
line-height: var(--line-height-200);
|
|
115
|
+
font-weight: var(--font-weight-bold);
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
background-color: var(--color-container-bg-decorative-green);
|
|
118
|
+
color: var(--color-text-decorative-green);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.seeds-metric-highlight-trend-down {
|
|
122
|
+
background-color: var(--color-container-bg-decorative-red);
|
|
123
|
+
color: var(--color-text-decorative-red);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.seeds-metric-highlight-trend-arrow {
|
|
127
|
+
flex: 0 0 auto;
|
|
128
|
+
width: 1em;
|
|
129
|
+
height: 1em;
|
|
130
|
+
stroke: currentColor;
|
|
131
|
+
stroke-width: 1.5;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Sparkline — a single normalized polyline stretched to fit. The non-scaling
|
|
135
|
+
stroke keeps a crisp 2px line at any aspect ratio. */
|
|
136
|
+
.seeds-metric-highlight-spark {
|
|
137
|
+
display: block;
|
|
138
|
+
flex: 0 0 auto;
|
|
139
|
+
/* Matches the design's Data Viz / Categorical 01 line color (#0b968f). */
|
|
140
|
+
stroke: var(--color-teal-700);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Downward trend: the red analog at the same scale step (#db3e3e). */
|
|
144
|
+
.seeds-metric-highlight-spark-negative {
|
|
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;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Horizontal: a compact fixed-width chart beside the metric, matched to the
|
|
155
|
+
metric block's height. */
|
|
156
|
+
.seeds-metric-highlight:not(.seeds-metric-highlight-vertical) .seeds-metric-highlight-spark {
|
|
157
|
+
width: 104px;
|
|
158
|
+
align-self: stretch;
|
|
159
|
+
min-height: 40px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.seeds-metric-highlight-small:not(.seeds-metric-highlight-vertical) .seeds-metric-highlight-spark {
|
|
163
|
+
width: 88px;
|
|
164
|
+
height: 40px;
|
|
165
|
+
align-self: center;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Vertical: a full-width chart stacked beneath the metric. */
|
|
169
|
+
.seeds-metric-highlight-vertical .seeds-metric-highlight-spark {
|
|
170
|
+
width: 100%;
|
|
171
|
+
height: 96px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.seeds-metric-highlight-vertical.seeds-metric-highlight-small .seeds-metric-highlight-spark {
|
|
175
|
+
height: 56px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
}
|