@sproutsocial/seeds-react-narrative-kit 0.3.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 +27 -0
- package/dist/ai-container.css +39 -0
- package/dist/esm/index.js +98 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +123 -4
- package/dist/index.d.ts +123 -4
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -1
- package/dist/narrative-data-callout.css +103 -0
- package/dist/narrative-ordered-list.css +112 -0
- package/dist/pull-quote.css +4 -25
- package/package.json +8 -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/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/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/Playground/Playground.stories.tsx +93 -5
- package/src/PullQuote/PullQuote.stories.tsx +1 -0
- package/src/PullQuote/PullQuote.tsx +6 -3
- package/src/ai-container.css +39 -0
- package/src/index.ts +14 -0
- package/src/narrative-data-callout.css +103 -0
- package/src/narrative-ordered-list.css +112 -0
- package/src/pull-quote.css +4 -25
package/dist/pull-quote.css
CHANGED
|
@@ -2,20 +2,17 @@
|
|
|
2
2
|
* Seeds PullQuote component classes.
|
|
3
3
|
* Use these instead of writing out individual Tailwind utility classes.
|
|
4
4
|
*
|
|
5
|
-
* Requires @sproutsocial/seeds-react-theme/dist/theme-all.css
|
|
6
|
-
* CSS variable definitions and
|
|
5
|
+
* Requires @sproutsocial/seeds-react-theme/dist/theme-all.css and
|
|
6
|
+
* ai-container.css imported for CSS variable definitions and surface styles.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* <figure class="seeds-pull-quote">…</figure> // subtle (default)
|
|
10
|
-
* <figure class="seeds-pull-quote seeds-pull-quote-vivid">…</figure>
|
|
9
|
+
* <figure class="seeds-pull-quote seeds-ai-container">…</figure> // subtle (default)
|
|
10
|
+
* <figure class="seeds-pull-quote seeds-ai-container seeds-ai-container-vivid seeds-pull-quote-vivid">…</figure>
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
@layer components {
|
|
14
14
|
|
|
15
15
|
.seeds-pull-quote {
|
|
16
|
-
position: relative;
|
|
17
|
-
overflow: hidden;
|
|
18
|
-
box-sizing: border-box;
|
|
19
16
|
width: 100%;
|
|
20
17
|
display: flex;
|
|
21
18
|
flex-direction: column;
|
|
@@ -24,24 +21,6 @@
|
|
|
24
21
|
/* 40px block / 48px inline — 48px is off the space scale, so derive it from
|
|
25
22
|
two tokens (40 + 8) rather than hardcoding an off-scale value. */
|
|
26
23
|
padding: var(--space-600) calc(var(--space-600) + var(--space-300));
|
|
27
|
-
border-radius: var(--radius-800);
|
|
28
|
-
box-shadow: var(--shadow-low);
|
|
29
|
-
font-family: var(--font-family);
|
|
30
|
-
/* Subtle (default): the 20% AI gradient tint composited over the base
|
|
31
|
-
surface. --color-gradient-ai-subtle is already translucent, so layering it
|
|
32
|
-
over the opaque base color produces the soft wash. */
|
|
33
|
-
background-color: var(--color-container-bg-base);
|
|
34
|
-
background-image: var(--color-gradient-ai-subtle);
|
|
35
|
-
color: var(--color-text-headline);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/* Vivid: the full-saturation AI gradient with white content. White has no
|
|
39
|
-
semantic Seeds token; it is part of the AI gradient treatment (mirrors the
|
|
40
|
-
raw brand hex used in --color-gradient-ai). */
|
|
41
|
-
.seeds-pull-quote-vivid {
|
|
42
|
-
background-color: transparent;
|
|
43
|
-
background-image: var(--color-gradient-ai);
|
|
44
|
-
color: #fff;
|
|
45
24
|
}
|
|
46
25
|
|
|
47
26
|
/* The accented bar holding the quote and attribution. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-narrative-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Seeds React Narrative Kit — composable narrative primitives for executive-brief style pages",
|
|
5
5
|
"author": "Sprout Social, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,10 +19,13 @@
|
|
|
19
19
|
"./dist/metric-highlight.css": "./dist/metric-highlight.css",
|
|
20
20
|
"./dist/eyebrow-token.css": "./dist/eyebrow-token.css",
|
|
21
21
|
"./dist/narrative-summary.css": "./dist/narrative-summary.css",
|
|
22
|
-
"./dist/narrative-divider.css": "./dist/narrative-divider.css"
|
|
22
|
+
"./dist/narrative-divider.css": "./dist/narrative-divider.css",
|
|
23
|
+
"./dist/narrative-ordered-list.css": "./dist/narrative-ordered-list.css",
|
|
24
|
+
"./dist/narrative-data-callout.css": "./dist/narrative-data-callout.css",
|
|
25
|
+
"./dist/ai-container.css": "./dist/ai-container.css"
|
|
23
26
|
},
|
|
24
27
|
"scripts": {
|
|
25
|
-
"build": "tsup --dts && cp src/narrative-container.css dist/narrative-container.css && cp src/narrative-headline.css dist/narrative-headline.css && cp src/pull-quote.css dist/pull-quote.css && cp src/metric-highlight.css dist/metric-highlight.css && cp src/eyebrow-token.css dist/eyebrow-token.css && cp src/narrative-summary.css dist/narrative-summary.css && cp src/narrative-divider.css dist/narrative-divider.css",
|
|
28
|
+
"build": "tsup --dts && cp src/narrative-container.css dist/narrative-container.css && cp src/narrative-headline.css dist/narrative-headline.css && cp src/pull-quote.css dist/pull-quote.css && cp src/metric-highlight.css dist/metric-highlight.css && cp src/eyebrow-token.css dist/eyebrow-token.css && cp src/narrative-summary.css dist/narrative-summary.css && cp src/narrative-divider.css dist/narrative-divider.css && cp src/narrative-ordered-list.css dist/narrative-ordered-list.css && cp src/narrative-data-callout.css dist/narrative-data-callout.css && cp src/ai-container.css dist/ai-container.css",
|
|
26
29
|
"build:debug": "tsup --dts --metafile",
|
|
27
30
|
"dev": "tsup --watch --dts",
|
|
28
31
|
"clean": "rm -rf .turbo dist",
|
|
@@ -32,13 +35,13 @@
|
|
|
32
35
|
"test:watch": "jest --watch --coverage=false"
|
|
33
36
|
},
|
|
34
37
|
"dependencies": {
|
|
35
|
-
"@sproutsocial/seeds-react-theme": "^4.1.
|
|
38
|
+
"@sproutsocial/seeds-react-theme": "^4.1.1"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
38
41
|
"@sproutsocial/eslint-config-seeds": "*",
|
|
39
42
|
"@sproutsocial/seeds-react-button": "*",
|
|
40
43
|
"@sproutsocial/seeds-react-data-viz": "*",
|
|
41
|
-
"@sproutsocial/seeds-react-grid": "^0.2.
|
|
44
|
+
"@sproutsocial/seeds-react-grid": "^0.2.6",
|
|
42
45
|
"@sproutsocial/seeds-react-text": "*",
|
|
43
46
|
"@sproutsocial/seeds-react-testing-library": "*",
|
|
44
47
|
"@sproutsocial/seeds-testing": "*",
|
|
@@ -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,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,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
|
+
}
|