@nationaldesignstudio/react 0.0.14 → 0.0.16

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 (166) hide show
  1. package/dist/tailwind.css +15 -1
  2. package/dist/tokens.css +45 -60
  3. package/package.json +5 -10
  4. package/src/App.css +0 -0
  5. package/src/App.tsx +7 -0
  6. package/src/assets/fonts/PPNeueMontreal-Variable.woff2 +0 -0
  7. package/src/assets/react.svg +1 -0
  8. package/src/components/atoms/accordion/accordion.stories.tsx +228 -0
  9. package/src/components/atoms/accordion/accordion.tsx +219 -0
  10. package/src/components/atoms/accordion/index.ts +6 -0
  11. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-darwin.png +0 -0
  12. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-chromium-linux.png +0 -0
  13. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-darwin.png +0 -0
  14. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-chromium-linux.png +0 -0
  15. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-darwin.png +0 -0
  16. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-charcoal-outline-quiet-chromium-linux.png +0 -0
  17. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-darwin.png +0 -0
  18. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-disabled-chromium-linux.png +0 -0
  19. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-darwin.png +0 -0
  20. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-chromium-linux.png +0 -0
  21. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-darwin.png +0 -0
  22. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-chromium-linux.png +0 -0
  23. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-darwin.png +0 -0
  24. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-ivory-outline-quiet-chromium-linux.png +0 -0
  25. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-darwin.png +0 -0
  26. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-large-chromium-linux.png +0 -0
  27. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-darwin.png +0 -0
  28. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-medium-chromium-linux.png +0 -0
  29. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-darwin.png +0 -0
  30. package/src/components/atoms/button/__screenshots__/button.visual.test.tsx/button-size-small-chromium-linux.png +0 -0
  31. package/src/components/atoms/button/button.stories.tsx +102 -0
  32. package/src/components/atoms/button/button.test.tsx +135 -0
  33. package/src/components/atoms/button/button.tsx +139 -0
  34. package/src/components/atoms/button/button.visual.test.tsx +102 -0
  35. package/src/components/atoms/button/icon-button.stories.tsx +166 -0
  36. package/src/components/atoms/button/icon-button.tsx +120 -0
  37. package/src/components/atoms/button/index.ts +6 -0
  38. package/src/components/atoms/ndstudio-footer/index.ts +1 -0
  39. package/src/components/atoms/ndstudio-footer/ndstudio-footer.tsx +55 -0
  40. package/src/components/atoms/pager-control/index.ts +5 -0
  41. package/src/components/atoms/pager-control/pager-control.stories.tsx +209 -0
  42. package/src/components/atoms/pager-control/pager-control.test.tsx +130 -0
  43. package/src/components/atoms/pager-control/pager-control.tsx +329 -0
  44. package/src/components/dev-tools/dev-toolbar/dev-toolbar.stories.tsx +82 -0
  45. package/src/components/dev-tools/dev-toolbar/dev-toolbar.tsx +196 -0
  46. package/src/components/dev-tools/dev-toolbar/index.ts +1 -0
  47. package/src/components/dev-tools/grid-overlay/grid-overlay.tsx +41 -0
  48. package/src/components/dev-tools/grid-overlay/index.ts +1 -0
  49. package/src/components/dev-tools/index.ts +2 -0
  50. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-darwin.png +0 -0
  51. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-default-vertical-chromium-linux.png +0 -0
  52. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-darwin.png +0 -0
  53. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-horizontal-chromium-linux.png +0 -0
  54. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-darwin.png +0 -0
  55. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-minimal-chromium-linux.png +0 -0
  56. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-darwin.png +0 -0
  57. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-actions-chromium-linux.png +0 -0
  58. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-darwin.png +0 -0
  59. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-eyebrow-chromium-linux.png +0 -0
  60. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-darwin.png +0 -0
  61. package/src/components/organisms/card/__screenshots__/card.visual.test.tsx/card-without-image-chromium-linux.png +0 -0
  62. package/src/components/organisms/card/card.stories.tsx +293 -0
  63. package/src/components/organisms/card/card.test.tsx +245 -0
  64. package/src/components/organisms/card/card.tsx +225 -0
  65. package/src/components/organisms/card/card.visual.test.tsx +197 -0
  66. package/src/components/organisms/card/index.ts +19 -0
  67. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-darwin.png +0 -0
  68. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-active-link-chromium-linux.png +0 -0
  69. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-darwin.png +0 -0
  70. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-brand-only-chromium-linux.png +0 -0
  71. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-darwin.png +0 -0
  72. package/src/components/organisms/navbar/__screenshots__/navbar.visual.test.tsx/navbar-default-chromium-linux.png +0 -0
  73. package/src/components/organisms/navbar/index.ts +18 -0
  74. package/src/components/organisms/navbar/navbar.stories.tsx +313 -0
  75. package/src/components/organisms/navbar/navbar.test.tsx +190 -0
  76. package/src/components/organisms/navbar/navbar.tsx +323 -0
  77. package/src/components/organisms/navbar/navbar.visual.test.tsx +85 -0
  78. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-darwin.png +0 -0
  79. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-icon-chromium-linux.png +0 -0
  80. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-darwin.png +0 -0
  81. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-custom-text-chromium-linux.png +0 -0
  82. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-darwin.png +0 -0
  83. package/src/components/organisms/us-gov-banner/__screenshots__/us-gov-banner.visual.test.tsx/us-gov-banner-default-chromium-linux.png +0 -0
  84. package/src/components/organisms/us-gov-banner/index.ts +1 -0
  85. package/src/components/organisms/us-gov-banner/us-gov-banner.stories.tsx +35 -0
  86. package/src/components/organisms/us-gov-banner/us-gov-banner.test.tsx +107 -0
  87. package/src/components/organisms/us-gov-banner/us-gov-banner.tsx +73 -0
  88. package/src/components/organisms/us-gov-banner/us-gov-banner.visual.test.tsx +46 -0
  89. package/src/components/sections/banner/banner.stories.tsx +150 -0
  90. package/src/components/sections/banner/banner.test.tsx +185 -0
  91. package/src/components/sections/banner/banner.tsx +130 -0
  92. package/src/components/sections/banner/index.ts +2 -0
  93. package/src/components/sections/card-grid/card-grid.stories.tsx +351 -0
  94. package/src/components/sections/card-grid/card-grid.tsx +116 -0
  95. package/src/components/sections/card-grid/index.ts +1 -0
  96. package/src/components/sections/faq-section/faq-section.stories.tsx +453 -0
  97. package/src/components/sections/faq-section/faq-section.tsx +84 -0
  98. package/src/components/sections/faq-section/index.ts +2 -0
  99. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-darwin.png +0 -0
  100. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-desktop-chromium-linux.png +0 -0
  101. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-darwin.png +0 -0
  102. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-mobile-chromium-linux.png +0 -0
  103. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-darwin.png +0 -0
  104. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a1-tablet-chromium-linux.png +0 -0
  105. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-darwin.png +0 -0
  106. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-desktop-chromium-linux.png +0 -0
  107. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-darwin.png +0 -0
  108. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-mobile-chromium-linux.png +0 -0
  109. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-darwin.png +0 -0
  110. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a2-tablet-chromium-linux.png +0 -0
  111. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-darwin.png +0 -0
  112. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-desktop-chromium-linux.png +0 -0
  113. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-darwin.png +0 -0
  114. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-mobile-chromium-linux.png +0 -0
  115. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-darwin.png +0 -0
  116. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-a3-tablet-chromium-linux.png +0 -0
  117. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-darwin.png +0 -0
  118. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-custom-class-chromium-linux.png +0 -0
  119. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-default-chromium-linux.png +0 -0
  120. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-darwin.png +0 -0
  121. package/src/components/sections/hero/__screenshots__/hero.visual.test.tsx/hero-long-title-chromium-linux.png +0 -0
  122. package/src/components/sections/hero/hero.stories.tsx +274 -0
  123. package/src/components/sections/hero/hero.test.tsx +135 -0
  124. package/src/components/sections/hero/hero.tsx +453 -0
  125. package/src/components/sections/hero/hero.visual.test.tsx +140 -0
  126. package/src/components/sections/hero/index.ts +10 -0
  127. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-darwin.png +0 -0
  128. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-h3-heading-chromium-linux.png +0 -0
  129. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-darwin.png +0 -0
  130. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-paragraphs-chromium-linux.png +0 -0
  131. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-darwin.png +0 -0
  132. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-multiple-sections-chromium-linux.png +0 -0
  133. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-darwin.png +0 -0
  134. package/src/components/sections/prose/__screenshots__/prose.visual.test.tsx/prose-single-section-chromium-linux.png +0 -0
  135. package/src/components/sections/prose/index.ts +6 -0
  136. package/src/components/sections/prose/prose.stories.tsx +144 -0
  137. package/src/components/sections/prose/prose.test.tsx +178 -0
  138. package/src/components/sections/prose/prose.tsx +88 -0
  139. package/src/components/sections/prose/prose.visual.test.tsx +105 -0
  140. package/src/components/sections/river/index.ts +1 -0
  141. package/src/components/sections/river/river.stories.tsx +237 -0
  142. package/src/components/sections/river/river.test.tsx +268 -0
  143. package/src/components/sections/river/river.tsx +173 -0
  144. package/src/components/sections/tout/index.ts +1 -0
  145. package/src/components/sections/tout/tout.stories.tsx +171 -0
  146. package/src/components/sections/tout/tout.test.tsx +242 -0
  147. package/src/components/sections/tout/tout.tsx +270 -0
  148. package/src/components/sections/two-column-section/index.ts +5 -0
  149. package/src/components/sections/two-column-section/two-column-section.stories.tsx +285 -0
  150. package/src/components/sections/two-column-section/two-column-section.tsx +162 -0
  151. package/src/hooks/index.ts +1 -0
  152. package/src/hooks/use-event-listener.ts +73 -0
  153. package/src/index.ts +155 -0
  154. package/src/lib/theme.ts +1000 -0
  155. package/src/lib/utils.ts +6 -0
  156. package/src/main.tsx +13 -0
  157. package/src/stories/GridSystem.stories.tsx +84 -0
  158. package/src/stories/Introduction.mdx +114 -0
  159. package/src/stories/ThemeProvider.stories.tsx +357 -0
  160. package/src/stories/TokenShowcase.stories.tsx +92 -0
  161. package/src/stories/TokenShowcase.tsx +1429 -0
  162. package/src/styles.css +11 -0
  163. package/src/theme/ThemeProvider.tsx +297 -0
  164. package/src/theme/hooks.ts +40 -0
  165. package/src/theme/index.ts +43 -0
  166. package/src/theme/utils.ts +104 -0
