@sproutsocial/seeds-react-narrative-kit 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +33 -0
  3. package/dist/ai-container.css +39 -0
  4. package/dist/esm/index.js +199 -0
  5. package/dist/esm/index.js.map +1 -1
  6. package/dist/index.d.mts +186 -4
  7. package/dist/index.d.ts +186 -4
  8. package/dist/index.js +204 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/narrative-attribution.css +56 -0
  11. package/dist/narrative-data-callout.css +103 -0
  12. package/dist/narrative-ordered-list.css +112 -0
  13. package/dist/narrative-social-media-card.css +82 -0
  14. package/dist/pull-quote.css +4 -25
  15. package/package.json +12 -5
  16. package/src/AIContainer/AIContainer.stories.tsx +54 -0
  17. package/src/AIContainer/AIContainer.tsx +35 -0
  18. package/src/AIContainer/AIContainerTypes.ts +14 -0
  19. package/src/AIContainer/__tests__/AIContainer.test.tsx +61 -0
  20. package/src/AIContainer/index.ts +2 -0
  21. package/src/NarrativeAttribution/NarrativeAttribution.stories.tsx +78 -0
  22. package/src/NarrativeAttribution/NarrativeAttribution.tsx +54 -0
  23. package/src/NarrativeAttribution/NarrativeAttributionTypes.ts +13 -0
  24. package/src/NarrativeAttribution/index.ts +2 -0
  25. package/src/NarrativeDataCallout/NarrativeDataCallout.stories.tsx +71 -0
  26. package/src/NarrativeDataCallout/NarrativeDataCallout.tsx +50 -0
  27. package/src/NarrativeDataCallout/NarrativeDataCalloutTypes.ts +18 -0
  28. package/src/NarrativeDataCallout/__tests__/NarrativeDataCallout.test.tsx +69 -0
  29. package/src/NarrativeDataCallout/index.ts +2 -0
  30. package/src/NarrativeOrderedList/NarrativeOrderedList.stories.tsx +69 -0
  31. package/src/NarrativeOrderedList/NarrativeOrderedList.tsx +74 -0
  32. package/src/NarrativeOrderedList/NarrativeOrderedListTypes.ts +39 -0
  33. package/src/NarrativeOrderedList/__tests__/NarrativeOrderedList.test.tsx +89 -0
  34. package/src/NarrativeOrderedList/index.ts +6 -0
  35. package/src/NarrativeSocialMediaCard/NarrativeSocialMediaCard.stories.tsx +111 -0
  36. package/src/NarrativeSocialMediaCard/NarrativeSocialMediaCard.tsx +80 -0
  37. package/src/NarrativeSocialMediaCard/NarrativeSocialMediaCardTypes.ts +30 -0
  38. package/src/NarrativeSocialMediaCard/__tests__/NarrativeSocialMediaCard.test.tsx +154 -0
  39. package/src/NarrativeSocialMediaCard/index.ts +5 -0
  40. package/src/Playground/Playground.stories.tsx +176 -5
  41. package/src/PullQuote/PullQuote.stories.tsx +1 -0
  42. package/src/PullQuote/PullQuote.tsx +6 -3
  43. package/src/ai-container.css +39 -0
  44. package/src/css.d.ts +1 -0
  45. package/src/index.ts +25 -0
  46. package/src/narrative-attribution.css +56 -0
  47. package/src/narrative-data-callout.css +103 -0
  48. package/src/narrative-ordered-list.css +112 -0
  49. package/src/narrative-social-media-card.css +82 -0
  50. package/src/pull-quote.css +4 -25
