@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,41 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import styles from './FAQ.module.css'
|
|
4
|
+
|
|
5
|
+
export interface FAQItem {
|
|
6
|
+
question: string
|
|
7
|
+
answer: ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FAQProps {
|
|
11
|
+
headline?: string
|
|
12
|
+
items: FAQItem[]
|
|
13
|
+
/** If true, the first item is open by default. */
|
|
14
|
+
defaultOpenFirst?: boolean
|
|
15
|
+
className?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function FAQ({ headline, items, defaultOpenFirst = false, className }: FAQProps) {
|
|
19
|
+
return (
|
|
20
|
+
<section className={clsx(styles.faq, className)}>
|
|
21
|
+
<div className={styles.container}>
|
|
22
|
+
{headline ? <h2 className={styles.headline}>{headline}</h2> : null}
|
|
23
|
+
<ul className={styles.list}>
|
|
24
|
+
{items.map((item, index) => (
|
|
25
|
+
<li key={item.question}>
|
|
26
|
+
<details className={styles.item} open={defaultOpenFirst && index === 0}>
|
|
27
|
+
<summary className={styles.question}>
|
|
28
|
+
<span>{item.question}</span>
|
|
29
|
+
<span aria-hidden="true" className={styles.chevron}>
|
|
30
|
+
+
|
|
31
|
+
</span>
|
|
32
|
+
</summary>
|
|
33
|
+
<div className={styles.answer}>{item.answer}</div>
|
|
34
|
+
</details>
|
|
35
|
+
</li>
|
|
36
|
+
))}
|
|
37
|
+
</ul>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { ComponentContract } from '@forumone/throughline-design-contract'
|
|
2
|
+
|
|
3
|
+
export const contract: ComponentContract = {
|
|
4
|
+
name: 'Hero',
|
|
5
|
+
category: 'hero',
|
|
6
|
+
description:
|
|
7
|
+
'A page opener with prominent headline, supporting copy, and primary call to action.',
|
|
8
|
+
intent:
|
|
9
|
+
'Establish what a page is about within the first viewport. Use when the page introduces a new topic, program, or initiative that deserves prominent editorial framing. Typically appears once at the top of a page.',
|
|
10
|
+
|
|
11
|
+
composition: {
|
|
12
|
+
placement: ['page'],
|
|
13
|
+
maxPerPage: 1,
|
|
14
|
+
requiredSiblings: [],
|
|
15
|
+
forbiddenAdjacent: ['Hero', '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, typically the section or program name',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'headline',
|
|
29
|
+
type: 'text',
|
|
30
|
+
required: true,
|
|
31
|
+
maxLength: 80,
|
|
32
|
+
constraints: 'Sentence case; avoid all caps; keep scannable',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'body',
|
|
36
|
+
type: 'text',
|
|
37
|
+
required: false,
|
|
38
|
+
maxLength: 240,
|
|
39
|
+
constraints: 'Two to three sentences that support the headline without repeating it',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'cta',
|
|
43
|
+
type: 'group',
|
|
44
|
+
required: false,
|
|
45
|
+
of: [
|
|
46
|
+
{ name: 'label', type: 'text', required: true, maxLength: 30 },
|
|
47
|
+
{ name: 'url', type: 'link', required: true },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'secondaryCta',
|
|
52
|
+
type: 'group',
|
|
53
|
+
required: false,
|
|
54
|
+
of: [
|
|
55
|
+
{ name: 'label', type: 'text', required: true, maxLength: 30 },
|
|
56
|
+
{ name: 'url', type: 'link', required: true },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'media',
|
|
61
|
+
type: 'image',
|
|
62
|
+
required: false,
|
|
63
|
+
constraints: 'Required when variant is "split"; otherwise optional',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
variants: [
|
|
67
|
+
{
|
|
68
|
+
name: 'default',
|
|
69
|
+
description: 'Centered text, no media',
|
|
70
|
+
whenToUse: 'Editorial pages and mission statements where content carries the weight',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'compact',
|
|
74
|
+
description: 'Reduced vertical padding',
|
|
75
|
+
whenToUse: 'Subpages where the hero is contextual framing rather than primary introduction',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'split',
|
|
79
|
+
description: 'Text on one side, media on the other',
|
|
80
|
+
whenToUse: 'Program pages or anywhere a single supporting image carries meaning',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
tokens: {
|
|
86
|
+
consumes: [
|
|
87
|
+
'color.bg.primary',
|
|
88
|
+
'color.bg.secondary',
|
|
89
|
+
'color.bg.inverse',
|
|
90
|
+
'color.text.primary',
|
|
91
|
+
'color.text.secondary',
|
|
92
|
+
'color.text.inverse',
|
|
93
|
+
'color.brand.primary',
|
|
94
|
+
'color.brand.primaryHover',
|
|
95
|
+
'color.border.strong',
|
|
96
|
+
'spacing.section',
|
|
97
|
+
'spacing.container',
|
|
98
|
+
'spacing.12',
|
|
99
|
+
'spacing.8',
|
|
100
|
+
'spacing.6',
|
|
101
|
+
'spacing.4',
|
|
102
|
+
'spacing.3',
|
|
103
|
+
'font.size.5xl',
|
|
104
|
+
'font.size.4xl',
|
|
105
|
+
'font.size.xl',
|
|
106
|
+
'font.size.sm',
|
|
107
|
+
'font.weight.bold',
|
|
108
|
+
'font.weight.semibold',
|
|
109
|
+
'line.height.tight',
|
|
110
|
+
'line.height.relaxed',
|
|
111
|
+
'letter.spacing.tight',
|
|
112
|
+
'letter.spacing.wide',
|
|
113
|
+
'radius.md',
|
|
114
|
+
'radius.lg',
|
|
115
|
+
],
|
|
116
|
+
configurable: [
|
|
117
|
+
{
|
|
118
|
+
prop: 'background',
|
|
119
|
+
tokenGroup: 'color.bg',
|
|
120
|
+
allowedValues: ['primary', 'secondary', 'inverse'],
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
accessibility: {
|
|
126
|
+
keyboardSupport: ['Tab to primary CTA', 'Tab to secondary CTA'],
|
|
127
|
+
screenReaderBehavior:
|
|
128
|
+
'The headline is announced as an h1 element. The eyebrow is read as supplementary text before the headline. Decorative media is treated as presentational when the alt attribute is empty.',
|
|
129
|
+
contentWarnings: [
|
|
130
|
+
'Avoid text overlay on busy images',
|
|
131
|
+
'Ensure sufficient color contrast between headline and background',
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
examples: [
|
|
136
|
+
{
|
|
137
|
+
label: 'Default',
|
|
138
|
+
intent: 'Standard editorial page opener with CTA',
|
|
139
|
+
storyId: 'hero--default',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
label: 'Program landing page',
|
|
143
|
+
intent: 'Introduce a new fellowship program',
|
|
144
|
+
storyId: 'hero--program-landing',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
label: 'Compact subpage',
|
|
148
|
+
intent: 'Open a secondary page with light framing',
|
|
149
|
+
storyId: 'hero--compact',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
label: 'Split with media',
|
|
153
|
+
intent: 'Open a content page with supporting visual context',
|
|
154
|
+
storyId: 'hero--split',
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
|
|
158
|
+
antiExamples: [
|
|
159
|
+
{
|
|
160
|
+
label: 'Multiple heroes on one page',
|
|
161
|
+
why: 'A page should have a single primary opener; multiple heroes flatten hierarchy and confuse screen readers with multiple h1s',
|
|
162
|
+
useInstead: 'Use SectionIntro for secondary section headers',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
label: 'Hero at the bottom of a page',
|
|
166
|
+
why: 'Heroes are editorial openers, not closers',
|
|
167
|
+
useInstead: 'Use CTASection for page-bottom calls to action',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
label: 'Split variant without media',
|
|
171
|
+
why: 'The split layout allocates half the space for media; leaving it empty creates awkward whitespace',
|
|
172
|
+
useInstead: 'Use the default variant when media is not available',
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
|
|
176
|
+
behavior: {
|
|
177
|
+
fetchesData: false,
|
|
178
|
+
hasClientState: false,
|
|
179
|
+
animates: false,
|
|
180
|
+
requiresAnalytics: false,
|
|
181
|
+
},
|
|
182
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
.hero {
|
|
2
|
+
padding: var(--spacing-section) 0;
|
|
3
|
+
position: relative;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.bg-primary {
|
|
7
|
+
background: var(--color-bg-primary);
|
|
8
|
+
color: var(--color-text-primary);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.bg-secondary {
|
|
12
|
+
background: var(--color-bg-secondary);
|
|
13
|
+
color: var(--color-text-primary);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.bg-inverse {
|
|
17
|
+
background: var(--color-bg-inverse);
|
|
18
|
+
color: var(--color-text-inverse);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.container {
|
|
22
|
+
max-width: 72rem;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
padding: 0 var(--spacing-container);
|
|
25
|
+
display: grid;
|
|
26
|
+
gap: var(--spacing-12);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.split .container {
|
|
30
|
+
grid-template-columns: 1fr 1fr;
|
|
31
|
+
align-items: center;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.compact {
|
|
35
|
+
padding: var(--spacing-16) 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.content {
|
|
39
|
+
max-width: 42rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.eyebrow {
|
|
43
|
+
font-size: var(--font-size-sm);
|
|
44
|
+
font-weight: var(--font-weight-semibold);
|
|
45
|
+
text-transform: uppercase;
|
|
46
|
+
letter-spacing: var(--letter-spacing-wide);
|
|
47
|
+
color: var(--color-text-secondary);
|
|
48
|
+
margin-bottom: var(--spacing-3);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.headline {
|
|
52
|
+
font-size: var(--font-size-5xl);
|
|
53
|
+
font-weight: var(--font-weight-bold);
|
|
54
|
+
line-height: var(--line-height-tight);
|
|
55
|
+
letter-spacing: var(--letter-spacing-tight);
|
|
56
|
+
margin-bottom: var(--spacing-6);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.body {
|
|
60
|
+
font-size: var(--font-size-xl);
|
|
61
|
+
line-height: var(--line-height-relaxed);
|
|
62
|
+
color: var(--color-text-secondary);
|
|
63
|
+
margin-bottom: var(--spacing-8);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.bg-inverse .body {
|
|
67
|
+
color: var(--color-text-inverse);
|
|
68
|
+
opacity: 0.85;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.actions {
|
|
72
|
+
display: flex;
|
|
73
|
+
gap: var(--spacing-4);
|
|
74
|
+
flex-wrap: wrap;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.primaryCta,
|
|
78
|
+
.secondaryCta {
|
|
79
|
+
display: inline-flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
padding: var(--spacing-3) var(--spacing-6);
|
|
82
|
+
border-radius: var(--radius-md);
|
|
83
|
+
font-weight: var(--font-weight-semibold);
|
|
84
|
+
text-decoration: none;
|
|
85
|
+
transition: background-color 0.15s ease, border-color 0.15s ease;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.primaryCta {
|
|
89
|
+
background: var(--color-brand-primary);
|
|
90
|
+
color: var(--color-text-inverse);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.primaryCta:hover {
|
|
94
|
+
background: var(--color-brand-primary-hover);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.secondaryCta {
|
|
98
|
+
border: 1px solid var(--color-border-strong);
|
|
99
|
+
color: var(--color-text-primary);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.bg-inverse .secondaryCta {
|
|
103
|
+
border-color: var(--color-text-inverse);
|
|
104
|
+
color: var(--color-text-inverse);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.secondaryCta:hover {
|
|
108
|
+
background: var(--color-bg-secondary);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.media img {
|
|
112
|
+
width: 100%;
|
|
113
|
+
border-radius: var(--radius-lg);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@media (max-width: 768px) {
|
|
117
|
+
.split .container {
|
|
118
|
+
grid-template-columns: 1fr;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.headline {
|
|
122
|
+
font-size: var(--font-size-4xl);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Hero } from './Hero.js'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Hero> = {
|
|
5
|
+
title: 'Hero',
|
|
6
|
+
component: Hero,
|
|
7
|
+
parameters: { layout: 'fullscreen' },
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
type Story = StoryObj<typeof Hero>
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
headline: 'Build tomorrow, starting today',
|
|
17
|
+
body: 'An opinionated starting point for teams that take content seriously.',
|
|
18
|
+
cta: { label: 'Learn more', url: '#' },
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const ProgramLanding: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
eyebrow: 'New program',
|
|
25
|
+
headline: 'Fellowship for climate researchers',
|
|
26
|
+
body: 'A one-year fellowship supporting researchers at the intersection of climate and community resilience.',
|
|
27
|
+
cta: { label: 'Apply now', url: '#' },
|
|
28
|
+
secondaryCta: { label: 'Learn about eligibility', url: '#' },
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Compact: Story = {
|
|
33
|
+
args: {
|
|
34
|
+
variant: 'compact',
|
|
35
|
+
headline: 'About our work',
|
|
36
|
+
body: 'Meet the team and learn how we approach our mission.',
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Split: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
variant: 'split',
|
|
43
|
+
eyebrow: 'Case study',
|
|
44
|
+
headline: 'How we helped a foundation modernize its grant process',
|
|
45
|
+
body: 'A twelve-month engagement that reduced processing time by 60% while improving applicant experience.',
|
|
46
|
+
cta: { label: 'Read the case study', url: '#' },
|
|
47
|
+
media: { url: 'https://placehold.co/800x600', alt: 'Foundation office' },
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const Inverse: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
background: 'inverse',
|
|
54
|
+
headline: 'A darker opener',
|
|
55
|
+
body: 'Use the inverse background when a hero needs to cut against surrounding content.',
|
|
56
|
+
cta: { label: 'Get started', url: '#' },
|
|
57
|
+
},
|
|
58
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { Hero } from './Hero.js'
|
|
4
|
+
|
|
5
|
+
describe('Hero', () => {
|
|
6
|
+
it('renders the headline as h1', () => {
|
|
7
|
+
render(<Hero headline="Big news" />)
|
|
8
|
+
const heading = screen.getByRole('heading', { level: 1 })
|
|
9
|
+
expect(heading).toHaveTextContent('Big news')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('renders eyebrow, body, and both CTAs when provided', () => {
|
|
13
|
+
render(
|
|
14
|
+
<Hero
|
|
15
|
+
eyebrow="Program"
|
|
16
|
+
headline="Fellowship for climate researchers"
|
|
17
|
+
body="A one-year program."
|
|
18
|
+
cta={{ label: 'Apply', url: '/apply' }}
|
|
19
|
+
secondaryCta={{ label: 'Eligibility', url: '/eligibility' }}
|
|
20
|
+
/>,
|
|
21
|
+
)
|
|
22
|
+
expect(screen.getByText('Program')).toBeInTheDocument()
|
|
23
|
+
expect(screen.getByText('A one-year program.')).toBeInTheDocument()
|
|
24
|
+
expect(screen.getByRole('link', { name: 'Apply' })).toHaveAttribute('href', '/apply')
|
|
25
|
+
expect(screen.getByRole('link', { name: 'Eligibility' })).toHaveAttribute(
|
|
26
|
+
'href',
|
|
27
|
+
'/eligibility',
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('omits the media region unless variant is split', () => {
|
|
32
|
+
const { container, rerender } = render(
|
|
33
|
+
<Hero
|
|
34
|
+
headline="Hi"
|
|
35
|
+
media={{ url: '/img.jpg', alt: 'pic' }}
|
|
36
|
+
/>,
|
|
37
|
+
)
|
|
38
|
+
expect(container.querySelector('img')).toBeNull()
|
|
39
|
+
|
|
40
|
+
rerender(
|
|
41
|
+
<Hero
|
|
42
|
+
headline="Hi"
|
|
43
|
+
variant="split"
|
|
44
|
+
media={{ url: '/img.jpg', alt: 'pic' }}
|
|
45
|
+
/>,
|
|
46
|
+
)
|
|
47
|
+
expect(container.querySelector('img')).not.toBeNull()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import styles from './Hero.module.css'
|
|
3
|
+
|
|
4
|
+
export type HeroVariant = 'default' | 'compact' | 'split'
|
|
5
|
+
export type HeroBackground = 'primary' | 'secondary' | 'inverse'
|
|
6
|
+
|
|
7
|
+
export interface HeroProps {
|
|
8
|
+
eyebrow?: string
|
|
9
|
+
headline: string
|
|
10
|
+
body?: string
|
|
11
|
+
cta?: { label: string; url: string }
|
|
12
|
+
secondaryCta?: { label: string; url: string }
|
|
13
|
+
media?: { url: string; alt: string }
|
|
14
|
+
variant?: HeroVariant
|
|
15
|
+
background?: HeroBackground
|
|
16
|
+
className?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Hero({
|
|
20
|
+
eyebrow,
|
|
21
|
+
headline,
|
|
22
|
+
body,
|
|
23
|
+
cta,
|
|
24
|
+
secondaryCta,
|
|
25
|
+
media,
|
|
26
|
+
variant = 'default',
|
|
27
|
+
background = 'primary',
|
|
28
|
+
className,
|
|
29
|
+
}: HeroProps) {
|
|
30
|
+
return (
|
|
31
|
+
<section
|
|
32
|
+
className={clsx(
|
|
33
|
+
styles.hero,
|
|
34
|
+
styles[variant],
|
|
35
|
+
styles[`bg-${background}`],
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<div className={styles.container}>
|
|
40
|
+
<div className={styles.content}>
|
|
41
|
+
{eyebrow ? <p className={styles.eyebrow}>{eyebrow}</p> : null}
|
|
42
|
+
<h1 className={styles.headline}>{headline}</h1>
|
|
43
|
+
{body ? <p className={styles.body}>{body}</p> : null}
|
|
44
|
+
{cta || secondaryCta ? (
|
|
45
|
+
<div className={styles.actions}>
|
|
46
|
+
{cta ? (
|
|
47
|
+
<a href={cta.url} className={styles.primaryCta}>
|
|
48
|
+
{cta.label}
|
|
49
|
+
</a>
|
|
50
|
+
) : null}
|
|
51
|
+
{secondaryCta ? (
|
|
52
|
+
<a href={secondaryCta.url} className={styles.secondaryCta}>
|
|
53
|
+
{secondaryCta.label}
|
|
54
|
+
</a>
|
|
55
|
+
) : null}
|
|
56
|
+
</div>
|
|
57
|
+
) : null}
|
|
58
|
+
</div>
|
|
59
|
+
{variant === 'split' && media ? (
|
|
60
|
+
<div className={styles.media}>
|
|
61
|
+
<img src={media.url} alt={media.alt} />
|
|
62
|
+
</div>
|
|
63
|
+
) : null}
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ComponentContract } from '@forumone/throughline-design-contract'
|
|
2
|
+
|
|
3
|
+
export const contract: ComponentContract = {
|
|
4
|
+
name: 'MediaBlock',
|
|
5
|
+
category: 'media',
|
|
6
|
+
description:
|
|
7
|
+
'A full-width image or video block with optional caption and configurable aspect ratio.',
|
|
8
|
+
intent:
|
|
9
|
+
'Insert a single piece of featured media inside a content flow. Use for editorial imagery that deserves its own moment — a photograph illustrating a story, a portrait accompanying a quote, or a video introducing a program. Not a gallery; for multiple items compose a custom layout.',
|
|
10
|
+
|
|
11
|
+
composition: {
|
|
12
|
+
placement: ['section'],
|
|
13
|
+
maxPerPage: null,
|
|
14
|
+
requiredSiblings: [],
|
|
15
|
+
forbiddenAdjacent: [],
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
content: {
|
|
19
|
+
fields: [
|
|
20
|
+
{
|
|
21
|
+
name: 'media',
|
|
22
|
+
type: 'group',
|
|
23
|
+
required: true,
|
|
24
|
+
of: [
|
|
25
|
+
{ name: 'type', type: 'select', required: true },
|
|
26
|
+
{ name: 'url', type: 'link', required: true },
|
|
27
|
+
{ name: 'alt', type: 'text', required: false, maxLength: 200 },
|
|
28
|
+
{ name: 'poster', type: 'image', required: false },
|
|
29
|
+
{ name: 'ariaLabel', type: 'text', required: false, maxLength: 120 },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'caption',
|
|
34
|
+
type: 'text',
|
|
35
|
+
required: false,
|
|
36
|
+
maxLength: 200,
|
|
37
|
+
constraints: 'One sentence that adds context the image alone cannot carry',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
variants: [
|
|
41
|
+
{ name: '16:9', description: 'Cinematic widescreen', whenToUse: 'Most photographs and video' },
|
|
42
|
+
{ name: '4:3', description: 'Classic television ratio', whenToUse: 'Vintage or squared compositions' },
|
|
43
|
+
{ name: 'square', description: '1:1', whenToUse: 'Portraits or product shots' },
|
|
44
|
+
{ name: 'auto', description: 'Media\'s native ratio', whenToUse: 'Diagrams, infographics, or tall images' },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
tokens: {
|
|
49
|
+
consumes: [
|
|
50
|
+
'color.bg.tertiary',
|
|
51
|
+
'color.text.secondary',
|
|
52
|
+
'spacing.container',
|
|
53
|
+
'spacing.3',
|
|
54
|
+
'font.size.sm',
|
|
55
|
+
'radius.lg',
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
accessibility: {
|
|
60
|
+
keyboardSupport: ['Video: native play/pause/seek controls'],
|
|
61
|
+
screenReaderBehavior:
|
|
62
|
+
'Images announce their alt text. Videos announce their aria-label (or fall back to native labeling). Captions are associated with the figure semantically via <figcaption>.',
|
|
63
|
+
contentWarnings: [
|
|
64
|
+
'Always author alt text for images that carry information; use empty alt ("") for purely decorative media',
|
|
65
|
+
'Videos with spoken content must ship captions',
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
examples: [
|
|
70
|
+
{ label: 'Image with caption', intent: 'Editorial photograph', storyId: 'media-block--image' },
|
|
71
|
+
{ label: 'Square crop', intent: 'Portrait or product shot', storyId: 'media-block--square' },
|
|
72
|
+
{ label: 'Video', intent: 'Introductory video with poster', storyId: 'media-block--video' },
|
|
73
|
+
],
|
|
74
|
+
|
|
75
|
+
antiExamples: [
|
|
76
|
+
{
|
|
77
|
+
label: 'Stacking three MediaBlocks for a gallery',
|
|
78
|
+
why: 'MediaBlock is a spotlight element; stacking them creates awkward repetition without true gallery ergonomics',
|
|
79
|
+
useInstead: 'Build a dedicated gallery component in your client DS',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
label: 'Decorative image with invented alt text',
|
|
83
|
+
why: 'Screen readers read the alt text; inventing "people smiling" for decorative media wastes listener attention',
|
|
84
|
+
useInstead: 'Use alt="" for purely decorative media',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
|
|
88
|
+
behavior: {
|
|
89
|
+
fetchesData: false,
|
|
90
|
+
hasClientState: false,
|
|
91
|
+
animates: false,
|
|
92
|
+
requiresAnalytics: false,
|
|
93
|
+
},
|
|
94
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.figure {
|
|
2
|
+
margin: 0 auto;
|
|
3
|
+
max-width: 72rem;
|
|
4
|
+
padding: 0 var(--spacing-container);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.frame {
|
|
8
|
+
position: relative;
|
|
9
|
+
width: 100%;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
border-radius: var(--radius-lg);
|
|
12
|
+
background: var(--color-bg-tertiary);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.aspect16x9 {
|
|
16
|
+
aspect-ratio: 16 / 9;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.aspect4x3 {
|
|
20
|
+
aspect-ratio: 4 / 3;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.aspect-square {
|
|
24
|
+
aspect-ratio: 1 / 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.aspect-auto {
|
|
28
|
+
aspect-ratio: auto;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.media {
|
|
32
|
+
position: absolute;
|
|
33
|
+
inset: 0;
|
|
34
|
+
width: 100%;
|
|
35
|
+
height: 100%;
|
|
36
|
+
object-fit: cover;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.aspect-auto .media {
|
|
40
|
+
position: static;
|
|
41
|
+
height: auto;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.caption {
|
|
45
|
+
margin-top: var(--spacing-3);
|
|
46
|
+
font-size: var(--font-size-sm);
|
|
47
|
+
color: var(--color-text-secondary);
|
|
48
|
+
text-align: center;
|
|
49
|
+
}
|