@@ -0,0 +1,144 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { Prose, ProseSection } from ".";
3
+
4
+ const meta: Meta<typeof Prose> = {
5
+ title: "Sections/Prose",
6
+ component: Prose,
7
+ parameters: {
8
+ layout: "centered",
9
+ },
10
+ } as Meta<typeof Prose>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof Prose>;
14
+
15
+ const sampleIntro =
16
+ "On the modern web, typography is not decoration; it's the cognitive interface. Before a user understands your product, policy, or program, they're unconsciously reading the structure of your type: the hierarchy of headings, the discipline of your spacing, the confidence or hesitation in your line lengths.";
17
+
18
+ const sampleParagraph1 =
19
+ "Style here isn't about clever fonts, it's about creating a system of visual cues that makes thinking easier. A consistent scale, clear contrast between roles, and careful use of weight and size become a kind of mental exoskeleton for the reader. Done well, typography lowers the friction of understanding to the point where attention can be spent on the content itself rather than on decoding the layout.";
20
+
21
+ const sampleParagraph2 =
22
+ 'Rhythm is where that structure turns into a voice. Online, people experience your text as a sequence of micro-moments: the way a headline sits in space, how paragraphs breathe, where the eye can rest. Tight, compressed leading and dense blocks say, "this is serious, pay attention." Generous white space and measured line lengths say, "you have room to think." Over time, those choices form a recognizable persona, just as surely as a writing style does. When the rhythm is intentional, your site speaks with a calm, authoritative cadence that users begin to trust. When it\'s sloppy or inconsistent, the voice fractures, and so does the sense that there\'s a clear, competent mind behind the interface.';
23
+
24
+ // =============================================================================
25
+ // Default
26
+ // =============================================================================
27
+
28
+ export const Default: Story = {
29
+ render: () => (
30
+ <Prose>
31
+ <ProseSection heading="Headline - Medium" as="h2">
32
+ <p>{sampleIntro}</p>
33
+ </ProseSection>
34
+ <ProseSection heading="Headline - Small" as="h3">
35
+ <p>{sampleParagraph1}</p>
36
+ <p>{sampleParagraph2}</p>
37
+ </ProseSection>
38
+ </Prose>
39
+ ),
40
+ };
41
+
42
+ // =============================================================================
43
+ // Responsive Variants
44
+ // =============================================================================
45
+
46
+ export const Desktop: Story = {
47
+ render: () => (
48
+ <Prose>
49
+ <ProseSection heading="Headline - Medium" as="h2">
50
+ <p>{sampleIntro}</p>
51
+ </ProseSection>
52
+ <ProseSection heading="Headline - Small" as="h3">
53
+ <p>{sampleParagraph1}</p>
54
+ <p>{sampleParagraph2}</p>
55
+ </ProseSection>
56
+ </Prose>
57
+ ),
58
+ globals: {
59
+ viewport: { value: "lg", isRotated: false },
60
+ },
61
+ };
62
+
63
+ export const Tablet: Story = {
64
+ render: () => (
65
+ <Prose>
66
+ <ProseSection heading="Headline - Medium" as="h2">
67
+ <p>{sampleIntro}</p>
68
+ </ProseSection>
69
+ <ProseSection heading="Headline - Small" as="h3">
70
+ <p>{sampleParagraph1}</p>
71
+ <p>{sampleParagraph2}</p>
72
+ </ProseSection>
73
+ </Prose>
74
+ ),
75
+ globals: {
76
+ viewport: { value: "md", isRotated: false },
77
+ },
78
+ };
79
+
80
+ export const Mobile: Story = {
81
+ render: () => (
82
+ <Prose>
83
+ <ProseSection heading="Headline - Medium" as="h2">
84
+ <p>{sampleIntro}</p>
85
+ </ProseSection>
86
+ <ProseSection heading="Headline - Small" as="h3">
87
+ <p>{sampleParagraph1}</p>
88
+ <p>{sampleParagraph2}</p>
89
+ </ProseSection>
90
+ </Prose>
91
+ ),
92
+ globals: {
93
+ viewport: { value: "sm", isRotated: false },
94
+ },
95
+ };
96
+
97
+ // =============================================================================
98
+ // Playground
99
+ // =============================================================================
100
+
101
+ export const Playground: Story = {
102
+ render: (args) => (
103
+ <Prose {...args}>
104
+ <ProseSection heading="Main Heading" as="h2">
105
+ <p>{sampleIntro}</p>
106
+ </ProseSection>
107
+ <ProseSection heading="Subheading" as="h3">
108
+ <p>{sampleParagraph1}</p>
109
+ <p>{sampleParagraph2}</p>
110
+ </ProseSection>
111
+ </Prose>
112
+ ),
113
+ };
114
+
115
+ // =============================================================================
116
+ // Examples
117
+ // =============================================================================
118
+
119
+ export const SingleSection: Story = {
120
+ render: () => (
121
+ <Prose>
122
+ <ProseSection heading="About Our Mission" as="h2">
123
+ <p>{sampleIntro}</p>
124
+ <p>{sampleParagraph1}</p>
125
+ </ProseSection>
126
+ </Prose>
127
+ ),
128
+ };
129
+
130
+ export const MultipleSections: Story = {
131
+ render: () => (
132
+ <Prose>
133
+ <ProseSection heading="Introduction" as="h2">
134
+ <p>{sampleIntro}</p>
135
+ </ProseSection>
136
+ <ProseSection heading="The Importance of Style" as="h3">
137
+ <p>{sampleParagraph1}</p>
138
+ </ProseSection>
139
+ <ProseSection heading="Finding Your Rhythm" as="h3">
140
+ <p>{sampleParagraph2}</p>
141
+ </ProseSection>
142
+ </Prose>
143
+ ),
144
+ };
@@ -0,0 +1,178 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { page } from "vitest/browser";
3
+ import { render } from "vitest-browser-react";
4
+ import { Prose, ProseSection } from "./prose";
5
+
6
+ describe("Prose", () => {
7
+ describe("Accessibility", () => {
8
+ test("ProseSection renders as section landmark", async () => {
9
+ render(
10
+ <Prose>
11
+ <ProseSection heading="Test Section" data-testid="section">
12
+ <p>Content</p>
13
+ </ProseSection>
14
+ </Prose>,
15
+ );
16
+
17
+ const section = page.getByTestId("section");
18
+ await expect.element(section).toBeInTheDocument();
19
+ });
20
+
21
+ test("ProseSection heading renders as h2 by default", async () => {
22
+ render(
23
+ <Prose>
24
+ <ProseSection heading="Default Heading">
25
+ <p>Content</p>
26
+ </ProseSection>
27
+ </Prose>,
28
+ );
29
+
30
+ await expect
31
+ .element(
32
+ page.getByRole("heading", { level: 2, name: "Default Heading" }),
33
+ )
34
+ .toBeInTheDocument();
35
+ });
36
+
37
+ test("ProseSection heading renders as h3 when specified", async () => {
38
+ render(
39
+ <Prose>
40
+ <ProseSection heading="H3 Heading" as="h3">
41
+ <p>Content</p>
42
+ </ProseSection>
43
+ </Prose>,
44
+ );
45
+
46
+ await expect
47
+ .element(page.getByRole("heading", { level: 3, name: "H3 Heading" }))
48
+ .toBeInTheDocument();
49
+ });
50
+
51
+ test("prose content is readable", async () => {
52
+ render(
53
+ <Prose>
54
+ <ProseSection heading="Readable Content">
55
+ <p>This is some readable body text.</p>
56
+ </ProseSection>
57
+ </Prose>,
58
+ );
59
+
60
+ await expect
61
+ .element(page.getByText("This is some readable body text."))
62
+ .toBeInTheDocument();
63
+ });
64
+ });
65
+
66
+ describe("Props", () => {
67
+ test("Prose renders children", async () => {
68
+ render(
69
+ <Prose data-testid="prose">
70
+ <ProseSection heading="Child Section">
71
+ <p>Child content</p>
72
+ </ProseSection>
73
+ </Prose>,
74
+ );
75
+
76
+ await expect.element(page.getByText("Child content")).toBeInTheDocument();
77
+ });
78
+
79
+ test("Prose supports custom className", async () => {
80
+ render(
81
+ <Prose className="custom-prose" data-testid="prose">
82
+ <ProseSection heading="Test">
83
+ <p>Content</p>
84
+ </ProseSection>
85
+ </Prose>,
86
+ );
87
+
88
+ const prose = page.getByTestId("prose");
89
+ await expect.element(prose).toHaveClass(/custom-prose/);
90
+ });
91
+
92
+ test("ProseSection supports custom className", async () => {
93
+ render(
94
+ <Prose>
95
+ <ProseSection
96
+ heading="Test"
97
+ className="custom-section"
98
+ data-testid="section"
99
+ >
100
+ <p>Content</p>
101
+ </ProseSection>
102
+ </Prose>,
103
+ );
104
+
105
+ const section = page.getByTestId("section");
106
+ await expect.element(section).toHaveClass(/custom-section/);
107
+ });
108
+ });
109
+
110
+ describe("Styling", () => {
111
+ test("Prose has max-width constraint", async () => {
112
+ render(
113
+ <Prose data-testid="prose">
114
+ <ProseSection heading="Test">
115
+ <p>Content</p>
116
+ </ProseSection>
117
+ </Prose>,
118
+ );
119
+
120
+ const prose = page.getByTestId("prose");
121
+ await expect.element(prose).toHaveClass(/max-w-\[700px\]/);
122
+ });
123
+
124
+ test("h2 heading has correct typography class", async () => {
125
+ render(
126
+ <Prose>
127
+ <ProseSection heading="H2 Test">
128
+ <p>Content</p>
129
+ </ProseSection>
130
+ </Prose>,
131
+ );
132
+
133
+ const heading = page.getByRole("heading", { name: "H2 Test" });
134
+ await expect.element(heading).toHaveClass(/typography-headline-medium/);
135
+ });
136
+
137
+ test("h3 heading has correct typography class", async () => {
138
+ render(
139
+ <Prose>
140
+ <ProseSection heading="H3 Test" as="h3">
141
+ <p>Content</p>
142
+ </ProseSection>
143
+ </Prose>,
144
+ );
145
+
146
+ const heading = page.getByRole("heading", { name: "H3 Test" });
147
+ await expect.element(heading).toHaveClass(/typography-headline-small/);
148
+ });
149
+ });
150
+
151
+ describe("Composition", () => {
152
+ test("renders multiple sections", async () => {
153
+ render(
154
+ <Prose>
155
+ <ProseSection heading="First">
156
+ <p>First content</p>
157
+ </ProseSection>
158
+ <ProseSection heading="Second">
159
+ <p>Second content</p>
160
+ </ProseSection>
161
+ <ProseSection heading="Third">
162
+ <p>Third content</p>
163
+ </ProseSection>
164
+ </Prose>,
165
+ );
166
+
167
+ await expect
168
+ .element(page.getByRole("heading", { name: "First" }))
169
+ .toBeInTheDocument();
170
+ await expect
171
+ .element(page.getByRole("heading", { name: "Second" }))
172
+ .toBeInTheDocument();
173
+ await expect
174
+ .element(page.getByRole("heading", { name: "Third" }))
175
+ .toBeInTheDocument();
176
+ });
177
+ });
178
+ });
@@ -0,0 +1,88 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export interface ProseProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ children: React.ReactNode;
6
+ }
7
+
8
+ /**
9
+ * Prose container for long-form content with proper typography and spacing.
10
+ *
11
+ * Provides responsive spacing between content blocks:
12
+ * - Desktop (1440px+): 96px gap
13
+ * - Tablet (768px+): 72px gap
14
+ * - Mobile: 56px gap
15
+ *
16
+ * Use with ProseSection components for proper content structure.
17
+ */
18
+ const Prose = React.forwardRef<HTMLDivElement, ProseProps>(
19
+ ({ className, children, ...props }, ref) => {
20
+ return (
21
+ <div
22
+ ref={ref}
23
+ className={cn(
24
+ "flex w-full max-w-[700px] flex-col overflow-hidden",
25
+ // Responsive gap between sections
26
+ "gap-spacing-56 md:gap-spacing-72 xl:gap-spacing-96",
27
+ className,
28
+ )}
29
+ {...props}
30
+ >
31
+ {children}
32
+ </div>
33
+ );
34
+ },
35
+ );
36
+ Prose.displayName = "Prose";
37
+
38
+ export interface ProseSectionProps extends React.HTMLAttributes<HTMLElement> {
39
+ /**
40
+ * The heading text for this section
41
+ */
42
+ heading: string;
43
+ /**
44
+ * The heading level to render (h2 or h3)
45
+ * @default "h2"
46
+ */
47
+ as?: "h2" | "h3";
48
+ children: React.ReactNode;
49
+ }
50
+
51
+ /**
52
+ * A section within Prose content, containing a heading and body text.
53
+ *
54
+ * Responsive typography:
55
+ * - h2: Uses typography-headline-medium (42px mobile → 56px tablet → 72px desktop)
56
+ * - h3: Uses typography-headline-small (32px mobile → 42px tablet → 56px desktop)
57
+ * - Body: Uses typography-body-medium (16px mobile/tablet → 18px desktop)
58
+ */
59
+ const ProseSection = React.forwardRef<HTMLElement, ProseSectionProps>(
60
+ ({ className, heading, as = "h2", children, ...props }, ref) => {
61
+ const Heading = as;
62
+ const headingClass =
63
+ as === "h2" ? "typography-headline-medium" : "typography-headline-small";
64
+
65
+ return (
66
+ <section
67
+ ref={ref}
68
+ className={cn(
69
+ "flex w-full flex-col",
70
+ // Responsive gap between heading and body
71
+ "gap-spacing-24 md:gap-spacing-40",
72
+ className,
73
+ )}
74
+ {...props}
75
+ >
76
+ <Heading className={cn(headingClass, "text-gray-900")}>
77
+ {heading}
78
+ </Heading>
79
+ <div className="typography-body-medium flex flex-col gap-[1em] text-gray-800 break-words">
80
+ {children}
81
+ </div>
82
+ </section>
83
+ );
84
+ },
85
+ );
86
+ ProseSection.displayName = "ProseSection";
87
+
88
+ export { Prose, ProseSection };
@@ -0,0 +1,105 @@
1
+ import { render } from "@testing-library/react";
2
+ import { describe, expect, test } from "vitest";
3
+ import { page } from "vitest/browser";
4
+ import { Prose, ProseSection } from "./prose";
5
+
6
+ describe("Prose Visual Regression", () => {
7
+ test("prose with single section renders correctly", async () => {
8
+ render(
9
+ <div
10
+ style={{ width: "800px", backgroundColor: "#ffffff", padding: "40px" }}
11
+ >
12
+ <Prose data-testid="prose">
13
+ <ProseSection heading="Section Heading">
14
+ <p>
15
+ This is a paragraph of body text that demonstrates the prose
16
+ component's typography and spacing. It should be easy to read and
17
+ properly formatted.
18
+ </p>
19
+ </ProseSection>
20
+ </Prose>
21
+ </div>,
22
+ );
23
+
24
+ await expect(page.getByTestId("prose")).toMatchScreenshot(
25
+ "prose-single-section",
26
+ );
27
+ });
28
+
29
+ test("prose with multiple sections renders correctly", async () => {
30
+ render(
31
+ <div
32
+ style={{ width: "800px", backgroundColor: "#ffffff", padding: "40px" }}
33
+ >
34
+ <Prose data-testid="prose">
35
+ <ProseSection heading="First Section">
36
+ <p>
37
+ This is the first section of content with some body text to
38
+ demonstrate spacing between sections.
39
+ </p>
40
+ </ProseSection>
41
+ <ProseSection heading="Second Section">
42
+ <p>
43
+ This is the second section showing how multiple sections stack
44
+ with proper gaps between them.
45
+ </p>
46
+ </ProseSection>
47
+ </Prose>
48
+ </div>,
49
+ );
50
+
51
+ await expect(page.getByTestId("prose")).toMatchScreenshot(
52
+ "prose-multiple-sections",
53
+ );
54
+ });
55
+
56
+ test("prose with h3 heading renders correctly", async () => {
57
+ render(
58
+ <div
59
+ style={{ width: "800px", backgroundColor: "#ffffff", padding: "40px" }}
60
+ >
61
+ <Prose data-testid="prose">
62
+ <ProseSection heading="H3 Heading" as="h3">
63
+ <p>
64
+ This section uses an h3 heading which has smaller typography than
65
+ the default h2.
66
+ </p>
67
+ </ProseSection>
68
+ </Prose>
69
+ </div>,
70
+ );
71
+
72
+ await expect(page.getByTestId("prose")).toMatchScreenshot(
73
+ "prose-h3-heading",
74
+ );
75
+ });
76
+
77
+ test("prose with multiple paragraphs renders correctly", async () => {
78
+ render(
79
+ <div
80
+ style={{ width: "800px", backgroundColor: "#ffffff", padding: "40px" }}
81
+ >
82
+ <Prose data-testid="prose">
83
+ <ProseSection heading="Long Content">
84
+ <p>
85
+ First paragraph with some content to show how multiple paragraphs
86
+ are spaced within a prose section.
87
+ </p>
88
+ <p>
89
+ Second paragraph demonstrating the gap between paragraphs in the
90
+ prose component.
91
+ </p>
92
+ <p>
93
+ Third paragraph to show consistent spacing throughout the content
94
+ area.
95
+ </p>
96
+ </ProseSection>
97
+ </Prose>
98
+ </div>,
99
+ );
100
+
101
+ await expect(page.getByTestId("prose")).toMatchScreenshot(
102
+ "prose-multiple-paragraphs",
103
+ );
104
+ });
105
+ });
@@ -0,0 +1 @@
1
+ export { River, type RiverProps, riverVariants } from "./river";