@forumone/create-throughline 0.2.1 → 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 +14 -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,37 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { MediaBlock } from './MediaBlock.js'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof MediaBlock> = {
|
|
5
|
+
title: 'Media Block',
|
|
6
|
+
component: MediaBlock,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default meta
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof MediaBlock>
|
|
12
|
+
|
|
13
|
+
export const Image: Story = {
|
|
14
|
+
args: {
|
|
15
|
+
media: { type: 'image', url: 'https://placehold.co/1600x900', alt: 'Foundation staff at work' },
|
|
16
|
+
caption: 'Team members gathered for the annual retreat.',
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Square: Story = {
|
|
21
|
+
args: {
|
|
22
|
+
aspect: 'square',
|
|
23
|
+
media: { type: 'image', url: 'https://placehold.co/800x800', alt: 'Portrait' },
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Video: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
media: {
|
|
30
|
+
type: 'video',
|
|
31
|
+
url: 'https://example.com/clip.mp4',
|
|
32
|
+
poster: 'https://placehold.co/1600x900',
|
|
33
|
+
ariaLabel: 'Introduction video',
|
|
34
|
+
},
|
|
35
|
+
caption: 'A three-minute introduction to the program.',
|
|
36
|
+
},
|
|
37
|
+
}
|
package/dist/templates/with-reference-ds/design-system/src/components/MediaBlock/MediaBlock.test.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { MediaBlock } from './MediaBlock.js'
|
|
4
|
+
|
|
5
|
+
describe('MediaBlock', () => {
|
|
6
|
+
it('renders an image with alt text', () => {
|
|
7
|
+
render(
|
|
8
|
+
<MediaBlock media={{ type: 'image', url: '/pic.jpg', alt: 'A picture' }} />,
|
|
9
|
+
)
|
|
10
|
+
expect(screen.getByRole('img', { name: 'A picture' })).toHaveAttribute('src', '/pic.jpg')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('renders a caption inside a figcaption when provided', () => {
|
|
14
|
+
render(
|
|
15
|
+
<MediaBlock
|
|
16
|
+
media={{ type: 'image', url: '/pic.jpg', alt: 'x' }}
|
|
17
|
+
caption="Explanatory caption"
|
|
18
|
+
/>,
|
|
19
|
+
)
|
|
20
|
+
const caption = screen.getByText('Explanatory caption')
|
|
21
|
+
expect(caption.tagName.toLowerCase()).toBe('figcaption')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('renders a video element with aria-label and poster', () => {
|
|
25
|
+
const { container } = render(
|
|
26
|
+
<MediaBlock
|
|
27
|
+
media={{ type: 'video', url: '/clip.mp4', poster: '/poster.jpg', ariaLabel: 'Intro' }}
|
|
28
|
+
/>,
|
|
29
|
+
)
|
|
30
|
+
const video = container.querySelector('video')
|
|
31
|
+
expect(video).not.toBeNull()
|
|
32
|
+
expect(video).toHaveAttribute('aria-label', 'Intro')
|
|
33
|
+
expect(video).toHaveAttribute('poster', '/poster.jpg')
|
|
34
|
+
})
|
|
35
|
+
})
|
package/dist/templates/with-reference-ds/design-system/src/components/MediaBlock/MediaBlock.tsx
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import styles from './MediaBlock.module.css'
|
|
3
|
+
|
|
4
|
+
export type MediaBlockAspect = '16:9' | '4:3' | 'square' | 'auto'
|
|
5
|
+
|
|
6
|
+
type ImageMedia = { type: 'image'; url: string; alt: string }
|
|
7
|
+
type VideoMedia = {
|
|
8
|
+
type: 'video'
|
|
9
|
+
url: string
|
|
10
|
+
poster?: string
|
|
11
|
+
/** Accessible label used when the video lacks audio/captions narrative context. */
|
|
12
|
+
ariaLabel?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type MediaBlockMedia = ImageMedia | VideoMedia
|
|
16
|
+
|
|
17
|
+
export interface MediaBlockProps {
|
|
18
|
+
media: MediaBlockMedia
|
|
19
|
+
caption?: string
|
|
20
|
+
aspect?: MediaBlockAspect
|
|
21
|
+
className?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function MediaBlock({
|
|
25
|
+
media,
|
|
26
|
+
caption,
|
|
27
|
+
aspect = '16:9',
|
|
28
|
+
className,
|
|
29
|
+
}: MediaBlockProps) {
|
|
30
|
+
const aspectKey = aspect === '16:9' ? 'aspect16x9' : aspect === '4:3' ? 'aspect4x3' : `aspect-${aspect}`
|
|
31
|
+
return (
|
|
32
|
+
<figure className={clsx(styles.figure, className)}>
|
|
33
|
+
<div className={clsx(styles.frame, styles[aspectKey])}>
|
|
34
|
+
{media.type === 'image' ? (
|
|
35
|
+
<img src={media.url} alt={media.alt} className={styles.media} />
|
|
36
|
+
) : (
|
|
37
|
+
<video
|
|
38
|
+
src={media.url}
|
|
39
|
+
poster={media.poster}
|
|
40
|
+
controls
|
|
41
|
+
aria-label={media.ariaLabel}
|
|
42
|
+
className={styles.media}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
{caption ? <figcaption className={styles.caption}>{caption}</figcaption> : null}
|
|
47
|
+
</figure>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ComponentContract } from '@forumone/throughline-design-contract'
|
|
2
|
+
|
|
3
|
+
export const contract: ComponentContract = {
|
|
4
|
+
name: 'Prose',
|
|
5
|
+
category: 'section',
|
|
6
|
+
description:
|
|
7
|
+
'A typographic container for long-form content — articles, policies, documentation, any flowing prose with headings and lists.',
|
|
8
|
+
intent:
|
|
9
|
+
'Wrap free-flowing editorial content with sensible typography defaults (heading hierarchy, paragraph rhythm, list styling, blockquote treatment). Use any time you have a block of content authored as rich text and you want it to read well without styling each element individually.',
|
|
10
|
+
|
|
11
|
+
composition: {
|
|
12
|
+
placement: ['section'],
|
|
13
|
+
maxPerPage: null,
|
|
14
|
+
requiredSiblings: [],
|
|
15
|
+
forbiddenAdjacent: [],
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
content: {
|
|
19
|
+
fields: [
|
|
20
|
+
{
|
|
21
|
+
name: 'children',
|
|
22
|
+
type: 'richtext',
|
|
23
|
+
required: true,
|
|
24
|
+
constraints:
|
|
25
|
+
'Rich text content authored in the CMS; can include h2-h4, paragraphs, lists, blockquotes, links',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
variants: [
|
|
29
|
+
{
|
|
30
|
+
name: 'default',
|
|
31
|
+
description: 'Standard reading rhythm',
|
|
32
|
+
whenToUse: 'Most articles and editorial content',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'compact',
|
|
36
|
+
description: 'Tighter spacing and line height',
|
|
37
|
+
whenToUse: 'Denser reference content, product docs, or reading-room pages',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'spacious',
|
|
41
|
+
description: 'Looser spacing and line height',
|
|
42
|
+
whenToUse: 'Long-form essays where the reader benefits from breathing room',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
tokens: {
|
|
48
|
+
consumes: [
|
|
49
|
+
'color.text.primary',
|
|
50
|
+
'color.text.secondary',
|
|
51
|
+
'color.border.strong',
|
|
52
|
+
'color.brand.primary',
|
|
53
|
+
'spacing.container',
|
|
54
|
+
'spacing.10',
|
|
55
|
+
'spacing.8',
|
|
56
|
+
'spacing.6',
|
|
57
|
+
'spacing.5',
|
|
58
|
+
'spacing.4',
|
|
59
|
+
'spacing.2',
|
|
60
|
+
'font.size.3xl',
|
|
61
|
+
'font.size.2xl',
|
|
62
|
+
'font.size.xl',
|
|
63
|
+
'font.size.lg',
|
|
64
|
+
'font.weight.semibold',
|
|
65
|
+
'line.height.snug',
|
|
66
|
+
'line.height.normal',
|
|
67
|
+
'line.height.relaxed',
|
|
68
|
+
'line.height.loose',
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
accessibility: {
|
|
73
|
+
keyboardSupport: [],
|
|
74
|
+
screenReaderBehavior:
|
|
75
|
+
'Children render as their native semantic HTML (h2, h3, p, ul, ol, blockquote, a). Prose adds no ARIA beyond what the authored content provides.',
|
|
76
|
+
contentWarnings: [
|
|
77
|
+
'Author h2 and below inside Prose; never h1 (the page already has one)',
|
|
78
|
+
'Use real list markup, not paragraphs with bullets',
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
examples: [
|
|
83
|
+
{ label: 'Default', intent: 'Standard article body', storyId: 'prose--default' },
|
|
84
|
+
{ label: 'Compact', intent: 'Reference or documentation page', storyId: 'prose--compact' },
|
|
85
|
+
{ label: 'Spacious', intent: 'Long-form essay or manifesto', storyId: 'prose--spacious' },
|
|
86
|
+
],
|
|
87
|
+
|
|
88
|
+
antiExamples: [
|
|
89
|
+
{
|
|
90
|
+
label: 'Nesting Prose inside Prose',
|
|
91
|
+
why: 'Creates duplicate typography scales and unpredictable spacing',
|
|
92
|
+
useInstead: 'Use a single Prose at the outermost level',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: 'Putting interactive components inside Prose',
|
|
96
|
+
why: 'Prose is for flowing editorial content; components like Stats or CardGrid have their own layout',
|
|
97
|
+
useInstead: 'Close the Prose, render the component, open another Prose for continuing text',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
|
|
101
|
+
behavior: {
|
|
102
|
+
fetchesData: false,
|
|
103
|
+
hasClientState: false,
|
|
104
|
+
animates: false,
|
|
105
|
+
requiresAnalytics: false,
|
|
106
|
+
},
|
|
107
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
.prose {
|
|
2
|
+
max-width: 42rem;
|
|
3
|
+
margin: 0 auto;
|
|
4
|
+
padding: 0 var(--spacing-container);
|
|
5
|
+
color: var(--color-text-primary);
|
|
6
|
+
line-height: var(--line-height-relaxed);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.prose > * + * {
|
|
10
|
+
margin-top: var(--spacing-5);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.prose h2 {
|
|
14
|
+
font-size: var(--font-size-3xl);
|
|
15
|
+
font-weight: var(--font-weight-semibold);
|
|
16
|
+
line-height: var(--line-height-snug);
|
|
17
|
+
margin-top: var(--spacing-10);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.prose h3 {
|
|
21
|
+
font-size: var(--font-size-2xl);
|
|
22
|
+
font-weight: var(--font-weight-semibold);
|
|
23
|
+
line-height: var(--line-height-snug);
|
|
24
|
+
margin-top: var(--spacing-8);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.prose h4 {
|
|
28
|
+
font-size: var(--font-size-xl);
|
|
29
|
+
font-weight: var(--font-weight-semibold);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.prose p {
|
|
33
|
+
font-size: var(--font-size-lg);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.prose a {
|
|
37
|
+
color: var(--color-brand-primary);
|
|
38
|
+
text-decoration: underline;
|
|
39
|
+
text-underline-offset: 0.15em;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.prose ul,
|
|
43
|
+
.prose ol {
|
|
44
|
+
padding-inline-start: var(--spacing-6);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.prose li + li {
|
|
48
|
+
margin-top: var(--spacing-2);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.prose blockquote {
|
|
52
|
+
border-inline-start: 3px solid var(--color-border-strong);
|
|
53
|
+
padding-inline-start: var(--spacing-4);
|
|
54
|
+
color: var(--color-text-secondary);
|
|
55
|
+
font-style: italic;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.compact {
|
|
59
|
+
line-height: var(--line-height-normal);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.compact > * + * {
|
|
63
|
+
margin-top: var(--spacing-4);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.spacious {
|
|
67
|
+
line-height: var(--line-height-loose);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.spacious > * + * {
|
|
71
|
+
margin-top: var(--spacing-6);
|
|
72
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Prose } from './Prose.js'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Prose> = {
|
|
5
|
+
title: 'Prose',
|
|
6
|
+
component: Prose,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default meta
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof Prose>
|
|
12
|
+
|
|
13
|
+
const sampleContent = (
|
|
14
|
+
<>
|
|
15
|
+
<h2>Why content-first CMSes matter</h2>
|
|
16
|
+
<p>
|
|
17
|
+
A content management system should help editorial teams describe the world accurately, not
|
|
18
|
+
force them into a grid of tiles and buckets. When the CMS models the world well, the rest of
|
|
19
|
+
the work — rendering, governance, publication — becomes tractable.
|
|
20
|
+
</p>
|
|
21
|
+
<h3>The three pillars</h3>
|
|
22
|
+
<ul>
|
|
23
|
+
<li>Content models that describe intent, not layout</li>
|
|
24
|
+
<li>Governance that encodes approval policy</li>
|
|
25
|
+
<li>Publication that's reversible and auditable</li>
|
|
26
|
+
</ul>
|
|
27
|
+
<blockquote>
|
|
28
|
+
“The best CMS is the one your editors forget they are using.”
|
|
29
|
+
</blockquote>
|
|
30
|
+
<p>
|
|
31
|
+
Everything else is detail. The reference design system exists to prove the framework can
|
|
32
|
+
ship a working, accessible, and accountable editorial experience with just a dozen components.
|
|
33
|
+
</p>
|
|
34
|
+
</>
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
export const Default: Story = { args: { children: sampleContent } }
|
|
38
|
+
export const Compact: Story = { args: { size: 'compact', children: sampleContent } }
|
|
39
|
+
export const Spacious: Story = { args: { size: 'spacious', children: sampleContent } }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { Prose } from './Prose.js'
|
|
4
|
+
|
|
5
|
+
describe('Prose', () => {
|
|
6
|
+
it('renders child headings with their native semantics', () => {
|
|
7
|
+
render(
|
|
8
|
+
<Prose>
|
|
9
|
+
<h2>Section</h2>
|
|
10
|
+
<p>Body</p>
|
|
11
|
+
</Prose>,
|
|
12
|
+
)
|
|
13
|
+
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('Section')
|
|
14
|
+
expect(screen.getByText('Body')).toBeInTheDocument()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('applies size variants', () => {
|
|
18
|
+
const { container } = render(
|
|
19
|
+
<Prose size="compact">
|
|
20
|
+
<p>One</p>
|
|
21
|
+
</Prose>,
|
|
22
|
+
)
|
|
23
|
+
const root = container.firstChild as HTMLElement
|
|
24
|
+
expect(root.className).toContain('compact')
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import styles from './Prose.module.css'
|
|
4
|
+
|
|
5
|
+
export type ProseSize = 'default' | 'compact' | 'spacious'
|
|
6
|
+
|
|
7
|
+
export interface ProseProps {
|
|
8
|
+
children: ReactNode
|
|
9
|
+
size?: ProseSize
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Prose({ children, size = 'default', className }: ProseProps) {
|
|
14
|
+
return <div className={clsx(styles.prose, styles[size], className)}>{children}</div>
|
|
15
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ComponentContract } from '@forumone/throughline-design-contract'
|
|
2
|
+
|
|
3
|
+
export const contract: ComponentContract = {
|
|
4
|
+
name: 'Quote',
|
|
5
|
+
category: 'media',
|
|
6
|
+
description:
|
|
7
|
+
'A pullquote or testimonial rendered as a <figure> with a <blockquote> and optional figcaption attribution.',
|
|
8
|
+
intent:
|
|
9
|
+
'Surface a single human-scale quotation — a testimonial, a pullquote, a foundational claim. Use sparingly; quotes lose weight when stacked. The attribution is optional because some quotes stand on their own without a named author.',
|
|
10
|
+
|
|
11
|
+
composition: {
|
|
12
|
+
placement: ['section'],
|
|
13
|
+
maxPerPage: null,
|
|
14
|
+
requiredSiblings: [],
|
|
15
|
+
forbiddenAdjacent: ['Quote'],
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
content: {
|
|
19
|
+
fields: [
|
|
20
|
+
{
|
|
21
|
+
name: 'quote',
|
|
22
|
+
type: 'text',
|
|
23
|
+
required: true,
|
|
24
|
+
maxLength: 400,
|
|
25
|
+
constraints: 'One or two sentences; longer belongs in Prose',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'attribution',
|
|
29
|
+
type: 'group',
|
|
30
|
+
required: false,
|
|
31
|
+
of: [
|
|
32
|
+
{ name: 'name', type: 'text', required: true, maxLength: 80 },
|
|
33
|
+
{ name: 'role', type: 'text', required: false, maxLength: 100 },
|
|
34
|
+
{ name: 'avatar', type: 'image', required: false },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
variants: [
|
|
39
|
+
{ name: 'default', description: 'Standard size', whenToUse: 'Most testimonials and pullquotes' },
|
|
40
|
+
{ name: 'large', description: 'Oversized display treatment', whenToUse: 'Editorial openers or mission-page quotes that carry real weight' },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
tokens: {
|
|
45
|
+
consumes: [
|
|
46
|
+
'color.text.primary',
|
|
47
|
+
'color.text.secondary',
|
|
48
|
+
'spacing.10',
|
|
49
|
+
'spacing.6',
|
|
50
|
+
'spacing.3',
|
|
51
|
+
'spacing.container',
|
|
52
|
+
'font.size.4xl',
|
|
53
|
+
'font.size.2xl',
|
|
54
|
+
'font.size.sm',
|
|
55
|
+
'font.weight.semibold',
|
|
56
|
+
'line.height.tight',
|
|
57
|
+
'line.height.snug',
|
|
58
|
+
'radius.full',
|
|
59
|
+
],
|
|
60
|
+
configurable: [
|
|
61
|
+
{ prop: 'size', tokenGroup: 'layout', allowedValues: ['default', 'large'] },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
accessibility: {
|
|
66
|
+
keyboardSupport: [],
|
|
67
|
+
screenReaderBehavior:
|
|
68
|
+
'Rendered as <figure> containing a <blockquote>; attribution is read as the figcaption. Decorative quotation marks are generated via CSS and are not in the accessible name.',
|
|
69
|
+
contentWarnings: [
|
|
70
|
+
'Do not quote someone without permission',
|
|
71
|
+
'Use their actual words; a paraphrase inside quotation marks misrepresents them',
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
examples: [
|
|
76
|
+
{ label: 'Default', intent: 'Testimonial with role', storyId: 'quote--default' },
|
|
77
|
+
{ label: 'With avatar', intent: 'Testimonial with portrait', storyId: 'quote--with-avatar' },
|
|
78
|
+
{ label: 'Large', intent: 'Editorial mission statement', storyId: 'quote--large' },
|
|
79
|
+
{ label: 'Without attribution', intent: 'Aphoristic pullquote', storyId: 'quote--without-attribution' },
|
|
80
|
+
],
|
|
81
|
+
|
|
82
|
+
antiExamples: [
|
|
83
|
+
{
|
|
84
|
+
label: 'Four quotes in a row',
|
|
85
|
+
why: 'Quote is meant to punctuate content, not fill a page',
|
|
86
|
+
useInstead: 'Pick the strongest quote; move the rest into Prose or a CardGrid of Cards',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: 'Using Quote for product blurbs',
|
|
90
|
+
why: 'Quote implies a real human author; marketing copy without a source undermines trust',
|
|
91
|
+
useInstead: 'CTASection or Prose',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
|
|
95
|
+
behavior: {
|
|
96
|
+
fetchesData: false,
|
|
97
|
+
hasClientState: false,
|
|
98
|
+
animates: false,
|
|
99
|
+
requiresAnalytics: false,
|
|
100
|
+
},
|
|
101
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
.figure {
|
|
2
|
+
max-width: 48rem;
|
|
3
|
+
margin: 0 auto;
|
|
4
|
+
padding: var(--spacing-10) var(--spacing-container);
|
|
5
|
+
text-align: center;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.quote {
|
|
9
|
+
margin: 0 0 var(--spacing-6);
|
|
10
|
+
font-style: italic;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.quote p {
|
|
14
|
+
font-size: var(--font-size-2xl);
|
|
15
|
+
line-height: var(--line-height-snug);
|
|
16
|
+
color: var(--color-text-primary);
|
|
17
|
+
margin: 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.large .quote p {
|
|
21
|
+
font-size: var(--font-size-4xl);
|
|
22
|
+
line-height: var(--line-height-tight);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.quote p::before {
|
|
26
|
+
content: '\201C';
|
|
27
|
+
margin-right: 0.1em;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.quote p::after {
|
|
31
|
+
content: '\201D';
|
|
32
|
+
margin-left: 0.1em;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.attribution {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: var(--spacing-3);
|
|
39
|
+
color: var(--color-text-secondary);
|
|
40
|
+
font-size: var(--font-size-sm);
|
|
41
|
+
font-style: normal;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.avatar {
|
|
45
|
+
width: 2.5rem;
|
|
46
|
+
height: 2.5rem;
|
|
47
|
+
border-radius: var(--radius-full);
|
|
48
|
+
object-fit: cover;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.name {
|
|
52
|
+
font-weight: var(--font-weight-semibold);
|
|
53
|
+
color: var(--color-text-primary);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.role::before {
|
|
57
|
+
content: '\00B7';
|
|
58
|
+
margin: 0 0.25em;
|
|
59
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Quote } from './Quote.js'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Quote> = {
|
|
5
|
+
title: 'Quote',
|
|
6
|
+
component: Quote,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default meta
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof Quote>
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {
|
|
14
|
+
args: {
|
|
15
|
+
quote:
|
|
16
|
+
'The best CMS is the one your editorial team forgets they are using.',
|
|
17
|
+
attribution: { name: 'Jordan Rivers', role: 'Design director' },
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const WithAvatar: Story = {
|
|
22
|
+
args: {
|
|
23
|
+
quote:
|
|
24
|
+
'After we moved to a content-first model, our publishing velocity nearly doubled without adding to the team.',
|
|
25
|
+
attribution: {
|
|
26
|
+
name: 'Maria Santos',
|
|
27
|
+
role: 'Head of content, Greenfield Foundation',
|
|
28
|
+
avatar: { url: 'https://placehold.co/80x80', alt: 'Portrait of Maria Santos' },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const Large: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
size: 'large',
|
|
36
|
+
quote: 'Editorial teams deserve tools that take their workflows seriously.',
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const WithoutAttribution: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
quote: 'Content first. Everything else follows.',
|
|
43
|
+
},
|
|
44
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { Quote } from './Quote.js'
|
|
4
|
+
|
|
5
|
+
describe('Quote', () => {
|
|
6
|
+
it('renders the quote inside a blockquote', () => {
|
|
7
|
+
const { container } = render(<Quote quote="Content first." />)
|
|
8
|
+
expect(container.querySelector('blockquote')).not.toBeNull()
|
|
9
|
+
expect(screen.getByText('Content first.')).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('renders the attribution inside a figcaption when provided', () => {
|
|
13
|
+
const { container } = render(
|
|
14
|
+
<Quote
|
|
15
|
+
quote="Words."
|
|
16
|
+
attribution={{ name: 'Ada Lovelace', role: 'Mathematician' }}
|
|
17
|
+
/>,
|
|
18
|
+
)
|
|
19
|
+
const caption = container.querySelector('figcaption')
|
|
20
|
+
expect(caption).not.toBeNull()
|
|
21
|
+
expect(caption).toHaveTextContent('Ada Lovelace')
|
|
22
|
+
expect(caption).toHaveTextContent('Mathematician')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('renders an avatar when attribution includes one', () => {
|
|
26
|
+
render(
|
|
27
|
+
<Quote
|
|
28
|
+
quote="Words."
|
|
29
|
+
attribution={{
|
|
30
|
+
name: 'Grace Hopper',
|
|
31
|
+
avatar: { url: '/avatar.jpg', alt: 'Grace Hopper portrait' },
|
|
32
|
+
}}
|
|
33
|
+
/>,
|
|
34
|
+
)
|
|
35
|
+
expect(screen.getByRole('img', { name: 'Grace Hopper portrait' })).toBeInTheDocument()
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import styles from './Quote.module.css'
|
|
3
|
+
|
|
4
|
+
export type QuoteSize = 'default' | 'large'
|
|
5
|
+
|
|
6
|
+
export interface QuoteProps {
|
|
7
|
+
quote: string
|
|
8
|
+
attribution?: {
|
|
9
|
+
name: string
|
|
10
|
+
role?: string
|
|
11
|
+
avatar?: { url: string; alt: string }
|
|
12
|
+
}
|
|
13
|
+
size?: QuoteSize
|
|
14
|
+
className?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Quote({ quote, attribution, size = 'default', className }: QuoteProps) {
|
|
18
|
+
return (
|
|
19
|
+
<figure className={clsx(styles.figure, styles[size], className)}>
|
|
20
|
+
<blockquote className={styles.quote}>
|
|
21
|
+
<p>{quote}</p>
|
|
22
|
+
</blockquote>
|
|
23
|
+
{attribution ? (
|
|
24
|
+
<figcaption className={styles.attribution}>
|
|
25
|
+
{attribution.avatar ? (
|
|
26
|
+
<img
|
|
27
|
+
src={attribution.avatar.url}
|
|
28
|
+
alt={attribution.avatar.alt}
|
|
29
|
+
className={styles.avatar}
|
|
30
|
+
/>
|
|
31
|
+
) : null}
|
|
32
|
+
<span className={styles.name}>{attribution.name}</span>
|
|
33
|
+
{attribution.role ? (
|
|
34
|
+
<span className={styles.role}>{attribution.role}</span>
|
|
35
|
+
) : null}
|
|
36
|
+
</figcaption>
|
|
37
|
+
) : null}
|
|
38
|
+
</figure>
|
|
39
|
+
)
|
|
40
|
+
}
|