@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,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@sproutsocial/seeds-react-testing-library";
|
|
3
|
+
import NarrativeDivider from "../NarrativeDivider";
|
|
4
|
+
|
|
5
|
+
describe("NarrativeDivider", () => {
|
|
6
|
+
it("renders a horizontal rule with the base class", () => {
|
|
7
|
+
const { container } = render(<NarrativeDivider />);
|
|
8
|
+
|
|
9
|
+
const root = container.querySelector(".seeds-narrative-divider");
|
|
10
|
+
expect(root).toBeInTheDocument();
|
|
11
|
+
expect(root?.tagName).toBe("HR");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("merges a consumer className with the base class", () => {
|
|
15
|
+
const { container } = render(<NarrativeDivider className="custom" />);
|
|
16
|
+
|
|
17
|
+
const root = container.querySelector(".seeds-narrative-divider");
|
|
18
|
+
expect(root).toBeInTheDocument();
|
|
19
|
+
expect(root).toHaveClass("custom");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("forwards extra props to the underlying element", () => {
|
|
23
|
+
render(<NarrativeDivider data-testid="divider" />);
|
|
24
|
+
|
|
25
|
+
expect(screen.getByTestId("divider")).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("forwards its ref to the underlying hr element", () => {
|
|
29
|
+
const ref = React.createRef<HTMLHRElement>();
|
|
30
|
+
render(<NarrativeDivider ref={ref} />);
|
|
31
|
+
|
|
32
|
+
expect(ref.current).toBeInstanceOf(HTMLHRElement);
|
|
33
|
+
expect(ref.current?.tagName).toBe("HR");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -17,19 +17,28 @@ import type { TypeNarrativeHeadlineProps } from "./NarrativeHeadlineTypes";
|
|
|
17
17
|
const NarrativeHeadline = React.forwardRef<
|
|
18
18
|
HTMLHeadingElement,
|
|
19
19
|
TypeNarrativeHeadlineProps
|
|
20
|
-
>(
|
|
21
|
-
|
|
20
|
+
>(
|
|
21
|
+
(
|
|
22
|
+
{ children, headingLevel = 2, size = "default", className, ...props },
|
|
23
|
+
ref
|
|
24
|
+
) => {
|
|
25
|
+
const Heading = `h${headingLevel}` as const;
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
return (
|
|
28
|
+
<Heading
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn(
|
|
31
|
+
"seeds-narrative-headline",
|
|
32
|
+
{ "seeds-narrative-headline-small": size === "small" },
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</Heading>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
);
|
|
33
42
|
|
|
34
43
|
NarrativeHeadline.displayName = "NarrativeHeadline";
|
|
35
44
|
|
|
@@ -74,7 +74,10 @@ const NarrativeHeadlineRoll = React.forwardRef<
|
|
|
74
74
|
>
|
|
75
75
|
{/* Sizer keeps the container width matched to the active word — without
|
|
76
76
|
it the absolutely-positioned words would collapse the box. */}
|
|
77
|
-
<span
|
|
77
|
+
<span
|
|
78
|
+
className="seeds-narrative-headline-roll-sizer"
|
|
79
|
+
aria-hidden="true"
|
|
80
|
+
>
|
|
78
81
|
{words[i]}
|
|
79
82
|
</span>
|
|
80
83
|
{words.map((w, idx) => (
|
|
@@ -3,6 +3,9 @@ import type * as React from "react";
|
|
|
3
3
|
/** Heading level the headline text renders as. */
|
|
4
4
|
export type TypeNarrativeHeadlineLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
5
5
|
|
|
6
|
+
/** Visual size of the headline. Independent of the semantic heading level. */
|
|
7
|
+
export type TypeNarrativeHeadlineSize = "default" | "small";
|
|
8
|
+
|
|
6
9
|
export interface TypeNarrativeHeadlineProps
|
|
7
10
|
extends React.HTMLAttributes<HTMLHeadingElement> {
|
|
8
11
|
/**
|
|
@@ -17,6 +20,12 @@ export interface TypeNarrativeHeadlineProps
|
|
|
17
20
|
* @default 2
|
|
18
21
|
*/
|
|
19
22
|
headingLevel?: TypeNarrativeHeadlineLevel;
|
|
23
|
+
/**
|
|
24
|
+
* Visual size. `"default"` is the display size (32px / 40px); `"small"` is the
|
|
25
|
+
* compact size (24px / 32px) used inside denser layouts like NarrativeSummary.
|
|
26
|
+
* @default "default"
|
|
27
|
+
*/
|
|
28
|
+
size?: TypeNarrativeHeadlineSize;
|
|
20
29
|
}
|
|
21
30
|
|
|
22
31
|
export interface TypeNarrativeHeadlineHighlightProps
|
|
@@ -12,9 +12,7 @@ describe("NarrativeHeadline", () => {
|
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
it("renders the headline as an h2 by default and honors headingLevel", () => {
|
|
15
|
-
const { rerender } = render(
|
|
16
|
-
<NarrativeHeadline>Title</NarrativeHeadline>
|
|
17
|
-
);
|
|
15
|
+
const { rerender } = render(<NarrativeHeadline>Title</NarrativeHeadline>);
|
|
18
16
|
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
|
|
19
17
|
"Title"
|
|
20
18
|
);
|
|
@@ -101,10 +99,7 @@ describe("NarrativeHeadlineRoll", () => {
|
|
|
101
99
|
jest.useFakeTimers();
|
|
102
100
|
try {
|
|
103
101
|
const { container } = render(
|
|
104
|
-
<NarrativeHeadlineRoll
|
|
105
|
-
words={["first", "second"]}
|
|
106
|
-
intervalMs={1000}
|
|
107
|
-
/>
|
|
102
|
+
<NarrativeHeadlineRoll words={["first", "second"]} intervalMs={1000} />
|
|
108
103
|
);
|
|
109
104
|
|
|
110
105
|
const wordsBefore = container.querySelectorAll(
|
|
@@ -4,6 +4,7 @@ export { default as NarrativeHeadlineRoll } from "./NarrativeHeadlineRoll";
|
|
|
4
4
|
export type {
|
|
5
5
|
TypeNarrativeHeadlineProps,
|
|
6
6
|
TypeNarrativeHeadlineLevel,
|
|
7
|
+
TypeNarrativeHeadlineSize,
|
|
7
8
|
TypeNarrativeHeadlineHighlightProps,
|
|
8
9
|
TypeNarrativeHeadlineRollProps,
|
|
9
10
|
} from "./NarrativeHeadlineTypes";
|
|
@@ -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;
|
|
@@ -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,79 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import Button from "@sproutsocial/seeds-react-button";
|
|
4
|
+
import NarrativeSummary from "./NarrativeSummary";
|
|
5
|
+
import "../narrative-summary.css";
|
|
6
|
+
import "../narrative-headline.css";
|
|
7
|
+
import "../eyebrow-token.css";
|
|
8
|
+
import "@sproutsocial/seeds-react-button/dist/button.css";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* NarrativeSummary pairs a lead column (eyebrow + headline + optional action)
|
|
12
|
+
* with a Summary section and a Key themes list. It renders inner content only,
|
|
13
|
+
* meant to be dropped into a `NarrativeContainer` in production (see the
|
|
14
|
+
* Playground story for that composition).
|
|
15
|
+
*
|
|
16
|
+
* The layout is a container-query-driven CSS grid, so it responds to the width
|
|
17
|
+
* it is given rather than the viewport.
|
|
18
|
+
*/
|
|
19
|
+
const meta = {
|
|
20
|
+
title: "Narrative Kit/NarrativeSummary",
|
|
21
|
+
component: NarrativeSummary,
|
|
22
|
+
args: {
|
|
23
|
+
eyebrow: "June 10th",
|
|
24
|
+
headline: "Headline goes here and wraps to second line",
|
|
25
|
+
summary:
|
|
26
|
+
"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. Praesent pretium libero vitae arcu blandit pellentesque. Aenean fermentum, nisi eu blandit placerat, ligula ligula euismod ante, id semper nibh diam ac ipsum.",
|
|
27
|
+
keyThemes: [
|
|
28
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
|
29
|
+
"Donec odio leo, gravida vitae sodales vitae, tempor eget ligula.",
|
|
30
|
+
"Vestibulum nec enim eget mi placerat finibus.",
|
|
31
|
+
"Donec tempor tincidunt elit. Praesent pretium libero vitae arcu blandit pellentesque.",
|
|
32
|
+
],
|
|
33
|
+
// No system props → the Button renders its Tailwind path.
|
|
34
|
+
action: <Button appearance="primary">Action</Button>,
|
|
35
|
+
},
|
|
36
|
+
// Constrain the width so the grid has a realistic amount of room to lay out.
|
|
37
|
+
decorators: [
|
|
38
|
+
(Story) => (
|
|
39
|
+
<div className="max-w-256">
|
|
40
|
+
<Story />
|
|
41
|
+
</div>
|
|
42
|
+
),
|
|
43
|
+
],
|
|
44
|
+
} satisfies Meta<typeof NarrativeSummary>;
|
|
45
|
+
|
|
46
|
+
export default meta;
|
|
47
|
+
type Story = StoryObj<typeof meta>;
|
|
48
|
+
|
|
49
|
+
/** Lead | Summary | Key themes — three side-by-side columns. */
|
|
50
|
+
export const ThreeColumn: Story = {};
|
|
51
|
+
|
|
52
|
+
/** A 1/3 | 2/3 split: the lead column beside a stacked Summary + Key themes. */
|
|
53
|
+
export const TwoColumn: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
layout: "two-column",
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** The action is optional — omit it and the lead column drops the button. */
|
|
60
|
+
export const WithoutAction: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
action: undefined,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Responsiveness is container-driven. In this narrow container the grid
|
|
68
|
+
* collapses to a single stacked column; widen the container (or view the
|
|
69
|
+
* ThreeColumn story) to see it expand. Resize the canvas to watch it reflow.
|
|
70
|
+
*/
|
|
71
|
+
export const Responsive: Story = {
|
|
72
|
+
decorators: [
|
|
73
|
+
(Story) => (
|
|
74
|
+
<div className="max-w-96">
|
|
75
|
+
<Story />
|
|
76
|
+
</div>
|
|
77
|
+
),
|
|
78
|
+
],
|
|
79
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../_internal/cn";
|
|
3
|
+
import EyebrowToken from "../EyebrowToken/EyebrowToken";
|
|
4
|
+
import NarrativeHeadline from "../NarrativeHeadline/NarrativeHeadline";
|
|
5
|
+
import type { TypeNarrativeSummaryProps } from "./NarrativeSummaryTypes";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* NarrativeSummary — a composed narrative block that pairs a lead column
|
|
9
|
+
* (eyebrow + headline + optional action) with a Summary section and a Key
|
|
10
|
+
* themes list.
|
|
11
|
+
*
|
|
12
|
+
* Renders inner content only — it carries no surface, shadow, or accent of its
|
|
13
|
+
* own. Drop it into a `NarrativeContainer` (see the stories / playground).
|
|
14
|
+
*
|
|
15
|
+
* Layout is CSS Grid driven by container queries, so the columns collapse based
|
|
16
|
+
* on the width the component is actually given, not the viewport:
|
|
17
|
+
* - `"three-column"` → lead | Summary | Key themes.
|
|
18
|
+
* - `"two-column"` → a 1/3 | 2/3 split with Summary and Key themes stacked in
|
|
19
|
+
* the wider column.
|
|
20
|
+
*
|
|
21
|
+
* Styled via `narrative-summary.css` (Seeds CSS custom properties); consumers
|
|
22
|
+
* import the classes through `@sproutsocial/racine/css/components`. Overrides
|
|
23
|
+
* come via standard `className` / `style`.
|
|
24
|
+
*/
|
|
25
|
+
const NarrativeSummary = React.forwardRef<
|
|
26
|
+
HTMLDivElement,
|
|
27
|
+
TypeNarrativeSummaryProps
|
|
28
|
+
>(
|
|
29
|
+
(
|
|
30
|
+
{
|
|
31
|
+
eyebrow,
|
|
32
|
+
headline,
|
|
33
|
+
headingLevel = 2,
|
|
34
|
+
action,
|
|
35
|
+
summary,
|
|
36
|
+
summaryLabel = "Summary",
|
|
37
|
+
keyThemes,
|
|
38
|
+
keyThemesLabel = "Key themes",
|
|
39
|
+
layout = "three-column",
|
|
40
|
+
className,
|
|
41
|
+
...props
|
|
42
|
+
},
|
|
43
|
+
ref
|
|
44
|
+
) => {
|
|
45
|
+
const hasKeyThemes = keyThemes != null && keyThemes.length > 0;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn("seeds-narrative-summary", className)}
|
|
51
|
+
{...props}
|
|
52
|
+
>
|
|
53
|
+
<div
|
|
54
|
+
className={cn("seeds-narrative-summary-grid", {
|
|
55
|
+
"seeds-narrative-summary-two-column": layout === "two-column",
|
|
56
|
+
})}
|
|
57
|
+
>
|
|
58
|
+
<div className="seeds-narrative-summary-lead">
|
|
59
|
+
{eyebrow != null && <EyebrowToken>{eyebrow}</EyebrowToken>}
|
|
60
|
+
<div className="seeds-narrative-summary-headline">
|
|
61
|
+
<NarrativeHeadline size="small" headingLevel={headingLevel}>
|
|
62
|
+
{headline}
|
|
63
|
+
</NarrativeHeadline>
|
|
64
|
+
</div>
|
|
65
|
+
{action != null && (
|
|
66
|
+
<div className="seeds-narrative-summary-action">{action}</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className="seeds-narrative-summary-content">
|
|
71
|
+
{summary != null && (
|
|
72
|
+
<section className="seeds-narrative-summary-section">
|
|
73
|
+
<p className="seeds-narrative-summary-label">{summaryLabel}</p>
|
|
74
|
+
<p className="seeds-narrative-summary-text">{summary}</p>
|
|
75
|
+
</section>
|
|
76
|
+
)}
|
|
77
|
+
{hasKeyThemes && (
|
|
78
|
+
<section className="seeds-narrative-summary-section">
|
|
79
|
+
<p className="seeds-narrative-summary-label">
|
|
80
|
+
{keyThemesLabel}
|
|
81
|
+
</p>
|
|
82
|
+
<ul className="seeds-narrative-summary-list">
|
|
83
|
+
{keyThemes.map((theme, i) => (
|
|
84
|
+
<li key={i}>{theme}</li>
|
|
85
|
+
))}
|
|
86
|
+
</ul>
|
|
87
|
+
</section>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
NarrativeSummary.displayName = "NarrativeSummary";
|
|
97
|
+
|
|
98
|
+
export default NarrativeSummary;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import type { TypeNarrativeHeadlineLevel } from "../NarrativeHeadline/NarrativeHeadlineTypes";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Column arrangement.
|
|
6
|
+
* - `"three-column"`: lead | Summary | Key themes, three side-by-side columns.
|
|
7
|
+
* - `"two-column"`: a 1/3 | 2/3 split — lead on the left, with Summary and Key
|
|
8
|
+
* themes stacked together in the wider right column.
|
|
9
|
+
*/
|
|
10
|
+
export type TypeNarrativeSummaryLayout = "three-column" | "two-column";
|
|
11
|
+
|
|
12
|
+
export interface TypeNarrativeSummaryProps
|
|
13
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
14
|
+
/**
|
|
15
|
+
* Kicker shown in an `EyebrowToken` above the headline (e.g. a date). Omit to
|
|
16
|
+
* hide the eyebrow entirely.
|
|
17
|
+
*/
|
|
18
|
+
eyebrow?: React.ReactNode;
|
|
19
|
+
/** Headline text — rendered via `NarrativeHeadline` at `size="small"`. */
|
|
20
|
+
headline: React.ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* Heading element the headline renders as, for document outline /
|
|
23
|
+
* accessibility.
|
|
24
|
+
* @default 2
|
|
25
|
+
*/
|
|
26
|
+
headingLevel?: TypeNarrativeHeadlineLevel;
|
|
27
|
+
/**
|
|
28
|
+
* Optional action, typically a `<Button>`. Rendered below the headline only
|
|
29
|
+
* when provided. Pass a Button without system props so it renders the Tailwind
|
|
30
|
+
* path (e.g. `<Button appearance="primary">Action</Button>`).
|
|
31
|
+
*
|
|
32
|
+
* Note: in the MVP this will most often be left unset (null) — the AC is
|
|
33
|
+
* poorly defined at this time and needs more definition before the data model
|
|
34
|
+
* will properly support it.
|
|
35
|
+
*/
|
|
36
|
+
action?: React.ReactNode;
|
|
37
|
+
/** Summary body content. The section is omitted when this is empty. */
|
|
38
|
+
summary?: React.ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Label above the summary.
|
|
41
|
+
* @default "Summary"
|
|
42
|
+
*/
|
|
43
|
+
summaryLabel?: React.ReactNode;
|
|
44
|
+
/**
|
|
45
|
+
* Key themes, rendered as a bulleted list. The section is omitted when this
|
|
46
|
+
* is empty.
|
|
47
|
+
*/
|
|
48
|
+
keyThemes?: string[];
|
|
49
|
+
/**
|
|
50
|
+
* Label above the key themes.
|
|
51
|
+
* @default "Key themes"
|
|
52
|
+
*/
|
|
53
|
+
keyThemesLabel?: React.ReactNode;
|
|
54
|
+
/**
|
|
55
|
+
* Column arrangement. `"two-column"` is a 1/3 | 2/3 split.
|
|
56
|
+
* @default "three-column"
|
|
57
|
+
*/
|
|
58
|
+
layout?: TypeNarrativeSummaryLayout;
|
|
59
|
+
}
|