@@ -0,0 +1,39 @@
1
+ import type * as React from "react";
2
+
3
+ /**
4
+ * Item arrangement.
5
+ * - `"vertical"`: items stacked full-width, one per row (the default).
6
+ * - `"horizontal"`: items laid out as a row of equal columns that collapses
7
+ * back to a stack when the container is narrow (container query).
8
+ */
9
+ export type TypeNarrativeOrderedListOrientation = "vertical" | "horizontal";
10
+
11
+ export interface TypeNarrativeOrderedListItem {
12
+ /** Bold action title for the item. A plain string or any React node. */
13
+ title: React.ReactNode;
14
+ /**
15
+ * Descriptive text below the title. A plain string or any React node. The
16
+ * paragraph is omitted when this is empty.
17
+ */
18
+ description?: React.ReactNode;
19
+ /**
20
+ * Optional call-to-action rendered below the description, only when provided.
21
+ * Typically a Seeds `<Button>` in an AI appearance (e.g.
22
+ * `appearance="ai-secondary"`). Pass it without system props so it renders the
23
+ * Tailwind path — same as NarrativeSummary's `action` slot.
24
+ */
25
+ action?: React.ReactNode;
26
+ }
27
+
28
+ export interface TypeNarrativeOrderedListProps
29
+ extends React.HTMLAttributes<HTMLDivElement> {
30
+ /** The ordered items. Numbered automatically (01, 02, …) in order. */
31
+ items: TypeNarrativeOrderedListItem[];
32
+ /**
33
+ * Item arrangement. `"horizontal"` lays items in a row of equal columns that
34
+ * collapses to a stack when the container is narrow; `"vertical"` is always
35
+ * stacked.
36
+ * @default "vertical"
37
+ */
38
+ orientation?: TypeNarrativeOrderedListOrientation;
39
+ }
@@ -0,0 +1,89 @@
1
+ import React from "react";
2
+ import { render, screen } from "@sproutsocial/seeds-react-testing-library";
3
+ import NarrativeOrderedList from "../NarrativeOrderedList";
4
+ import type { TypeNarrativeOrderedListItem } from "../NarrativeOrderedListTypes";
5
+
6
+ const items: TypeNarrativeOrderedListItem[] = [
7
+ { title: "First action", description: "Do the first thing." },
8
+ { title: "Second action", description: "Do the second thing." },
9
+ { title: "Third action", description: "Do the third thing." },
10
+ ];
11
+
12
+ describe("NarrativeOrderedList", () => {
13
+ it("renders an ordered list with one item per entry", () => {
14
+ const { container } = render(<NarrativeOrderedList items={items} />);
15
+
16
+ expect(
17
+ container.querySelector("ol.seeds-narrative-ordered-list-track")
18
+ ).toBeInTheDocument();
19
+ expect(screen.getAllByRole("listitem")).toHaveLength(3);
20
+ });
21
+
22
+ it("renders each item's title and description", () => {
23
+ render(<NarrativeOrderedList items={items} />);
24
+
25
+ expect(screen.getByText("First action")).toBeInTheDocument();
26
+ expect(screen.getByText("Do the first thing.")).toBeInTheDocument();
27
+ expect(screen.getByText("Third action")).toBeInTheDocument();
28
+ });
29
+
30
+ it("auto-numbers items with zero-padded ordinals", () => {
31
+ render(<NarrativeOrderedList items={items} />);
32
+
33
+ expect(screen.getByText("01")).toBeInTheDocument();
34
+ expect(screen.getByText("02")).toBeInTheDocument();
35
+ expect(screen.getByText("03")).toBeInTheDocument();
36
+ });
37
+
38
+ it("omits the description when it is not provided", () => {
39
+ const { container } = render(
40
+ <NarrativeOrderedList items={[{ title: "No description" }]} />
41
+ );
42
+
43
+ expect(screen.getByText("No description")).toBeInTheDocument();
44
+ expect(
45
+ container.querySelector(".seeds-narrative-ordered-list-description")
46
+ ).not.toBeInTheDocument();
47
+ });
48
+
49
+ it("renders the action only when provided", () => {
50
+ const { rerender } = render(<NarrativeOrderedList items={items} />);
51
+ expect(
52
+ screen.queryByRole("button", { name: "Action" })
53
+ ).not.toBeInTheDocument();
54
+
55
+ rerender(
56
+ <NarrativeOrderedList
57
+ items={[{ title: "With action", action: <button>Action</button> }]}
58
+ />
59
+ );
60
+ expect(screen.getByRole("button", { name: "Action" })).toBeInTheDocument();
61
+ });
62
+
63
+ it("applies the horizontal modifier class", () => {
64
+ const { container } = render(
65
+ <NarrativeOrderedList items={items} orientation="horizontal" />
66
+ );
67
+
68
+ expect(
69
+ container.querySelector(".seeds-narrative-ordered-list-horizontal")
70
+ ).toBeInTheDocument();
71
+ });
72
+
73
+ it("merges a consumer className with the base class", () => {
74
+ const { container } = render(
75
+ <NarrativeOrderedList items={items} className="custom" />
76
+ );
77
+
78
+ const root = container.querySelector(".seeds-narrative-ordered-list");
79
+ expect(root).toBeInTheDocument();
80
+ expect(root).toHaveClass("custom");
81
+ });
82
+
83
+ it("forwards its ref to the underlying container element", () => {
84
+ const ref = React.createRef<HTMLDivElement>();
85
+ render(<NarrativeOrderedList items={items} ref={ref} />);
86
+
87
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
88
+ });
89
+ });
@@ -0,0 +1,6 @@
1
+ export { default as NarrativeOrderedList } from "./NarrativeOrderedList";
2
+ export type {
3
+ TypeNarrativeOrderedListProps,
4
+ TypeNarrativeOrderedListItem,
5
+ TypeNarrativeOrderedListOrientation,
6
+ } from "./NarrativeOrderedListTypes";
@@ -0,0 +1,111 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import NarrativeSocialMediaCard from "./NarrativeSocialMediaCard";
4
+ import { PartnerLogo } from "@sproutsocial/seeds-react-partner-logo";
5
+ import "../narrative-social-media-card.css";
6
+ import Icon from "@sproutsocial/seeds-react-icon";
7
+
8
+ const metaArray = [
9
+ { icon: <Icon name="reply-outline" />, value: 12, label: "Replies" },
10
+ { icon: <Icon name="heart-outline" />, value: 128, label: "Likes" },
11
+ { icon: <Icon name="repost-outline" />, value: 42, label: "Reposts" },
12
+ ];
13
+
14
+ const meta = {
15
+ title: "Narrative Kit/NarrativeSocialMediaCard",
16
+ component: NarrativeSocialMediaCard,
17
+ args: {
18
+ avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Felix",
19
+ avatarAlt: "Profile avatar",
20
+ displayName: "Sprout Social",
21
+ socialIcon: <PartnerLogo partnerName="twitter" size="small" />,
22
+ meta: metaArray,
23
+ text: "Just launched our new product! Check it out and let us know what you think.",
24
+ },
25
+ decorators: [
26
+ (Story) => (
27
+ <div
28
+ style={{
29
+ padding: "24px",
30
+ backgroundColor: "white",
31
+ minHeight: "400px",
32
+ }}
33
+ >
34
+ <Story />
35
+ </div>
36
+ ),
37
+ ],
38
+ } satisfies Meta<typeof NarrativeSocialMediaCard>;
39
+
40
+ export default meta;
41
+ type Story = StoryObj<typeof meta>;
42
+
43
+ /** A typical social media card with gray background. */
44
+ export const Default: Story = {};
45
+
46
+ /** A card with longer content to show text wrapping. */
47
+ export const LongContent: Story = {
48
+ args: {
49
+ text: "We're thrilled to announce our biggest update yet! After months of hard work from the team, we're rolling out features that will transform how you connect with your audience. Stay tuned for more details coming soon.",
50
+ },
51
+ };
52
+
53
+ /** A card with a horizontal landscape image in the content area. */
54
+ export const WithHorizontalImage: Story = {
55
+ args: {
56
+ displayName: "Travel Blogger",
57
+ meta: metaArray,
58
+ image: (
59
+ <img
60
+ src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=600&h=400&fit=crop"
61
+ alt="Mountain sunset landscape"
62
+ style={{
63
+ width: "100%",
64
+ height: "auto",
65
+ borderRadius: "8px",
66
+ display: "block",
67
+ marginBottom: "12px",
68
+ }}
69
+ />
70
+ ),
71
+ text: "Just had an amazing sunset hike in the mountains! Nature never disappoints.",
72
+ },
73
+ };
74
+
75
+ /** A card with a tall vertical image in the content area. */
76
+ export const WithVerticalImage: Story = {
77
+ args: {
78
+ displayName: "Fashion Magazine",
79
+ meta: metaArray,
80
+ image: (
81
+ <img
82
+ src="https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=400&h=600&fit=crop"
83
+ alt="Fashion portrait"
84
+ style={{
85
+ width: "100%",
86
+ height: "auto",
87
+ borderRadius: "8px",
88
+ display: "block",
89
+ marginBottom: "12px",
90
+ }}
91
+ />
92
+ ),
93
+ text: "New collection dropping this Friday. Stay tuned for the full reveal!",
94
+ },
95
+ };
96
+
97
+ /** A card with no metadata. */
98
+ export const WithoutMetaData: Story = {
99
+ args: {
100
+ displayName: "Brand New Account",
101
+ meta: [],
102
+ text: "First post! Looking forward to connecting with everyone here.",
103
+ },
104
+ };
105
+
106
+ /** A card with no background that blends with the page. */
107
+ export const NoBackground: Story = {
108
+ args: {
109
+ appearance: "no-background",
110
+ },
111
+ };
@@ -0,0 +1,80 @@
1
+ import * as React from "react";
2
+ import { cn } from "../_internal/cn";
3
+ import NarrativeAttribution from "../NarrativeAttribution/NarrativeAttribution";
4
+ import type { TypeNarrativeSocialMediaCardProps } from "./NarrativeSocialMediaCardTypes";
5
+
6
+ /**
7
+ * NarrativeSocialMediaCard — a narrative primitive that displays a social media
8
+ * post card with a header (avatar + handle), content area, and footer with
9
+ * engagement metadata (replies, likes, reposts, etc.).
10
+ *
11
+ * Styled via narrative-social-media-card.css (Seeds CSS custom properties);
12
+ * consumers import the classes through @sproutsocial/racine/css/components.
13
+ * Layout is vertically stacked with clean typography and icon-based metadata in
14
+ * the footer. Default has a gray background; use appearance="no-background" to
15
+ * remove the background and blend seamlessly with the page.
16
+ */
17
+ const NarrativeSocialMediaCard = React.forwardRef<
18
+ HTMLDivElement,
19
+ TypeNarrativeSocialMediaCardProps
20
+ >(
21
+ (
22
+ {
23
+ avatar,
24
+ avatarAlt,
25
+ displayName,
26
+ socialIcon,
27
+ image,
28
+ text,
29
+ meta,
30
+ appearance,
31
+ className,
32
+ ...props
33
+ },
34
+ ref
35
+ ) => {
36
+ return (
37
+ <div
38
+ ref={ref}
39
+ className={cn(
40
+ "seeds-narrative-social-media-card",
41
+ appearance === "no-background" &&
42
+ "seeds-narrative-social-media-card--no-background",
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ <NarrativeAttribution
48
+ avatarUrl={avatar}
49
+ avatarAlt={avatarAlt}
50
+ userName={displayName}
51
+ socialIcon={socialIcon}
52
+ />
53
+
54
+ <div className="seeds-narrative-social-media-card-content">
55
+ {image}
56
+ {text}
57
+ </div>
58
+
59
+ <div className="seeds-narrative-social-media-card-footer">
60
+ {meta.map((item, index) => (
61
+ <div
62
+ key={index}
63
+ className="seeds-narrative-social-media-card-stat"
64
+ aria-label={item.label}
65
+ >
66
+ {item.icon}
67
+ <span className="seeds-narrative-social-media-card-stat-value">
68
+ {item.value}
69
+ </span>
70
+ </div>
71
+ ))}
72
+ </div>
73
+ </div>
74
+ );
75
+ }
76
+ );
77
+
78
+ NarrativeSocialMediaCard.displayName = "NarrativeSocialMediaCard";
79
+
80
+ export default NarrativeSocialMediaCard;
@@ -0,0 +1,30 @@
1
+ import type * as React from "react";
2
+
3
+ export interface TypeNarrativeSocialMediaCardMeta {
4
+ /** Icon for the metadata (e.g., reply, heart, repost). */
5
+ icon: React.ReactNode;
6
+ /** Number or formatted string value for the metadata. */
7
+ value: number | string;
8
+ /** Optional accessible label for the metadata. */
9
+ label?: string;
10
+ }
11
+
12
+ export interface TypeNarrativeSocialMediaCardProps
13
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
14
+ /** Avatar image URL for the social media profile. */
15
+ avatar: string;
16
+ /** Alt text for the avatar image. */
17
+ avatarAlt: string;
18
+ /** Display name for the social media profile. */
19
+ displayName: string;
20
+ /** Social media network icon (e.g., Twitter, Instagram, LinkedIn). */
21
+ socialIcon: React.ReactNode;
22
+ /** Optional image to display above the text content. */
23
+ image?: React.ReactNode;
24
+ /** The text content of the social media post. */
25
+ text: React.ReactNode;
26
+ /** Array of metadata with icons and values (e.g., replies, likes, reposts). */
27
+ meta: TypeNarrativeSocialMediaCardMeta[];
28
+ /** Visual appearance of the card. Default has gray background, "no-background" removes the background. */
29
+ appearance?: "no-background";
30
+ }
@@ -0,0 +1,154 @@
1
+ import React from "react";
2
+ import { render, screen } from "@sproutsocial/seeds-react-testing-library";
3
+ import NarrativeSocialMediaCard from "../NarrativeSocialMediaCard";
4
+ import { PartnerLogo } from "@sproutsocial/seeds-react-partner-logo";
5
+
6
+ // Test meta icons
7
+ const TestIcon1 = () => (
8
+ <svg viewBox="0 0 16 16" aria-hidden focusable={false}>
9
+ <path d="M2 8H14" stroke="currentColor" strokeWidth="1.5" />
10
+ </svg>
11
+ );
12
+
13
+ const TestIcon2 = () => (
14
+ <svg viewBox="0 0 16 16" aria-hidden focusable={false}>
15
+ <circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
16
+ </svg>
17
+ );
18
+
19
+ const TestIcon3 = () => (
20
+ <svg viewBox="0 0 16 16" aria-hidden focusable={false}>
21
+ <rect
22
+ x="4"
23
+ y="4"
24
+ width="8"
25
+ height="8"
26
+ stroke="currentColor"
27
+ strokeWidth="1.5"
28
+ />
29
+ </svg>
30
+ );
31
+
32
+ describe("NarrativeSocialMediaCard", () => {
33
+ const defaultProps = {
34
+ avatar: "https://example.com/avatar.jpg",
35
+ avatarAlt: "Profile avatar",
36
+ displayName: "Display Name",
37
+ socialIcon: <PartnerLogo partnerName="twitter" size="small" />,
38
+ text: "Test content",
39
+ meta: [
40
+ { icon: <TestIcon1 />, value: 42, label: "Action 1" },
41
+ { icon: <TestIcon2 />, value: 128, label: "Action 2" },
42
+ { icon: <TestIcon3 />, value: 17, label: "Action 3" },
43
+ ],
44
+ };
45
+
46
+ it("renders the avatar image", () => {
47
+ render(<NarrativeSocialMediaCard {...defaultProps} />);
48
+
49
+ const avatar = screen.getByRole("img");
50
+ expect(avatar).toBeInTheDocument();
51
+ expect(avatar).toHaveAttribute("src", "https://example.com/avatar.jpg");
52
+ });
53
+
54
+ it("renders the display name", () => {
55
+ render(<NarrativeSocialMediaCard {...defaultProps} />);
56
+
57
+ expect(screen.getByText("Display Name")).toBeInTheDocument();
58
+ });
59
+
60
+ it("renders the text content", () => {
61
+ render(
62
+ <NarrativeSocialMediaCard
63
+ {...defaultProps}
64
+ text="This is a test post with some content"
65
+ />
66
+ );
67
+
68
+ expect(
69
+ screen.getByText("This is a test post with some content")
70
+ ).toBeInTheDocument();
71
+ });
72
+
73
+ it("renders the meta values in the footer", () => {
74
+ render(<NarrativeSocialMediaCard {...defaultProps} />);
75
+
76
+ expect(screen.getByText("42")).toBeInTheDocument();
77
+ expect(screen.getByText("128")).toBeInTheDocument();
78
+ expect(screen.getByText("17")).toBeInTheDocument();
79
+ });
80
+
81
+ it("accepts string values for meta values", () => {
82
+ render(
83
+ <NarrativeSocialMediaCard
84
+ avatar="https://example.com/avatar.jpg"
85
+ avatarAlt="Profile avatar"
86
+ displayName="Display Name"
87
+ socialIcon={<PartnerLogo partnerName="twitter" size="small" />}
88
+ text="Test content"
89
+ meta={[
90
+ { icon: <TestIcon1 />, value: "1.2K", label: "Action 1" },
91
+ { icon: <TestIcon2 />, value: "5.3K", label: "Action 2" },
92
+ { icon: <TestIcon3 />, value: "842", label: "Action 3" },
93
+ ]}
94
+ />
95
+ );
96
+
97
+ expect(screen.getByText("1.2K")).toBeInTheDocument();
98
+ expect(screen.getByText("5.3K")).toBeInTheDocument();
99
+ expect(screen.getByText("842")).toBeInTheDocument();
100
+ });
101
+
102
+ it("renders the avatarAlt attribute", () => {
103
+ render(
104
+ <NarrativeSocialMediaCard
105
+ {...defaultProps}
106
+ avatarAlt="Custom avatar description"
107
+ />
108
+ );
109
+
110
+ const avatar = screen.getByRole("img");
111
+ expect(avatar).toHaveAttribute("alt", "Custom avatar description");
112
+ });
113
+
114
+ it("merges a consumer className with the base class", () => {
115
+ const { container } = render(
116
+ <NarrativeSocialMediaCard {...defaultProps} className="custom" />
117
+ );
118
+
119
+ const root = container.querySelector(".seeds-narrative-social-media-card");
120
+ expect(root).toBeInTheDocument();
121
+ expect(root).toHaveClass("custom");
122
+ });
123
+
124
+ it("forwards extra props to the underlying element", () => {
125
+ render(
126
+ <NarrativeSocialMediaCard {...defaultProps} data-testid="social-card" />
127
+ );
128
+
129
+ expect(screen.getByTestId("social-card")).toBeInTheDocument();
130
+ });
131
+
132
+ it("forwards its ref to the underlying div element", () => {
133
+ const ref = React.createRef<HTMLDivElement>();
134
+ render(<NarrativeSocialMediaCard {...defaultProps} ref={ref} />);
135
+
136
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
137
+ expect(ref.current?.tagName).toBe("DIV");
138
+ });
139
+
140
+ it("renders image above text when image is provided", () => {
141
+ const { container } = render(
142
+ <NarrativeSocialMediaCard
143
+ {...defaultProps}
144
+ image={<img src="test.jpg" alt="test" />}
145
+ text="Text content"
146
+ />
147
+ );
148
+
149
+ const content = container.querySelector(
150
+ ".seeds-narrative-social-media-card-content"
151
+ );
152
+ expect(content?.firstChild).toHaveProperty("tagName", "IMG");
153
+ });
154
+ });
@@ -0,0 +1,5 @@
1
+ export { default as NarrativeSocialMediaCard } from "./NarrativeSocialMediaCard";
2
+ export type {
3
+ TypeNarrativeSocialMediaCardProps,
4
+ TypeNarrativeSocialMediaCardMeta,
5
+ } from "./NarrativeSocialMediaCardTypes";