@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
package/dist/templates/with-reference-ds/design-system/src/components/CardGrid/CardGrid.contract.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ComponentContract } from '@forumone/throughline-design-contract'
|
|
2
|
+
|
|
3
|
+
export const contract: ComponentContract = {
|
|
4
|
+
name: 'CardGrid',
|
|
5
|
+
category: 'section',
|
|
6
|
+
description:
|
|
7
|
+
'A responsive layout container for two, three, or four Cards per row. Reflows to fewer columns on narrow viewports.',
|
|
8
|
+
intent:
|
|
9
|
+
'Lay out multiple Card components inside a section. Use for program lists, blog indexes, staff directories, and any collection of similarly-shaped content. CardGrid is purely layout; it does not fetch data or filter — authors list the Cards they want.',
|
|
10
|
+
|
|
11
|
+
composition: {
|
|
12
|
+
placement: ['section'],
|
|
13
|
+
maxPerPage: null,
|
|
14
|
+
requiredSiblings: ['Card'],
|
|
15
|
+
forbiddenAdjacent: [],
|
|
16
|
+
allowedSlots: { children: ['Card'] },
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
content: {
|
|
20
|
+
fields: [
|
|
21
|
+
{
|
|
22
|
+
name: 'children',
|
|
23
|
+
type: 'array',
|
|
24
|
+
required: true,
|
|
25
|
+
constraints: 'An array of Card components',
|
|
26
|
+
of: [{ name: 'card', type: 'group', required: true }],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
variants: [
|
|
30
|
+
{ name: '2', description: 'Two columns', whenToUse: 'Lists of 2-4 prominent items' },
|
|
31
|
+
{ name: '3', description: 'Three columns', whenToUse: 'Default — works for most collections' },
|
|
32
|
+
{ name: '4', description: 'Four columns', whenToUse: 'Denser lists of 8 or more short cards' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
tokens: {
|
|
37
|
+
consumes: ['spacing.container', 'spacing.6'],
|
|
38
|
+
configurable: [
|
|
39
|
+
{ prop: 'columns', tokenGroup: 'layout', allowedValues: ['2', '3', '4'] },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
accessibility: {
|
|
44
|
+
keyboardSupport: [],
|
|
45
|
+
screenReaderBehavior:
|
|
46
|
+
'Purely visual layout. Screen readers traverse children in source order; CardGrid itself adds no ARIA.',
|
|
47
|
+
contentWarnings: [
|
|
48
|
+
'Children must be Cards; mixing other components breaks the visual rhythm',
|
|
49
|
+
'Keep all cards roughly the same content length — dramatically unequal cards look broken',
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
examples: [
|
|
54
|
+
{ label: 'Two columns', intent: 'Featured program pair', storyId: 'card-grid--two-column' },
|
|
55
|
+
{ label: 'Three columns', intent: 'Standard content index', storyId: 'card-grid--three-column' },
|
|
56
|
+
{ label: 'Four columns', intent: 'Dense staff directory', storyId: 'card-grid--four-column' },
|
|
57
|
+
],
|
|
58
|
+
|
|
59
|
+
antiExamples: [
|
|
60
|
+
{
|
|
61
|
+
label: 'Mixing Cards and non-Card children',
|
|
62
|
+
why: 'The grid assumes uniform children; mixing Stats or MediaBlocks produces broken columns',
|
|
63
|
+
useInstead: 'Group by type — one CardGrid, then the other components below',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: 'Using CardGrid with a single Card',
|
|
67
|
+
why: 'A CardGrid wrapping one Card is overhead without benefit',
|
|
68
|
+
useInstead: 'Render the Card directly',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
|
|
72
|
+
behavior: {
|
|
73
|
+
fetchesData: false,
|
|
74
|
+
hasClientState: false,
|
|
75
|
+
animates: false,
|
|
76
|
+
requiresAnalytics: false,
|
|
77
|
+
},
|
|
78
|
+
}
|
package/dist/templates/with-reference-ds/design-system/src/components/CardGrid/CardGrid.module.css
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.grid {
|
|
2
|
+
max-width: 72rem;
|
|
3
|
+
margin: 0 auto;
|
|
4
|
+
padding: 0 var(--spacing-container);
|
|
5
|
+
display: grid;
|
|
6
|
+
gap: var(--spacing-6);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.cols-2 {
|
|
10
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.cols-3 {
|
|
14
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.cols-4 {
|
|
18
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@media (max-width: 960px) {
|
|
22
|
+
.cols-3,
|
|
23
|
+
.cols-4 {
|
|
24
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@media (max-width: 600px) {
|
|
29
|
+
.cols-2,
|
|
30
|
+
.cols-3,
|
|
31
|
+
.cols-4 {
|
|
32
|
+
grid-template-columns: 1fr;
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/templates/with-reference-ds/design-system/src/components/CardGrid/CardGrid.stories.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Card } from '../Card/Card.js'
|
|
3
|
+
import { CardGrid } from './CardGrid.js'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof CardGrid> = {
|
|
6
|
+
title: 'Card Grid',
|
|
7
|
+
component: CardGrid,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
type Story = StoryObj<typeof CardGrid>
|
|
13
|
+
|
|
14
|
+
function sampleCards(count: number) {
|
|
15
|
+
return Array.from({ length: count }, (_, index) => (
|
|
16
|
+
<Card
|
|
17
|
+
key={index}
|
|
18
|
+
image={{ url: `https://placehold.co/640x360?text=${index + 1}`, alt: `Placeholder ${index + 1}` }}
|
|
19
|
+
title={`Program ${index + 1}`}
|
|
20
|
+
description="Short description of this program or piece of content."
|
|
21
|
+
link={{ label: 'Learn more', url: '#' }}
|
|
22
|
+
/>
|
|
23
|
+
))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const TwoColumn: Story = {
|
|
27
|
+
args: { columns: 2, children: sampleCards(4) },
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const ThreeColumn: Story = {
|
|
31
|
+
args: { columns: 3, children: sampleCards(6) },
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const FourColumn: Story = {
|
|
35
|
+
args: { columns: 4, children: sampleCards(8) },
|
|
36
|
+
}
|
package/dist/templates/with-reference-ds/design-system/src/components/CardGrid/CardGrid.test.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { CardGrid } from './CardGrid.js'
|
|
4
|
+
|
|
5
|
+
describe('CardGrid', () => {
|
|
6
|
+
it('renders children', () => {
|
|
7
|
+
render(
|
|
8
|
+
<CardGrid>
|
|
9
|
+
<div data-testid="a">A</div>
|
|
10
|
+
<div data-testid="b">B</div>
|
|
11
|
+
</CardGrid>,
|
|
12
|
+
)
|
|
13
|
+
expect(screen.getByTestId('a')).toBeInTheDocument()
|
|
14
|
+
expect(screen.getByTestId('b')).toBeInTheDocument()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('applies the columns modifier class', () => {
|
|
18
|
+
const { container } = render(
|
|
19
|
+
<CardGrid columns={4}>
|
|
20
|
+
<div />
|
|
21
|
+
</CardGrid>,
|
|
22
|
+
)
|
|
23
|
+
expect((container.firstChild as HTMLElement).className).toContain('cols-4')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import styles from './CardGrid.module.css'
|
|
4
|
+
|
|
5
|
+
export type CardGridColumns = 2 | 3 | 4
|
|
6
|
+
|
|
7
|
+
export interface CardGridProps {
|
|
8
|
+
children: ReactNode
|
|
9
|
+
columns?: CardGridColumns
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CardGrid({ children, columns = 3, className }: CardGridProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div className={clsx(styles.grid, styles[`cols-${columns}`], className)}>
|
|
16
|
+
{children}
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
package/dist/templates/with-reference-ds/design-system/src/components/Divider/Divider.contract.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ComponentContract } from '@forumone/throughline-design-contract'
|
|
2
|
+
|
|
3
|
+
export const contract: ComponentContract = {
|
|
4
|
+
name: 'Divider',
|
|
5
|
+
category: 'utility',
|
|
6
|
+
description: 'A horizontal rule for separating sections of content.',
|
|
7
|
+
intent:
|
|
8
|
+
'Mark a visual pause between two blocks of content without introducing a new section header. Use sparingly — a SectionIntro is usually a better signal of a new topic. Divider is for rhythm, not hierarchy.',
|
|
9
|
+
|
|
10
|
+
composition: {
|
|
11
|
+
placement: ['page', 'section', 'inline'],
|
|
12
|
+
maxPerPage: null,
|
|
13
|
+
requiredSiblings: [],
|
|
14
|
+
forbiddenAdjacent: ['Divider'],
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
content: {
|
|
18
|
+
fields: [
|
|
19
|
+
{
|
|
20
|
+
name: 'spacing',
|
|
21
|
+
type: 'select',
|
|
22
|
+
required: false,
|
|
23
|
+
constraints: 'compact | default | spacious',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'decorative',
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
required: false,
|
|
29
|
+
constraints: 'When true (default), the divider is aria-hidden',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
variants: [
|
|
33
|
+
{ name: 'compact', description: 'Small vertical margin', whenToUse: 'Between tight pieces of content inside a list' },
|
|
34
|
+
{ name: 'default', description: 'Standard vertical margin', whenToUse: 'Most cases' },
|
|
35
|
+
{ name: 'spacious', description: 'Large vertical margin', whenToUse: 'Between major sections of a page' },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
tokens: {
|
|
40
|
+
consumes: [
|
|
41
|
+
'color.border.default',
|
|
42
|
+
'spacing.24',
|
|
43
|
+
'spacing.12',
|
|
44
|
+
'spacing.6',
|
|
45
|
+
],
|
|
46
|
+
configurable: [
|
|
47
|
+
{ prop: 'spacing', tokenGroup: 'spacing', allowedValues: ['compact', 'default', 'spacious'] },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
accessibility: {
|
|
52
|
+
role: 'separator',
|
|
53
|
+
keyboardSupport: [],
|
|
54
|
+
screenReaderBehavior:
|
|
55
|
+
'When decorative=true (default) the divider is marked aria-hidden and assistive tech skips it. When decorative=false it announces as a separator.',
|
|
56
|
+
contentWarnings: [
|
|
57
|
+
'Do not stack two Dividers in a row',
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
examples: [
|
|
62
|
+
{ label: 'Default', intent: 'Standard section break', storyId: 'divider--default' },
|
|
63
|
+
{ label: 'Compact', intent: 'Between related list items', storyId: 'divider--compact' },
|
|
64
|
+
{ label: 'Spacious', intent: 'Between major page sections', storyId: 'divider--spacious' },
|
|
65
|
+
{ label: 'Meaningful', intent: 'Semantic section break announced by screen readers', storyId: 'divider--meaningful' },
|
|
66
|
+
],
|
|
67
|
+
|
|
68
|
+
antiExamples: [
|
|
69
|
+
{
|
|
70
|
+
label: 'Using Divider instead of SectionIntro',
|
|
71
|
+
why: 'Dividers carry no semantic meaning; readers cannot tell a new section has started without the accompanying headline',
|
|
72
|
+
useInstead: 'SectionIntro',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: 'Decorative dividers announced to screen readers',
|
|
76
|
+
why: 'Adds noise without value',
|
|
77
|
+
useInstead: 'Leave decorative=true (the default)',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
|
|
81
|
+
behavior: {
|
|
82
|
+
fetchesData: false,
|
|
83
|
+
hasClientState: false,
|
|
84
|
+
animates: false,
|
|
85
|
+
requiresAnalytics: false,
|
|
86
|
+
},
|
|
87
|
+
}
|
package/dist/templates/with-reference-ds/design-system/src/components/Divider/Divider.module.css
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.divider {
|
|
2
|
+
border: 0;
|
|
3
|
+
border-top: 1px solid var(--color-border-default);
|
|
4
|
+
max-width: 72rem;
|
|
5
|
+
margin-inline: auto;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.compact {
|
|
9
|
+
margin-block: var(--spacing-6);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.default {
|
|
13
|
+
margin-block: var(--spacing-12);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.spacious {
|
|
17
|
+
margin-block: var(--spacing-24);
|
|
18
|
+
}
|
package/dist/templates/with-reference-ds/design-system/src/components/Divider/Divider.stories.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Divider } from './Divider.js'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Divider> = {
|
|
5
|
+
title: 'Divider',
|
|
6
|
+
component: Divider,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default meta
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof Divider>
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {
|
|
14
|
+
render: (args) => (
|
|
15
|
+
<div>
|
|
16
|
+
<p>Above the divider.</p>
|
|
17
|
+
<Divider {...args} />
|
|
18
|
+
<p>Below the divider.</p>
|
|
19
|
+
</div>
|
|
20
|
+
),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Compact: Story = { ...Default, args: { spacing: 'compact' } }
|
|
24
|
+
export const Spacious: Story = { ...Default, args: { spacing: 'spacious' } }
|
|
25
|
+
export const Meaningful: Story = {
|
|
26
|
+
...Default,
|
|
27
|
+
args: { decorative: false },
|
|
28
|
+
parameters: {
|
|
29
|
+
docs: {
|
|
30
|
+
description: {
|
|
31
|
+
story: 'Use decorative={false} when the divider marks a meaningful section break that assistive technology should announce.',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}
|
package/dist/templates/with-reference-ds/design-system/src/components/Divider/Divider.test.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { render } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { Divider } from './Divider.js'
|
|
4
|
+
|
|
5
|
+
describe('Divider', () => {
|
|
6
|
+
it('renders a hr element', () => {
|
|
7
|
+
const { container } = render(<Divider />)
|
|
8
|
+
expect(container.querySelector('hr')).not.toBeNull()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('defaults to aria-hidden for decorative use', () => {
|
|
12
|
+
const { container } = render(<Divider />)
|
|
13
|
+
expect(container.querySelector('hr')).toHaveAttribute('aria-hidden', 'true')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('removes aria-hidden when not decorative', () => {
|
|
17
|
+
const { container } = render(<Divider decorative={false} />)
|
|
18
|
+
expect(container.querySelector('hr')).not.toHaveAttribute('aria-hidden')
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import styles from './Divider.module.css'
|
|
3
|
+
|
|
4
|
+
export type DividerSpacing = 'compact' | 'default' | 'spacious'
|
|
5
|
+
|
|
6
|
+
export interface DividerProps {
|
|
7
|
+
spacing?: DividerSpacing
|
|
8
|
+
decorative?: boolean
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Divider({ spacing = 'default', decorative = true, className }: DividerProps) {
|
|
13
|
+
const ariaProps = decorative
|
|
14
|
+
? { role: 'presentation', 'aria-hidden': true as const }
|
|
15
|
+
: {}
|
|
16
|
+
return <hr {...ariaProps} className={clsx(styles.divider, styles[spacing], className)} />
|
|
17
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ComponentContract } from '@forumone/throughline-design-contract'
|
|
2
|
+
|
|
3
|
+
export const contract: ComponentContract = {
|
|
4
|
+
name: 'FAQ',
|
|
5
|
+
category: 'data',
|
|
6
|
+
description:
|
|
7
|
+
'A disclosure-style question and answer list using native <details>/<summary> for keyboard accessibility with no JavaScript.',
|
|
8
|
+
intent:
|
|
9
|
+
'Answer the predictable questions readers arrive with. Use for program applications, purchase funnels, onboarding pages — anywhere the same three to ten questions come up repeatedly. Not for searchable knowledge bases (those need their own search UI).',
|
|
10
|
+
|
|
11
|
+
composition: {
|
|
12
|
+
placement: ['section'],
|
|
13
|
+
maxPerPage: 2,
|
|
14
|
+
requiredSiblings: [],
|
|
15
|
+
forbiddenAdjacent: [],
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
content: {
|
|
19
|
+
fields: [
|
|
20
|
+
{
|
|
21
|
+
name: 'headline',
|
|
22
|
+
type: 'text',
|
|
23
|
+
required: false,
|
|
24
|
+
maxLength: 100,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'items',
|
|
28
|
+
type: 'array',
|
|
29
|
+
required: true,
|
|
30
|
+
constraints: 'Three to ten items; more than that needs its own dedicated page',
|
|
31
|
+
of: [
|
|
32
|
+
{ name: 'question', type: 'text', required: true, maxLength: 200 },
|
|
33
|
+
{ name: 'answer', type: 'richtext', required: true },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'defaultOpenFirst',
|
|
38
|
+
type: 'boolean',
|
|
39
|
+
required: false,
|
|
40
|
+
constraints: 'Set true when the first question is one readers always ask',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
tokens: {
|
|
46
|
+
consumes: [
|
|
47
|
+
'color.bg.primary',
|
|
48
|
+
'color.text.primary',
|
|
49
|
+
'color.text.secondary',
|
|
50
|
+
'color.border.default',
|
|
51
|
+
'color.border.strong',
|
|
52
|
+
'spacing.16',
|
|
53
|
+
'spacing.8',
|
|
54
|
+
'spacing.5',
|
|
55
|
+
'spacing.4',
|
|
56
|
+
'spacing.2',
|
|
57
|
+
'spacing.container',
|
|
58
|
+
'font.size.3xl',
|
|
59
|
+
'font.size.xl',
|
|
60
|
+
'font.weight.semibold',
|
|
61
|
+
'line.height.relaxed',
|
|
62
|
+
'radius.md',
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
accessibility: {
|
|
67
|
+
keyboardSupport: [
|
|
68
|
+
'Tab to each question summary',
|
|
69
|
+
'Enter or Space opens/closes the current item',
|
|
70
|
+
],
|
|
71
|
+
screenReaderBehavior:
|
|
72
|
+
'Each question is a <summary> inside <details>, so screen readers announce open/closed state natively. The answer becomes available to assistive tech when the item is expanded.',
|
|
73
|
+
contentWarnings: [
|
|
74
|
+
'Keep answers short and scannable — a paragraph at most',
|
|
75
|
+
'Do not nest FAQs inside FAQs',
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
examples: [
|
|
80
|
+
{ label: 'Default', intent: 'Program application FAQ', storyId: 'faq--default' },
|
|
81
|
+
{ label: 'First item open', intent: 'Lead with the most common question already answered', storyId: 'faq--open-first' },
|
|
82
|
+
{ label: 'Without headline', intent: 'Inline FAQ under a larger section', storyId: 'faq--no-headline' },
|
|
83
|
+
],
|
|
84
|
+
|
|
85
|
+
antiExamples: [
|
|
86
|
+
{
|
|
87
|
+
label: 'Twenty-question FAQ',
|
|
88
|
+
why: 'Past about ten items the disclosure pattern breaks down; readers cannot scan the list',
|
|
89
|
+
useInstead: 'Split into topic-specific FAQs across multiple pages, or build a searchable knowledge base',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
label: 'Using FAQ to hide primary content',
|
|
93
|
+
why: 'Important content should be visible by default, not hidden behind a click',
|
|
94
|
+
useInstead: 'Prose or a proper section with a SectionIntro',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
|
|
98
|
+
behavior: {
|
|
99
|
+
fetchesData: false,
|
|
100
|
+
hasClientState: true,
|
|
101
|
+
animates: false,
|
|
102
|
+
requiresAnalytics: false,
|
|
103
|
+
},
|
|
104
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
.faq {
|
|
2
|
+
padding: var(--spacing-16) 0;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
max-width: 48rem;
|
|
7
|
+
margin: 0 auto;
|
|
8
|
+
padding: 0 var(--spacing-container);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.headline {
|
|
12
|
+
font-size: var(--font-size-3xl);
|
|
13
|
+
font-weight: var(--font-weight-semibold);
|
|
14
|
+
margin-bottom: var(--spacing-8);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.list {
|
|
18
|
+
list-style: none;
|
|
19
|
+
padding: 0;
|
|
20
|
+
margin: 0;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
gap: var(--spacing-2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.item {
|
|
27
|
+
border: 1px solid var(--color-border-default);
|
|
28
|
+
border-radius: var(--radius-md);
|
|
29
|
+
background: var(--color-bg-primary);
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.item[open] {
|
|
34
|
+
border-color: var(--color-border-strong);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.question {
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
list-style: none;
|
|
40
|
+
padding: var(--spacing-4) var(--spacing-5);
|
|
41
|
+
font-weight: var(--font-weight-semibold);
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: space-between;
|
|
45
|
+
gap: var(--spacing-4);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.question::-webkit-details-marker {
|
|
49
|
+
display: none;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.chevron {
|
|
53
|
+
color: var(--color-text-secondary);
|
|
54
|
+
font-size: var(--font-size-xl);
|
|
55
|
+
transition: transform 0.15s ease;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.item[open] .chevron {
|
|
59
|
+
transform: rotate(45deg);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.answer {
|
|
63
|
+
padding: 0 var(--spacing-5) var(--spacing-5);
|
|
64
|
+
color: var(--color-text-secondary);
|
|
65
|
+
line-height: var(--line-height-relaxed);
|
|
66
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { FAQ } from './FAQ.js'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof FAQ> = {
|
|
5
|
+
title: 'FAQ',
|
|
6
|
+
component: FAQ,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default meta
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof FAQ>
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {
|
|
14
|
+
args: {
|
|
15
|
+
headline: 'Frequently asked questions',
|
|
16
|
+
items: [
|
|
17
|
+
{
|
|
18
|
+
question: 'Who is eligible to apply?',
|
|
19
|
+
answer:
|
|
20
|
+
'Researchers affiliated with accredited institutions whose work intersects climate and community resilience.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
question: 'When is the application deadline?',
|
|
24
|
+
answer:
|
|
25
|
+
'Applications close on October 15th. Decisions are announced by December 1st of the same year.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
question: 'What funding is available?',
|
|
29
|
+
answer: 'Grants range from $25,000 to $100,000 depending on the scope of the project.',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const OpenFirst: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
defaultOpenFirst: true,
|
|
38
|
+
headline: 'Common questions',
|
|
39
|
+
items: [
|
|
40
|
+
{
|
|
41
|
+
question: 'Can I request an extension?',
|
|
42
|
+
answer: 'Yes, one 30-day extension is allowed per application cycle.',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
question: 'What does the reporting look like?',
|
|
46
|
+
answer: 'A mid-grant report at six months and a final report at twelve months.',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const NoHeadline: Story = {
|
|
53
|
+
args: {
|
|
54
|
+
items: [
|
|
55
|
+
{
|
|
56
|
+
question: 'Is my data private?',
|
|
57
|
+
answer: 'All submissions are confidential and visible only to the review committee.',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { FAQ } from './FAQ.js'
|
|
4
|
+
|
|
5
|
+
describe('FAQ', () => {
|
|
6
|
+
const items = [
|
|
7
|
+
{ question: 'Q1', answer: 'A1' },
|
|
8
|
+
{ question: 'Q2', answer: 'A2' },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
it('renders each question inside a details/summary', () => {
|
|
12
|
+
const { container } = render(<FAQ items={items} />)
|
|
13
|
+
const details = container.querySelectorAll('details')
|
|
14
|
+
expect(details).toHaveLength(2)
|
|
15
|
+
expect(screen.getByText('Q1')).toBeInTheDocument()
|
|
16
|
+
expect(screen.getByText('Q2')).toBeInTheDocument()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('opens the first item when defaultOpenFirst is true', () => {
|
|
20
|
+
const { container } = render(<FAQ items={items} defaultOpenFirst />)
|
|
21
|
+
const details = container.querySelectorAll('details')
|
|
22
|
+
expect(details[0]).toHaveAttribute('open')
|
|
23
|
+
expect(details[1]).not.toHaveAttribute('open')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('renders the headline as h2 when provided', () => {
|
|
27
|
+
render(<FAQ headline="Help" items={items} />)
|
|
28
|
+
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('Help')
|
|
29
|
+
})
|
|
30
|
+
})
|