@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,54 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import AIContainer from "./AIContainer";
4
+ import "../ai-container.css";
5
+
6
+ const meta = {
7
+ title: "Narrative Kit/AIContainer",
8
+ component: AIContainer,
9
+ args: {
10
+ className: "w-80",
11
+ style: { padding: "24px" },
12
+ },
13
+ } satisfies Meta<typeof AIContainer>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ const ShortContent = () => (
19
+ <p>
20
+ Engagement is up 24% this month, driven by a strong week on LinkedIn and a
21
+ viral reply thread.
22
+ </p>
23
+ );
24
+
25
+ const RichContent = () => (
26
+ <div className="grid gap-300">
27
+ <strong>AI-generated summary</strong>
28
+ <p>
29
+ Audience growth held steady while impressions climbed. Your top-performing
30
+ post reached 3x the usual engagement rate.
31
+ </p>
32
+ </div>
33
+ );
34
+
35
+ export const Subtle: Story = {
36
+ args: {
37
+ appearance: "subtle",
38
+ children: <ShortContent />,
39
+ },
40
+ };
41
+
42
+ export const Vivid: Story = {
43
+ args: {
44
+ appearance: "vivid",
45
+ children: <ShortContent />,
46
+ },
47
+ };
48
+
49
+ export const WithChildren: Story = {
50
+ args: {
51
+ appearance: "subtle",
52
+ children: <RichContent />,
53
+ },
54
+ };
@@ -0,0 +1,35 @@
1
+ import * as React from "react";
2
+ import { cn } from "../_internal/cn";
3
+ import type { TypeAIContainerProps } from "./AIContainerTypes";
4
+
5
+ /**
6
+ * AIContainer — a rounded surface primitive with the AI gradient background.
7
+ * Provides the outer shell (border-radius, shadow, overflow, gradient) without
8
+ * any padding. Used by PullQuote and available as a standalone container.
9
+ *
10
+ * - `subtle` — a light 20% gradient tint over the base surface with adaptive text color.
11
+ * - `vivid` — the full-saturation AI gradient with white text (always white).
12
+ *
13
+ * Styled via `ai-container.css` (Seeds CSS custom properties).
14
+ */
15
+ const AIContainer = React.forwardRef<HTMLDivElement, TypeAIContainerProps>(
16
+ ({ children, appearance = "subtle", className, ...props }, ref) => {
17
+ return (
18
+ <div
19
+ ref={ref}
20
+ className={cn(
21
+ "seeds-ai-container",
22
+ { "seeds-ai-container-vivid": appearance === "vivid" },
23
+ className
24
+ )}
25
+ {...props}
26
+ >
27
+ {children}
28
+ </div>
29
+ );
30
+ }
31
+ );
32
+
33
+ AIContainer.displayName = "AIContainer";
34
+
35
+ export default AIContainer;
@@ -0,0 +1,14 @@
1
+ import type * as React from "react";
2
+
3
+ export interface TypeAIContainerProps
4
+ extends React.HTMLAttributes<HTMLDivElement> {
5
+ /** Content rendered inside the container. */
6
+ children?: React.ReactNode;
7
+ /**
8
+ * Visual treatment of the AI gradient surface.
9
+ * - `subtle` — a light 20% gradient tint over the base surface with dark text.
10
+ * - `vivid` — the full-saturation AI gradient with white text (always white).
11
+ * @default "subtle"
12
+ */
13
+ appearance?: "subtle" | "vivid";
14
+ }
@@ -0,0 +1,61 @@
1
+ import React from "react";
2
+ import { render, screen } from "@sproutsocial/seeds-react-testing-library";
3
+ import AIContainer from "../AIContainer";
4
+
5
+ describe("AIContainer", () => {
6
+ it("renders its children", () => {
7
+ render(<AIContainer>Summary text</AIContainer>);
8
+
9
+ expect(screen.getByText("Summary text")).toBeInTheDocument();
10
+ });
11
+
12
+ it("renders as a div", () => {
13
+ const { container } = render(<AIContainer>content</AIContainer>);
14
+
15
+ expect(container.firstChild?.nodeName).toBe("DIV");
16
+ });
17
+
18
+ it("applies the base class", () => {
19
+ const { container } = render(<AIContainer>content</AIContainer>);
20
+
21
+ expect(container.querySelector(".seeds-ai-container")).toBeInTheDocument();
22
+ });
23
+
24
+ it("does not apply the vivid modifier by default", () => {
25
+ const { container } = render(<AIContainer>content</AIContainer>);
26
+
27
+ expect(
28
+ container.querySelector(".seeds-ai-container-vivid")
29
+ ).not.toBeInTheDocument();
30
+ });
31
+
32
+ it("applies the vivid modifier only for the vivid appearance", () => {
33
+ const { container, rerender } = render(<AIContainer>content</AIContainer>);
34
+ expect(
35
+ container.querySelector(".seeds-ai-container-vivid")
36
+ ).not.toBeInTheDocument();
37
+
38
+ rerender(<AIContainer appearance="vivid">content</AIContainer>);
39
+ expect(
40
+ container.querySelector(".seeds-ai-container-vivid")
41
+ ).toBeInTheDocument();
42
+ });
43
+
44
+ it("merges a consumer className with the base class", () => {
45
+ const { container } = render(
46
+ <AIContainer className="custom">content</AIContainer>
47
+ );
48
+
49
+ const root = container.querySelector(".seeds-ai-container");
50
+ expect(root).toBeInTheDocument();
51
+ expect(root).toHaveClass("custom");
52
+ });
53
+
54
+ it("forwards its ref to the underlying div element", () => {
55
+ const ref = React.createRef<HTMLDivElement>();
56
+ render(<AIContainer ref={ref}>content</AIContainer>);
57
+
58
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
59
+ expect(ref.current?.tagName).toBe("DIV");
60
+ });
61
+ });
@@ -0,0 +1,2 @@
1
+ export { default as AIContainer } from "./AIContainer";
2
+ export type { TypeAIContainerProps } from "./AIContainerTypes";
@@ -0,0 +1,78 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import NarrativeAttribution from "./NarrativeAttribution";
4
+ import { PartnerLogo } from "@sproutsocial/seeds-react-partner-logo";
5
+ import "../narrative-attribution.css";
6
+
7
+ const meta = {
8
+ title: "Narrative Kit/NarrativeAttribution",
9
+ component: NarrativeAttribution,
10
+ args: {
11
+ avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=Felix",
12
+ userName: "Sprout Social",
13
+ },
14
+ } satisfies Meta<typeof NarrativeAttribution>;
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ /** Twitter/X attribution with partner logo. */
20
+ export const Twitter: Story = {
21
+ args: {
22
+ socialIcon: <PartnerLogo partnerName="twitter" size="small" />,
23
+ },
24
+ };
25
+
26
+ /** Instagram attribution with partner logo. */
27
+ export const Instagram: Story = {
28
+ args: {
29
+ avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=Aneka",
30
+ userName: "design.studio",
31
+ socialIcon: <PartnerLogo partnerName="instagram" size="small" />,
32
+ },
33
+ };
34
+
35
+ /** LinkedIn attribution with partner logo. */
36
+ export const LinkedIn: Story = {
37
+ args: {
38
+ avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=Bailey",
39
+ userName: "Tech Company",
40
+ socialIcon: <PartnerLogo partnerName="linkedin" size="small" />,
41
+ },
42
+ };
43
+
44
+ /** Facebook attribution with partner logo. */
45
+ export const Facebook: Story = {
46
+ args: {
47
+ avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=Charlie",
48
+ userName: "Community Page",
49
+ socialIcon: <PartnerLogo partnerName="facebook" size="small" />,
50
+ },
51
+ };
52
+
53
+ /** YouTube attribution with partner logo. */
54
+ export const YouTube: Story = {
55
+ args: {
56
+ avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=Dakota",
57
+ userName: "Creator Channel",
58
+ socialIcon: <PartnerLogo partnerName="youtube" size="small" />,
59
+ },
60
+ };
61
+
62
+ /** TikTok attribution with partner logo. */
63
+ export const TikTok: Story = {
64
+ args: {
65
+ avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=Ellis",
66
+ userName: "viral.content",
67
+ socialIcon: <PartnerLogo partnerName="tiktok" size="small" />,
68
+ },
69
+ };
70
+
71
+ /** Threads attribution with partner logo icon. */
72
+ export const Threads: Story = {
73
+ args: {
74
+ avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=Jordan",
75
+ userName: "threads.user",
76
+ socialIcon: <PartnerLogo partnerName="threads" size="small" />,
77
+ },
78
+ };
@@ -0,0 +1,54 @@
1
+ import * as React from "react";
2
+ import { cn } from "../_internal/cn";
3
+ import type { TypeNarrativeAttributionProps } from "./NarrativeAttributionTypes";
4
+
5
+ /**
6
+ * NarrativeAttribution — a narrative primitive that displays attribution
7
+ * information for social media content, including an avatar, social network
8
+ * icon, and user name.
9
+ *
10
+ * Styled via narrative-attribution.css (Seeds CSS custom properties);
11
+ * consumers import the classes through @sproutsocial/racine/css/components.
12
+ * Layout is horizontal with avatar, social icon, and user name arranged
13
+ * side by side. Surface overrides are done with standard className / style.
14
+ */
15
+ const NarrativeAttribution = React.forwardRef<
16
+ HTMLDivElement,
17
+ TypeNarrativeAttributionProps
18
+ >(
19
+ (
20
+ {
21
+ avatarUrl,
22
+ avatarAlt = "Profile avatar",
23
+ userName,
24
+ socialIcon,
25
+ className,
26
+ ...props
27
+ },
28
+ ref
29
+ ) => {
30
+ return (
31
+ <div
32
+ ref={ref}
33
+ className={cn("seeds-narrative-attribution", className)}
34
+ {...props}
35
+ >
36
+ <img
37
+ src={avatarUrl}
38
+ alt={avatarAlt}
39
+ className="seeds-narrative-attribution-avatar"
40
+ />
41
+ <div className="seeds-narrative-attribution-social-icon">
42
+ {socialIcon}
43
+ </div>
44
+ <span className="seeds-narrative-attribution-user-name">
45
+ {userName}
46
+ </span>
47
+ </div>
48
+ );
49
+ }
50
+ );
51
+
52
+ NarrativeAttribution.displayName = "NarrativeAttribution";
53
+
54
+ export default NarrativeAttribution;
@@ -0,0 +1,13 @@
1
+ import type * as React from "react";
2
+
3
+ export interface TypeNarrativeAttributionProps
4
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
5
+ /** Avatar image URL for the social media profile. */
6
+ avatarUrl: string;
7
+ /** Alt text for the avatar image. */
8
+ avatarAlt?: string;
9
+ /** User name or display name for the social media profile. */
10
+ userName: string;
11
+ /** Social media network icon (e.g., Twitter, Instagram, LinkedIn). */
12
+ socialIcon: React.ReactNode;
13
+ }
@@ -0,0 +1,2 @@
1
+ export { default as NarrativeAttribution } from "./NarrativeAttribution";
2
+ export type { TypeNarrativeAttributionProps } from "./NarrativeAttributionTypes";
@@ -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,2 @@
1
+ export { default as NarrativeDataCallout } from "./NarrativeDataCallout";
2
+ export type { TypeNarrativeDataCalloutProps } from "./NarrativeDataCalloutTypes";
@@ -0,0 +1,69 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import Button from "@sproutsocial/seeds-react-button";
4
+ import NarrativeOrderedList from "./NarrativeOrderedList";
5
+ import "../narrative-ordered-list.css";
6
+ import "@sproutsocial/seeds-react-button/dist/button.css";
7
+
8
+ /**
9
+ * NarrativeOrderedList renders a numbered list of action items. Each item pairs
10
+ * a large ordinal (01, 02, …) with a title, an optional description, and an
11
+ * optional call-to-action slot. It renders inner content only, meant to be
12
+ * dropped into a `NarrativeContainer` in production (see the Playground story
13
+ * for that composition).
14
+ *
15
+ * Layout is container-query-driven, so it responds to the width it is given
16
+ * rather than the viewport. The `"horizontal"` variant collapses back to a
17
+ * stack when the container is narrow.
18
+ */
19
+ const description =
20
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio leo, gravida vitae sodales vitae.";
21
+
22
+ // No system props → the Button renders its Tailwind path.
23
+ const suggestedAction = (
24
+ <Button appearance="ai-secondary">Suggested Action</Button>
25
+ );
26
+
27
+ const meta = {
28
+ title: "Narrative Kit/NarrativeOrderedList",
29
+ component: NarrativeOrderedList,
30
+ args: {
31
+ items: [
32
+ { title: "Action title", description, action: suggestedAction },
33
+ { title: "Action title", description, action: suggestedAction },
34
+ { title: "Action title", description, action: suggestedAction },
35
+ ],
36
+ },
37
+ // Constrain the width so the layout has a realistic amount of room.
38
+ decorators: [
39
+ (Story) => (
40
+ <div className="max-w-256">
41
+ <Story />
42
+ </div>
43
+ ),
44
+ ],
45
+ } satisfies Meta<typeof NarrativeOrderedList>;
46
+
47
+ export default meta;
48
+ type Story = StoryObj<typeof meta>;
49
+
50
+ /** Items stacked full-width, one per row (the default). */
51
+ export const Vertical: Story = {};
52
+
53
+ /** Items laid out as a row of equal columns. */
54
+ export const Horizontal: Story = {
55
+ args: {
56
+ orientation: "horizontal",
57
+ },
58
+ };
59
+
60
+ /** The action is optional — omit it and each item drops the button. */
61
+ export const WithoutAction: Story = {
62
+ args: {
63
+ items: [
64
+ { title: "Action title", description },
65
+ { title: "Action title", description },
66
+ { title: "Action title", description },
67
+ ],
68
+ },
69
+ };
@@ -0,0 +1,74 @@
1
+ import * as React from "react";
2
+ import { cn } from "../_internal/cn";
3
+ import type { TypeNarrativeOrderedListProps } from "./NarrativeOrderedListTypes";
4
+
5
+ /**
6
+ * NarrativeOrderedList — a numbered list of action items. Each item pairs a
7
+ * large ordinal (01, 02, …) with a title, an optional description, and an
8
+ * optional call-to-action slot.
9
+ *
10
+ * Renders inner content only — it carries no surface, shadow, or accent of its
11
+ * own. Drop it into a `NarrativeContainer` (see the stories / playground).
12
+ *
13
+ * Layout is driven by container queries, so it responds to the width the
14
+ * component is actually given, not the viewport:
15
+ * - `"vertical"` (default) → items stacked full-width, one per row.
16
+ * - `"horizontal"` → items laid out as a row of equal columns once there is
17
+ * room, collapsing back to a stack when the container is narrow.
18
+ *
19
+ * Styled via `narrative-ordered-list.css` (Seeds CSS custom properties);
20
+ * consumers import the classes through `@sproutsocial/racine/css/components`.
21
+ * Overrides come via standard `className` / `style`.
22
+ */
23
+ const NarrativeOrderedList = React.forwardRef<
24
+ HTMLDivElement,
25
+ TypeNarrativeOrderedListProps
26
+ >(({ items, orientation = "vertical", className, ...props }, ref) => {
27
+ return (
28
+ // The outer element establishes the query container. The ol inside switches
29
+ // direction based on this element's width — a container query can only style
30
+ // descendants of the container, never the container element itself.
31
+ <div
32
+ ref={ref}
33
+ className={cn("seeds-narrative-ordered-list", className)}
34
+ {...props}
35
+ >
36
+ <ol
37
+ className={cn("seeds-narrative-ordered-list-track", {
38
+ "seeds-narrative-ordered-list-horizontal":
39
+ orientation === "horizontal",
40
+ })}
41
+ >
42
+ {items.map((item, i) => (
43
+ <li key={i} className="seeds-narrative-ordered-list-item">
44
+ {/* Decorative — the ol/li already convey order to assistive tech, so
45
+ hide the visible ordinal from it to avoid double-announcing. */}
46
+ <span
47
+ className="seeds-narrative-ordered-list-number"
48
+ aria-hidden="true"
49
+ >
50
+ {String(i + 1).padStart(2, "0")}
51
+ </span>
52
+ <div className="seeds-narrative-ordered-list-body">
53
+ <p className="seeds-narrative-ordered-list-title">{item.title}</p>
54
+ {item.description != null && (
55
+ <p className="seeds-narrative-ordered-list-description">
56
+ {item.description}
57
+ </p>
58
+ )}
59
+ {item.action != null && (
60
+ <div className="seeds-narrative-ordered-list-action">
61
+ {item.action}
62
+ </div>
63
+ )}
64
+ </div>
65
+ </li>
66
+ ))}
67
+ </ol>
68
+ </div>
69
+ );
70
+ });
71
+
72
+ NarrativeOrderedList.displayName = "NarrativeOrderedList";
73
+
74
+ export default NarrativeOrderedList;