@forumone/create-throughline 0.2.0 → 0.3.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/CHANGELOG.md +26 -0
- package/dist/generator.d.ts +2 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +21 -4
- package/dist/generator.js.map +1 -1
- package/dist/templates/base/apps/web/package.json.template +2 -1
- package/dist/templates/base/apps/web/src/app/api/inngest/route.ts.template +2 -2
- package/dist/templates/base/apps/web/src/payload.config.ts.template +7 -5
- package/dist/templates/base/apps/web/tsconfig.json +2 -1
- package/dist/templates/base/gitignore +15 -0
- package/dist/templates/base/package.json.template +4 -1
- package/dist/templates/base/pnpm-workspace.yaml +1 -0
- package/dist/templates/base/turbo.json +11 -0
- package/dist/templates/with-reference-ds/design-system/.storybook/main.ts +21 -0
- package/dist/templates/with-reference-ds/design-system/.storybook/preview.ts +19 -0
- package/dist/templates/with-reference-ds/design-system/README.md +33 -0
- package/dist/templates/with-reference-ds/design-system/gitignore +5 -0
- package/dist/templates/with-reference-ds/design-system/package.json.template +62 -0
- package/dist/templates/with-reference-ds/design-system/scripts/build-manifest.ts +89 -0
- package/dist/templates/with-reference-ds/design-system/scripts/build-styles.ts +28 -0
- package/dist/templates/with-reference-ds/design-system/scripts/build-tokens-css.ts +43 -0
- package/dist/templates/with-reference-ds/design-system/scripts/validate.ts +58 -0
- package/dist/templates/with-reference-ds/design-system/src/components/CTASection/CTASection.contract.ts +128 -0
- package/dist/templates/with-reference-ds/design-system/src/components/CTASection/CTASection.module.css +83 -0
- package/dist/templates/with-reference-ds/design-system/src/components/CTASection/CTASection.stories.tsx +39 -0
- package/dist/templates/with-reference-ds/design-system/src/components/CTASection/CTASection.test.tsx +27 -0
- package/dist/templates/with-reference-ds/design-system/src/components/CTASection/CTASection.tsx +41 -0
- package/dist/templates/with-reference-ds/design-system/src/components/CTASection/index.ts +2 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Card/Card.contract.ts +122 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Card/Card.module.css +64 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Card/Card.stories.tsx +45 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Card/Card.test.tsx +37 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Card/Card.tsx +39 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Card/index.ts +2 -0
- package/dist/templates/with-reference-ds/design-system/src/components/CardGrid/CardGrid.contract.ts +78 -0
- package/dist/templates/with-reference-ds/design-system/src/components/CardGrid/CardGrid.module.css +34 -0
- package/dist/templates/with-reference-ds/design-system/src/components/CardGrid/CardGrid.stories.tsx +36 -0
- package/dist/templates/with-reference-ds/design-system/src/components/CardGrid/CardGrid.test.tsx +25 -0
- package/dist/templates/with-reference-ds/design-system/src/components/CardGrid/CardGrid.tsx +19 -0
- package/dist/templates/with-reference-ds/design-system/src/components/CardGrid/index.ts +2 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Divider/Divider.contract.ts +87 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Divider/Divider.module.css +18 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Divider/Divider.stories.tsx +35 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Divider/Divider.test.tsx +20 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Divider/Divider.tsx +17 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Divider/index.ts +2 -0
- package/dist/templates/with-reference-ds/design-system/src/components/FAQ/FAQ.contract.ts +104 -0
- package/dist/templates/with-reference-ds/design-system/src/components/FAQ/FAQ.module.css +66 -0
- package/dist/templates/with-reference-ds/design-system/src/components/FAQ/FAQ.stories.tsx +61 -0
- package/dist/templates/with-reference-ds/design-system/src/components/FAQ/FAQ.test.tsx +30 -0
- package/dist/templates/with-reference-ds/design-system/src/components/FAQ/FAQ.tsx +41 -0
- package/dist/templates/with-reference-ds/design-system/src/components/FAQ/index.ts +2 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Hero/Hero.contract.ts +182 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Hero/Hero.module.css +124 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Hero/Hero.stories.tsx +58 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Hero/Hero.test.tsx +49 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Hero/Hero.tsx +67 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Hero/index.ts +2 -0
- package/dist/templates/with-reference-ds/design-system/src/components/MediaBlock/MediaBlock.contract.ts +94 -0
- package/dist/templates/with-reference-ds/design-system/src/components/MediaBlock/MediaBlock.module.css +49 -0
- package/dist/templates/with-reference-ds/design-system/src/components/MediaBlock/MediaBlock.stories.tsx +37 -0
- package/dist/templates/with-reference-ds/design-system/src/components/MediaBlock/MediaBlock.test.tsx +35 -0
- package/dist/templates/with-reference-ds/design-system/src/components/MediaBlock/MediaBlock.tsx +49 -0
- package/dist/templates/with-reference-ds/design-system/src/components/MediaBlock/index.ts +7 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Prose/Prose.contract.ts +107 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Prose/Prose.module.css +72 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Prose/Prose.stories.tsx +39 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Prose/Prose.test.tsx +26 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Prose/Prose.tsx +15 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Prose/index.ts +2 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Quote/Quote.contract.ts +101 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Quote/Quote.module.css +59 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Quote/Quote.stories.tsx +44 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Quote/Quote.test.tsx +37 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Quote/Quote.tsx +40 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Quote/index.ts +2 -0
- package/dist/templates/with-reference-ds/design-system/src/components/SectionIntro/SectionIntro.contract.ts +108 -0
- package/dist/templates/with-reference-ds/design-system/src/components/SectionIntro/SectionIntro.module.css +36 -0
- package/dist/templates/with-reference-ds/design-system/src/components/SectionIntro/SectionIntro.stories.tsx +33 -0
- package/dist/templates/with-reference-ds/design-system/src/components/SectionIntro/SectionIntro.test.tsx +28 -0
- package/dist/templates/with-reference-ds/design-system/src/components/SectionIntro/SectionIntro.tsx +28 -0
- package/dist/templates/with-reference-ds/design-system/src/components/SectionIntro/index.ts +2 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Spacer/Spacer.contract.ts +76 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Spacer/Spacer.module.css +23 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Spacer/Spacer.stories.tsx +25 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Spacer/Spacer.test.tsx +17 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Spacer/Spacer.tsx +19 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Spacer/index.ts +2 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Stats/Stats.contract.ts +103 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Stats/Stats.module.css +88 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Stats/Stats.stories.tsx +44 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Stats/Stats.test.tsx +44 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Stats/Stats.tsx +39 -0
- package/dist/templates/with-reference-ds/design-system/src/components/Stats/index.ts +2 -0
- package/dist/templates/with-reference-ds/design-system/src/css-modules.d.ts +5 -0
- package/dist/templates/with-reference-ds/design-system/src/foundations/Colors.stories.tsx +46 -0
- package/dist/templates/with-reference-ds/design-system/src/foundations/Elevation.stories.tsx +37 -0
- package/dist/templates/with-reference-ds/design-system/src/foundations/LayoutContainers.stories.tsx +86 -0
- package/dist/templates/with-reference-ds/design-system/src/foundations/Radii.stories.tsx +38 -0
- package/dist/templates/with-reference-ds/design-system/src/foundations/Spacing.stories.tsx +33 -0
- package/dist/templates/with-reference-ds/design-system/src/foundations/Typography.stories.tsx +55 -0
- package/dist/templates/with-reference-ds/design-system/src/foundations/_helpers.tsx +82 -0
- package/dist/templates/with-reference-ds/design-system/src/index.ts +14 -0
- package/dist/templates/with-reference-ds/design-system/src/styles/base.css +55 -0
- package/dist/templates/with-reference-ds/design-system/src/styles/reset.css +48 -0
- package/dist/templates/with-reference-ds/design-system/src/styles/tokens.css +116 -0
- package/dist/templates/with-reference-ds/design-system/src/tokens/colors.ts +54 -0
- package/dist/templates/with-reference-ds/design-system/src/tokens/index.ts +32 -0
- package/dist/templates/with-reference-ds/design-system/src/tokens/layout.ts +26 -0
- package/dist/templates/with-reference-ds/design-system/src/tokens/radii.ts +11 -0
- package/dist/templates/with-reference-ds/design-system/src/tokens/spacing.ts +26 -0
- package/dist/templates/with-reference-ds/design-system/src/tokens/typography.ts +32 -0
- package/dist/templates/with-reference-ds/design-system/tsconfig.json +24 -0
- package/dist/templates/with-reference-ds/design-system/vitest.config.ts +15 -0
- package/dist/templates/with-reference-ds/design-system/vitest.setup.ts +7 -0
- package/dist/templates/without-reference-ds/design-system/.storybook/main.ts +21 -0
- package/dist/templates/without-reference-ds/design-system/.storybook/preview.ts +16 -0
- package/dist/templates/without-reference-ds/design-system/README.md +20 -0
- package/dist/templates/without-reference-ds/design-system/gitignore +5 -0
- package/dist/templates/without-reference-ds/design-system/package.json.template +33 -0
- package/dist/templates/without-reference-ds/design-system/src/foundations/Welcome.stories.tsx +24 -0
- package/dist/templates/without-reference-ds/design-system/src/index.ts +9 -0
- package/dist/templates/without-reference-ds/design-system/tsconfig.json +11 -0
- package/package.json +3 -1
- package/dist/templates/with-reference-ds/packages/design-system/package.json.template +0 -18
- package/dist/templates/with-reference-ds/packages/design-system/src/index.ts +0 -6
- package/dist/templates/with-reference-ds/packages/design-system/src/manifest.ts +0 -6
- package/dist/templates/with-reference-ds/packages/design-system/tsconfig.json +0 -9
- package/dist/templates/without-reference-ds/packages/design-system/README.md +0 -19
- package/dist/templates/without-reference-ds/packages/design-system/package.json.template +0 -14
- package/dist/templates/without-reference-ds/packages/design-system/src/index.ts +0 -8
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ComponentContract } from '@forumone/throughline-design-contract'
|
|
2
|
+
|
|
3
|
+
export const contract: ComponentContract = {
|
|
4
|
+
name: 'SectionIntro',
|
|
5
|
+
category: 'section',
|
|
6
|
+
description:
|
|
7
|
+
'A section opener that establishes what the next block of content is about without competing with the page hero.',
|
|
8
|
+
intent:
|
|
9
|
+
'Introduce a new section inside a page. Use when you need an h2-level heading with optional eyebrow and supporting body copy, such as "Our approach", "Recent work", or "What we believe". Pairs with any content block below it.',
|
|
10
|
+
|
|
11
|
+
composition: {
|
|
12
|
+
placement: ['section'],
|
|
13
|
+
maxPerPage: null,
|
|
14
|
+
requiredSiblings: [],
|
|
15
|
+
forbiddenAdjacent: ['SectionIntro'],
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
content: {
|
|
19
|
+
fields: [
|
|
20
|
+
{
|
|
21
|
+
name: 'eyebrow',
|
|
22
|
+
type: 'text',
|
|
23
|
+
required: false,
|
|
24
|
+
maxLength: 40,
|
|
25
|
+
constraints: 'Short kicker label above the headline',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'headline',
|
|
29
|
+
type: 'text',
|
|
30
|
+
required: true,
|
|
31
|
+
maxLength: 100,
|
|
32
|
+
constraints: 'Sentence case; scannable; reads like a chapter title',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'body',
|
|
36
|
+
type: 'text',
|
|
37
|
+
required: false,
|
|
38
|
+
maxLength: 240,
|
|
39
|
+
constraints: 'One or two sentences that set up the section',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
variants: [
|
|
43
|
+
{
|
|
44
|
+
name: 'start',
|
|
45
|
+
description: 'Left-aligned',
|
|
46
|
+
whenToUse: 'Default — pairs with left-aligned content blocks below',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'center',
|
|
50
|
+
description: 'Center-aligned',
|
|
51
|
+
whenToUse: 'Landing pages and marketing sections where the intro anchors a centered layout',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
tokens: {
|
|
57
|
+
consumes: [
|
|
58
|
+
'color.text.primary',
|
|
59
|
+
'color.text.secondary',
|
|
60
|
+
'spacing.container',
|
|
61
|
+
'spacing.3',
|
|
62
|
+
'spacing.2',
|
|
63
|
+
'font.size.3xl',
|
|
64
|
+
'font.size.lg',
|
|
65
|
+
'font.size.sm',
|
|
66
|
+
'font.weight.semibold',
|
|
67
|
+
'line.height.snug',
|
|
68
|
+
'line.height.relaxed',
|
|
69
|
+
'letter.spacing.wide',
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
accessibility: {
|
|
74
|
+
role: 'region',
|
|
75
|
+
keyboardSupport: [],
|
|
76
|
+
screenReaderBehavior:
|
|
77
|
+
'The headline is announced as an h2 element. The eyebrow is supplementary text read before the headline. No interactive controls.',
|
|
78
|
+
contentWarnings: [
|
|
79
|
+
'Do not use SectionIntro before Hero on the same page — the page must start with a single h1',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
examples: [
|
|
84
|
+
{ label: 'Default', intent: 'Introduce a section inside a content page', storyId: 'section-intro--default' },
|
|
85
|
+
{ label: 'Centered', intent: 'Anchor a centered marketing section', storyId: 'section-intro--centered' },
|
|
86
|
+
{ label: 'Headline only', intent: 'Minimal section divider with just a heading', storyId: 'section-intro--headline-only' },
|
|
87
|
+
],
|
|
88
|
+
|
|
89
|
+
antiExamples: [
|
|
90
|
+
{
|
|
91
|
+
label: 'Using SectionIntro as the page opener',
|
|
92
|
+
why: 'SectionIntro renders h2; a page should begin with an h1 for accessibility and SEO',
|
|
93
|
+
useInstead: 'Hero',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
label: 'Stacking two SectionIntros with no content between them',
|
|
97
|
+
why: 'Creates a visual and semantic duplicate that signals nothing new',
|
|
98
|
+
useInstead: 'Merge into a single SectionIntro with a longer body',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
|
|
102
|
+
behavior: {
|
|
103
|
+
fetchesData: false,
|
|
104
|
+
hasClientState: false,
|
|
105
|
+
animates: false,
|
|
106
|
+
requiresAnalytics: false,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
.intro {
|
|
2
|
+
max-width: 48rem;
|
|
3
|
+
padding: 0 var(--spacing-container);
|
|
4
|
+
margin: 0 auto;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.start {
|
|
8
|
+
text-align: start;
|
|
9
|
+
margin-inline-start: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.center {
|
|
13
|
+
text-align: center;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.eyebrow {
|
|
17
|
+
font-size: var(--font-size-sm);
|
|
18
|
+
font-weight: var(--font-weight-semibold);
|
|
19
|
+
text-transform: uppercase;
|
|
20
|
+
letter-spacing: var(--letter-spacing-wide);
|
|
21
|
+
color: var(--color-text-secondary);
|
|
22
|
+
margin-bottom: var(--spacing-2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.headline {
|
|
26
|
+
font-size: var(--font-size-3xl);
|
|
27
|
+
font-weight: var(--font-weight-semibold);
|
|
28
|
+
line-height: var(--line-height-snug);
|
|
29
|
+
margin-bottom: var(--spacing-3);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.body {
|
|
33
|
+
font-size: var(--font-size-lg);
|
|
34
|
+
line-height: var(--line-height-relaxed);
|
|
35
|
+
color: var(--color-text-secondary);
|
|
36
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { SectionIntro } from './SectionIntro.js'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof SectionIntro> = {
|
|
5
|
+
title: 'Section Intro',
|
|
6
|
+
component: SectionIntro,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default meta
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof SectionIntro>
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {
|
|
14
|
+
args: {
|
|
15
|
+
eyebrow: 'Our approach',
|
|
16
|
+
headline: 'How we work with partners',
|
|
17
|
+
body: 'We start with the content model, then the governance, then the visual design.',
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Centered: Story = {
|
|
22
|
+
args: {
|
|
23
|
+
alignment: 'center',
|
|
24
|
+
headline: 'What we believe',
|
|
25
|
+
body: 'Editorial teams deserve tools that take their workflows seriously.',
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const HeadlineOnly: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
headline: 'Recent work',
|
|
32
|
+
},
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { SectionIntro } from './SectionIntro.js'
|
|
4
|
+
|
|
5
|
+
describe('SectionIntro', () => {
|
|
6
|
+
it('renders the headline as h2', () => {
|
|
7
|
+
render(<SectionIntro headline="Our approach" />)
|
|
8
|
+
const heading = screen.getByRole('heading', { level: 2 })
|
|
9
|
+
expect(heading).toHaveTextContent('Our approach')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('renders eyebrow and body when provided', () => {
|
|
13
|
+
render(
|
|
14
|
+
<SectionIntro
|
|
15
|
+
eyebrow="About"
|
|
16
|
+
headline="How we work"
|
|
17
|
+
body="We start with content."
|
|
18
|
+
/>,
|
|
19
|
+
)
|
|
20
|
+
expect(screen.getByText('About')).toBeInTheDocument()
|
|
21
|
+
expect(screen.getByText('We start with content.')).toBeInTheDocument()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('omits the body when not provided', () => {
|
|
25
|
+
render(<SectionIntro headline="Plain" />)
|
|
26
|
+
expect(screen.queryByText(/we start/i)).not.toBeInTheDocument()
|
|
27
|
+
})
|
|
28
|
+
})
|
package/dist/templates/with-reference-ds/design-system/src/components/SectionIntro/SectionIntro.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import styles from './SectionIntro.module.css'
|
|
3
|
+
|
|
4
|
+
export type SectionIntroAlignment = 'start' | 'center'
|
|
5
|
+
|
|
6
|
+
export interface SectionIntroProps {
|
|
7
|
+
eyebrow?: string
|
|
8
|
+
headline: string
|
|
9
|
+
body?: string
|
|
10
|
+
alignment?: SectionIntroAlignment
|
|
11
|
+
className?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function SectionIntro({
|
|
15
|
+
eyebrow,
|
|
16
|
+
headline,
|
|
17
|
+
body,
|
|
18
|
+
alignment = 'start',
|
|
19
|
+
className,
|
|
20
|
+
}: SectionIntroProps) {
|
|
21
|
+
return (
|
|
22
|
+
<header className={clsx(styles.intro, styles[alignment], className)}>
|
|
23
|
+
{eyebrow ? <p className={styles.eyebrow}>{eyebrow}</p> : null}
|
|
24
|
+
<h2 className={styles.headline}>{headline}</h2>
|
|
25
|
+
{body ? <p className={styles.body}>{body}</p> : null}
|
|
26
|
+
</header>
|
|
27
|
+
)
|
|
28
|
+
}
|
package/dist/templates/with-reference-ds/design-system/src/components/Spacer/Spacer.contract.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ComponentContract } from '@forumone/throughline-design-contract'
|
|
2
|
+
|
|
3
|
+
export const contract: ComponentContract = {
|
|
4
|
+
name: 'Spacer',
|
|
5
|
+
category: 'utility',
|
|
6
|
+
description: 'An explicit vertical spacing block that inserts a token-sized gap between adjacent components.',
|
|
7
|
+
intent:
|
|
8
|
+
'Add breathing room between two components when neither component\'s own margins produce the right rhythm. Prefer component-owned spacing when possible; reach for Spacer only when layout composition requires it, such as between Stats and a following CardGrid on a dense page.',
|
|
9
|
+
|
|
10
|
+
composition: {
|
|
11
|
+
placement: ['page', 'section'],
|
|
12
|
+
maxPerPage: null,
|
|
13
|
+
requiredSiblings: [],
|
|
14
|
+
forbiddenAdjacent: ['Spacer'],
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
content: {
|
|
18
|
+
fields: [
|
|
19
|
+
{
|
|
20
|
+
name: 'size',
|
|
21
|
+
type: 'select',
|
|
22
|
+
required: false,
|
|
23
|
+
constraints: 'xs | sm | md | lg | xl',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
variants: [
|
|
27
|
+
{ name: 'xs', description: 'Extra small', whenToUse: 'Fine-grained adjustment' },
|
|
28
|
+
{ name: 'sm', description: 'Small', whenToUse: 'Between closely related elements' },
|
|
29
|
+
{ name: 'md', description: 'Medium (default)', whenToUse: 'Most cases' },
|
|
30
|
+
{ name: 'lg', description: 'Large', whenToUse: 'Between page sections that need visual separation' },
|
|
31
|
+
{ name: 'xl', description: 'Extra large', whenToUse: 'Major visual pauses on long landing pages' },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
tokens: {
|
|
36
|
+
consumes: ['spacing.2', 'spacing.4', 'spacing.8', 'spacing.16', 'spacing.24'],
|
|
37
|
+
configurable: [
|
|
38
|
+
{ prop: 'size', tokenGroup: 'spacing', allowedValues: ['xs', 'sm', 'md', 'lg', 'xl'] },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
accessibility: {
|
|
43
|
+
keyboardSupport: [],
|
|
44
|
+
screenReaderBehavior:
|
|
45
|
+
'Spacer is a presentational div marked aria-hidden. Assistive technology skips it entirely.',
|
|
46
|
+
contentWarnings: [
|
|
47
|
+
'Do not use Spacer to replace paragraph margins inside Prose — the typography already handles that',
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
examples: [
|
|
52
|
+
{ label: 'Small', intent: 'Subtle gap', storyId: 'spacer--small' },
|
|
53
|
+
{ label: 'Medium', intent: 'Standard gap', storyId: 'spacer--medium' },
|
|
54
|
+
{ label: 'Large', intent: 'Major section break', storyId: 'spacer--large' },
|
|
55
|
+
],
|
|
56
|
+
|
|
57
|
+
antiExamples: [
|
|
58
|
+
{
|
|
59
|
+
label: 'Stacking multiple Spacers to achieve a custom size',
|
|
60
|
+
why: 'Nesting spacers bypasses the token scale and makes spacing unauditable',
|
|
61
|
+
useInstead: 'Pick the nearest size in the scale or adjust the neighboring components',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: 'Using Spacer for semantic breaks',
|
|
65
|
+
why: 'Spacer has no semantic meaning; readers cannot tell one section has ended and another begun',
|
|
66
|
+
useInstead: 'Divider (decorative=false) or SectionIntro',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
|
|
70
|
+
behavior: {
|
|
71
|
+
fetchesData: false,
|
|
72
|
+
hasClientState: false,
|
|
73
|
+
animates: false,
|
|
74
|
+
requiresAnalytics: false,
|
|
75
|
+
},
|
|
76
|
+
}
|
package/dist/templates/with-reference-ds/design-system/src/components/Spacer/Spacer.module.css
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.spacer {
|
|
2
|
+
width: 100%;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.xs {
|
|
6
|
+
height: var(--spacing-2);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.sm {
|
|
10
|
+
height: var(--spacing-4);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.md {
|
|
14
|
+
height: var(--spacing-8);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.lg {
|
|
18
|
+
height: var(--spacing-16);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.xl {
|
|
22
|
+
height: var(--spacing-24);
|
|
23
|
+
}
|
package/dist/templates/with-reference-ds/design-system/src/components/Spacer/Spacer.stories.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Spacer, type SpacerSize } from './Spacer.js'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Spacer> = {
|
|
5
|
+
title: 'Spacer',
|
|
6
|
+
component: Spacer,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default meta
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof Spacer>
|
|
12
|
+
|
|
13
|
+
function bracket(size: SpacerSize) {
|
|
14
|
+
return (
|
|
15
|
+
<div style={{ background: 'var(--color-bg-secondary)', padding: '0.5rem' }}>
|
|
16
|
+
<p>Content above (size: {size})</p>
|
|
17
|
+
<Spacer size={size} />
|
|
18
|
+
<p>Content below</p>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Small: Story = { render: () => bracket('sm'), args: { size: 'sm' } }
|
|
24
|
+
export const Medium: Story = { render: () => bracket('md'), args: { size: 'md' } }
|
|
25
|
+
export const Large: Story = { render: () => bracket('lg'), args: { size: 'lg' } }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { render } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { Spacer } from './Spacer.js'
|
|
4
|
+
|
|
5
|
+
describe('Spacer', () => {
|
|
6
|
+
it('is aria-hidden and presentational', () => {
|
|
7
|
+
const { container } = render(<Spacer />)
|
|
8
|
+
const spacer = container.firstChild as HTMLElement
|
|
9
|
+
expect(spacer).toHaveAttribute('aria-hidden', 'true')
|
|
10
|
+
expect(spacer).toHaveAttribute('role', 'presentation')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('applies the size modifier class', () => {
|
|
14
|
+
const { container } = render(<Spacer size="lg" />)
|
|
15
|
+
expect((container.firstChild as HTMLElement).className).toContain('lg')
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import styles from './Spacer.module.css'
|
|
3
|
+
|
|
4
|
+
export type SpacerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
5
|
+
|
|
6
|
+
export interface SpacerProps {
|
|
7
|
+
size?: SpacerSize
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Spacer({ size = 'md', className }: SpacerProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
aria-hidden="true"
|
|
15
|
+
role="presentation"
|
|
16
|
+
className={clsx(styles.spacer, styles[size], className)}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { ComponentContract } from '@forumone/throughline-design-contract'
|
|
2
|
+
|
|
3
|
+
export const contract: ComponentContract = {
|
|
4
|
+
name: 'Stats',
|
|
5
|
+
category: 'data',
|
|
6
|
+
description:
|
|
7
|
+
'A numerical data display with two to four stat items, each with a value, a label, and an optional description.',
|
|
8
|
+
intent:
|
|
9
|
+
'Surface a small set of headline metrics in a scannable row. Use for impact reports, program highlights, and annual summaries where numbers tell the story. Not appropriate for dense tabular data — that needs a real table component.',
|
|
10
|
+
|
|
11
|
+
composition: {
|
|
12
|
+
placement: ['section'],
|
|
13
|
+
maxPerPage: null,
|
|
14
|
+
requiredSiblings: [],
|
|
15
|
+
forbiddenAdjacent: [],
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
content: {
|
|
19
|
+
fields: [
|
|
20
|
+
{
|
|
21
|
+
name: 'eyebrow',
|
|
22
|
+
type: 'text',
|
|
23
|
+
required: false,
|
|
24
|
+
maxLength: 40,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'headline',
|
|
28
|
+
type: 'text',
|
|
29
|
+
required: false,
|
|
30
|
+
maxLength: 100,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'items',
|
|
34
|
+
type: 'array',
|
|
35
|
+
required: true,
|
|
36
|
+
constraints: 'Two to four items',
|
|
37
|
+
of: [
|
|
38
|
+
{ name: 'value', type: 'text', required: true, maxLength: 20 },
|
|
39
|
+
{ name: 'label', type: 'text', required: true, maxLength: 60 },
|
|
40
|
+
{ name: 'description', type: 'text', required: false, maxLength: 80 },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
tokens: {
|
|
47
|
+
consumes: [
|
|
48
|
+
'color.text.primary',
|
|
49
|
+
'color.text.secondary',
|
|
50
|
+
'color.brand.primary',
|
|
51
|
+
'spacing.16',
|
|
52
|
+
'spacing.10',
|
|
53
|
+
'spacing.8',
|
|
54
|
+
'spacing.3',
|
|
55
|
+
'spacing.2',
|
|
56
|
+
'spacing.1',
|
|
57
|
+
'spacing.container',
|
|
58
|
+
'font.size.5xl',
|
|
59
|
+
'font.size.3xl',
|
|
60
|
+
'font.size.sm',
|
|
61
|
+
'font.weight.bold',
|
|
62
|
+
'font.weight.semibold',
|
|
63
|
+
'line.height.tight',
|
|
64
|
+
'letter.spacing.wide',
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
accessibility: {
|
|
69
|
+
keyboardSupport: [],
|
|
70
|
+
screenReaderBehavior:
|
|
71
|
+
'Statistics are rendered as a <dl> with <dt>/<dd> pairs. Screen readers traverse each value + label + description as a definition list entry.',
|
|
72
|
+
contentWarnings: [
|
|
73
|
+
'Keep values concise — "4,200" instead of "4,200,000,000 dollars"',
|
|
74
|
+
'Never invent numbers to fill the grid; use the real count even if unequal',
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
examples: [
|
|
79
|
+
{ label: 'Three stats', intent: 'Annual report highlights', storyId: 'stats--three-stats' },
|
|
80
|
+
{ label: 'Two stats', intent: 'Minimal impact summary', storyId: 'stats--two-stats' },
|
|
81
|
+
{ label: 'Four stats', intent: 'Dense overview', storyId: 'stats--four-stats' },
|
|
82
|
+
],
|
|
83
|
+
|
|
84
|
+
antiExamples: [
|
|
85
|
+
{
|
|
86
|
+
label: 'Five or more stats',
|
|
87
|
+
why: 'Beyond four items the layout cramps on mobile and the individual numbers lose impact',
|
|
88
|
+
useInstead: 'Pick the top four; move the rest to a full report page',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
label: 'Paragraph-length values',
|
|
92
|
+
why: 'Stats are designed for at-a-glance reading; long values overwhelm the layout',
|
|
93
|
+
useInstead: 'Use Prose for extended narrative',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
|
|
97
|
+
behavior: {
|
|
98
|
+
fetchesData: false,
|
|
99
|
+
hasClientState: false,
|
|
100
|
+
animates: false,
|
|
101
|
+
requiresAnalytics: false,
|
|
102
|
+
},
|
|
103
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
.stats {
|
|
2
|
+
padding: var(--spacing-16) 0;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
max-width: 72rem;
|
|
7
|
+
margin: 0 auto;
|
|
8
|
+
padding: 0 var(--spacing-container);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.eyebrow {
|
|
12
|
+
font-size: var(--font-size-sm);
|
|
13
|
+
font-weight: var(--font-weight-semibold);
|
|
14
|
+
text-transform: uppercase;
|
|
15
|
+
letter-spacing: var(--letter-spacing-wide);
|
|
16
|
+
color: var(--color-text-secondary);
|
|
17
|
+
margin-bottom: var(--spacing-3);
|
|
18
|
+
text-align: center;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.headline {
|
|
22
|
+
font-size: var(--font-size-3xl);
|
|
23
|
+
font-weight: var(--font-weight-semibold);
|
|
24
|
+
margin-bottom: var(--spacing-10);
|
|
25
|
+
text-align: center;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.list {
|
|
29
|
+
display: grid;
|
|
30
|
+
gap: var(--spacing-8);
|
|
31
|
+
margin: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.cols-2 {
|
|
35
|
+
grid-template-columns: repeat(2, 1fr);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.cols-3 {
|
|
39
|
+
grid-template-columns: repeat(3, 1fr);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.cols-4 {
|
|
43
|
+
grid-template-columns: repeat(4, 1fr);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@media (max-width: 720px) {
|
|
47
|
+
.cols-2,
|
|
48
|
+
.cols-3,
|
|
49
|
+
.cols-4 {
|
|
50
|
+
grid-template-columns: repeat(2, 1fr);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@media (max-width: 480px) {
|
|
55
|
+
.cols-2,
|
|
56
|
+
.cols-3,
|
|
57
|
+
.cols-4 {
|
|
58
|
+
grid-template-columns: 1fr;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.item {
|
|
63
|
+
text-align: center;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.value {
|
|
67
|
+
font-size: var(--font-size-5xl);
|
|
68
|
+
font-weight: var(--font-weight-bold);
|
|
69
|
+
line-height: var(--line-height-tight);
|
|
70
|
+
color: var(--color-brand-primary);
|
|
71
|
+
margin: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.label {
|
|
75
|
+
margin: var(--spacing-2) 0 0;
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
gap: var(--spacing-1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.labelText {
|
|
82
|
+
font-weight: var(--font-weight-semibold);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.description {
|
|
86
|
+
font-size: var(--font-size-sm);
|
|
87
|
+
color: var(--color-text-secondary);
|
|
88
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Stats } from './Stats.js'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Stats> = {
|
|
5
|
+
title: 'Stats',
|
|
6
|
+
component: Stats,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default meta
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof Stats>
|
|
12
|
+
|
|
13
|
+
export const ThreeStats: Story = {
|
|
14
|
+
args: {
|
|
15
|
+
eyebrow: 'Impact',
|
|
16
|
+
headline: 'Year in review',
|
|
17
|
+
items: [
|
|
18
|
+
{ value: '128', label: 'Grants awarded', description: 'Across twelve programs' },
|
|
19
|
+
{ value: '$4.2M', label: 'Total funded' },
|
|
20
|
+
{ value: '87%', label: 'Completion rate', description: 'Measured over five years' },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const TwoStats: Story = {
|
|
26
|
+
args: {
|
|
27
|
+
items: [
|
|
28
|
+
{ value: '30', label: 'Partner organizations' },
|
|
29
|
+
{ value: '1,200', label: 'Researchers supported' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const FourStats: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
headline: 'By the numbers',
|
|
37
|
+
items: [
|
|
38
|
+
{ value: '12', label: 'Countries' },
|
|
39
|
+
{ value: '48', label: 'Programs' },
|
|
40
|
+
{ value: '300+', label: 'Publications' },
|
|
41
|
+
{ value: '5M', label: 'People reached' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { Stats } from './Stats.js'
|
|
4
|
+
|
|
5
|
+
describe('Stats', () => {
|
|
6
|
+
it('renders each item as a dt/dd pair inside a dl', () => {
|
|
7
|
+
const { container } = render(
|
|
8
|
+
<Stats
|
|
9
|
+
items={[
|
|
10
|
+
{ value: '100', label: 'Projects' },
|
|
11
|
+
{ value: '50', label: 'Partners' },
|
|
12
|
+
]}
|
|
13
|
+
/>,
|
|
14
|
+
)
|
|
15
|
+
expect(container.querySelector('dl')).not.toBeNull()
|
|
16
|
+
const dts = container.querySelectorAll('dt')
|
|
17
|
+
const dds = container.querySelectorAll('dd')
|
|
18
|
+
expect(dts).toHaveLength(2)
|
|
19
|
+
expect(dds).toHaveLength(2)
|
|
20
|
+
expect(dts[0]).toHaveTextContent('100')
|
|
21
|
+
expect(dds[0]).toHaveTextContent('Projects')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('renders optional eyebrow + headline', () => {
|
|
25
|
+
render(
|
|
26
|
+
<Stats
|
|
27
|
+
eyebrow="Impact"
|
|
28
|
+
headline="Summary"
|
|
29
|
+
items={[{ value: '1', label: 'Thing' }]}
|
|
30
|
+
/>,
|
|
31
|
+
)
|
|
32
|
+
expect(screen.getByText('Impact')).toBeInTheDocument()
|
|
33
|
+
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('Summary')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('renders item description when present', () => {
|
|
37
|
+
render(
|
|
38
|
+
<Stats
|
|
39
|
+
items={[{ value: '1', label: 'Thing', description: 'Extra context' }]}
|
|
40
|
+
/>,
|
|
41
|
+
)
|
|
42
|
+
expect(screen.getByText('Extra context')).toBeInTheDocument()
|
|
43
|
+
})
|
|
44
|
+
})
|