-
Notifications
You must be signed in to change notification settings - Fork 10
chore(docs): Document spacing between components #2455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
815acce
973d68a
3cf04c3
589bd76
3946632
d693b07
1bb1214
9ade47f
9a28d17
f553543
140a489
bd02a31
5a519db
e31c067
e4b6615
f52504e
351fe72
c5f472b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <!-- @license CC0-1.0 --> | ||
|
|
||
| # Prose | ||
|
|
||
| Applies the recommended vertical spacing between the direct children of a container, so editorial content flows without hand-picked margins. | ||
|
|
||
| ## Class name | ||
|
|
||
| `ams-prose` | ||
|
|
||
| ## Guidelines | ||
|
|
||
| - Use this utility class on a container that wraps editorial content — an article, a rich-text block, or a section built from multiple components — so its children are spaced according to the [‘Space between’](/docs/docs-designer-guide-space-between--docs) recommendations. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here you mention a rich-text block. What is a rich-text block in relation to Prose? Aren't all components that can have text + various types of inline media rich-text blocks?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can consider adding an Article component that has the prose mixin applied by default. With the |
||
| - The rules cover Heading (levels 1 through 6), Paragraph (including the large variant), Blockquote, Image, Ordered List, Unordered List, Button, Call to Action Link, Standalone Link, Table, and Accordion. | ||
| Other direct children pick up the default body-to-body spacing. | ||
| - It sets `margin-block-end` on each direct child based on what follows it, using the `:has()` selector. | ||
| Browsers without `:has()` support fall back to a uniform spacing. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just as with Niels’ comment: this will be out-of-date in less than 3 months when it becomes baseline widely available. This line can then be removed. |
||
| - Content inside an Accordion panel nested under `.ams-prose` is spaced the same way, so an accordion and its surroundings share one rhythm. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accordion is mentioned in the docs, but no Story example. It would be good to have one. |
||
| - To add a single ad-hoc gap between two elements, prefer the [Margin Bottom](/docs/utilities-css-margin--docs) utility class on the first one. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let’s add a reference to the Prose docs in the Margin utility docs. |
||
| - To add even spacing between a set of siblings, prefer the [Gap](/docs/utilities-css-gap--docs) utility class on their parent. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let’s also add a reference to the Prose docs in the Gap utility docs. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| /** | ||
| * @license EUPL-1.2+ | ||
| * Copyright Gemeente Amsterdam | ||
| */ | ||
|
|
||
| $ams-button-or-cta: ".ams-button, .ams-call-to-action-link"; | ||
| $ams-heavy-body: ".ams-blockquote, .ams-image, .ams-paragraph--large, .ams-table"; | ||
|
RubenSibon marked this conversation as resolved.
|
||
| $ams-list-or-paragraph: ".ams-ordered-list, .ams-paragraph:not(.ams-paragraph--large), .ams-unordered-list"; | ||
| $ams-minor-heading: ".ams-heading--4, .ams-heading--5, .ams-heading--6"; | ||
| $ams-plain-paragraph: ".ams-paragraph:not(.ams-paragraph--large)"; | ||
|
RubenSibon marked this conversation as resolved.
|
||
|
|
||
| @mixin prose-spacing { | ||
| /* Fallback for older browsers: uniform spacing */ | ||
| > .ams-heading { | ||
| margin-block-end: var(--ams-space-s); | ||
| } | ||
|
|
||
| > :not(.ams-heading) { | ||
| margin-block-end: var(--ams-space-m); | ||
| } | ||
|
|
||
| > :last-child { | ||
| margin-block-end: 0; | ||
| } | ||
|
|
||
| /* Context-aware spacing for browsers with :has() support */ | ||
| /* stylelint-disable-next-line scss/operator-no-unspaced */ | ||
| @supports selector(:has(+ *)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the has selector is widely available soon this might be overkill, and if a browser doesn't support the selector the rule will not be applied right? |
||
| /* Reset fallback margins */ | ||
| > * { | ||
| margin-block-end: 0; | ||
| } | ||
|
|
||
| /* Body-to-body default */ | ||
| > :not(.ams-heading):has(+ :not(.ams-heading)) { | ||
| margin-block-end: var(--ams-space-m); | ||
| } | ||
|
|
||
| /* Heavy body element (Blockquote, Image, Paragraph (large), Table) on either side */ | ||
| > :is(#{$ams-heavy-body}):has(+ :not(.ams-heading)), | ||
| > :not(.ams-heading):has(+ :is(#{$ams-heavy-body})) { | ||
| margin-block-end: var(--ams-space-l); | ||
| } | ||
|
|
||
| /* Button or CTA Link followed by Button or CTA Link */ | ||
| > :is(#{$ams-button-or-cta}):has(+ :is(#{$ams-button-or-cta})) { | ||
| margin-block-end: var(--ams-space-l); | ||
| } | ||
|
|
||
| /* Paragraph (large) before Blockquote or Image needs extra air */ | ||
| > .ams-paragraph--large:has(+ :is(.ams-blockquote, .ams-image)) { | ||
| margin-block-end: var(--ams-space-xl); | ||
| } | ||
|
|
||
| /* Body content before an accordion needs a slightly larger gap */ | ||
| > :not(.ams-heading):has(+ .ams-accordion) { | ||
| margin-block-end: var(--ams-space-l); | ||
| } | ||
|
|
||
| /* Body before a minor heading (level 4-6): default */ | ||
| > :not(.ams-heading):has(+ :is(#{$ams-minor-heading})) { | ||
| margin-block-end: var(--ams-space-l); | ||
| } | ||
|
|
||
| /* List or regular Paragraph before a minor heading: tighter */ | ||
| > :is(#{$ams-list-or-paragraph}):has(+ :is(#{$ams-minor-heading})) { | ||
| margin-block-end: var(--ams-space-m); | ||
| } | ||
|
|
||
| /* Any element before a level 3 heading (subsection boundary) */ | ||
| > *:has(+ .ams-heading--3) { | ||
| margin-block-end: var(--ams-space-l); | ||
| } | ||
|
|
||
| /* Any element before a level 2 heading (section boundary) */ | ||
| > *:has(+ .ams-heading--2) { | ||
| margin-block-end: var(--ams-space-xl); | ||
| } | ||
|
|
||
| /* Heading level 3 followed by its content */ | ||
| > .ams-heading--3:has(+ :not(.ams-heading)) { | ||
| margin-block-end: var(--ams-space-xs); | ||
| } | ||
|
|
||
| /* Heading level 2 followed by its content */ | ||
| > .ams-heading--2:has(+ :not(.ams-heading)) { | ||
| margin-block-end: var(--ams-space-s); | ||
| } | ||
|
|
||
| /* Heading level 1 followed by a regular Paragraph */ | ||
| > .ams-heading--1:has(+ #{$ams-plain-paragraph}) { | ||
| margin-block-end: var(--ams-space-xs); | ||
| } | ||
|
|
||
| /* Heading level 1 followed by a Paragraph (large), typically lead or metadata */ | ||
| > .ams-heading--1:has(+ .ams-paragraph--large) { | ||
| margin-block-end: var(--ams-space-m); | ||
| } | ||
|
|
||
| /* Heading level 2 or 3 followed by another heading */ | ||
| > :is(.ams-heading--2, .ams-heading--3):has(+ .ams-heading) { | ||
| margin-block-end: var(--ams-space-m); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| .ams-prose { | ||
| @include prose-spacing; | ||
|
|
||
| .ams-accordion__panel { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see an example of this in the story and I'm curious to see what happens. I would assume the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it would be nice if this story shows all descendants that can be affected by Prose. Or if another story shows how an Accordion is affected. If showing all possible permutation in Storybook is a bit too much, you could of course add that full example to Prototypes and/or a Test story for Chromatic. Actually it might be a good idea to have snapshot tests for this specific utility class, because there are so many parts that could break at some point. |
||
| @include prose-spacing; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,11 +78,12 @@ export const parameters = { | |
| storySort: { | ||
| order: [ | ||
| 'Docs', | ||
| ['Introduction', 'Developer Guide', ['Getting Started']], | ||
| ['Introduction', ['Getting Started']], | ||
| 'Brand', | ||
|
VincentSmedinga marked this conversation as resolved.
|
||
| 'Components', | ||
| ['Buttons', 'Containers', 'Feedback', 'Forms', 'Layout', 'Media', 'Navigation', 'Text'], | ||
| 'Utilities', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having Utilities here feels wrong, because it between incrementally larger concerns: Tokens < Components < Patterns < Pages. Either:
Option 2 makes the most sense because our CSS utilities are actually CSS components in (almost?) everyway. |
||
| 'Patterns', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be a good place for other common component compositions such as form fields with inputs, labels, error messages and whatnot. |
||
| 'Pages', | ||
| ['Introduction', 'Public', ['Introduction', 'Home Page'], 'Internal', ['Introduction', 'Home Page']], | ||
| ], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| /** | ||
| * @license EUPL-1.2+ | ||
| * Copyright Gemeente Amsterdam | ||
| */ | ||
|
|
||
| import './space-between-finder.css' | ||
|
|
||
| import type { HTMLAttributes } from 'react' | ||
|
|
||
| import { clsx } from 'clsx' | ||
| import { useState } from 'react' | ||
|
|
||
| import { componentsAbove, componentsBelow, spaceBetween } from './config' | ||
|
|
||
| export const SpaceBetweenFinder = ({ className, ...restProps }: HTMLAttributes<HTMLDivElement>) => { | ||
| const [above, setAbove] = useState('') | ||
| const [below, setBelow] = useState('') | ||
|
|
||
| const key = `${above}|${below}` | ||
| const size = above && below ? spaceBetween[key] : undefined | ||
| const bothSelected = above !== '' && below !== '' | ||
|
|
||
| return ( | ||
| <div {...restProps} className={clsx('_ams-space-between-finder sb-docs sb-docs-content', className)}> | ||
|
VincentSmedinga marked this conversation as resolved.
|
||
| <div className="_ams-space-between-finder__fields"> | ||
| <div> | ||
| <label htmlFor="space-between-above">Component on top:</label> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missed opportunity to use our own components? |
||
| <select id="space-between-above" onChange={(e) => setAbove(e.target.value)} value={above}> | ||
| <option value="">Select a component</option> | ||
| {componentsAbove.map((name) => ( | ||
| <option key={name} value={name}> | ||
| {name} | ||
| </option> | ||
| ))} | ||
| </select> | ||
| </div> | ||
| <div> | ||
| <label htmlFor="space-between-below">Component below:</label> | ||
| <select id="space-between-below" onChange={(e) => setBelow(e.target.value)} value={below}> | ||
| <option value="">Select a component</option> | ||
| {componentsBelow.map((name) => ( | ||
| <option key={name} value={name}> | ||
| {name} | ||
| </option> | ||
| ))} | ||
| </select> | ||
| </div> | ||
| </div> | ||
| {bothSelected && ( | ||
| <p className="_ams-space-between-finder__result"> | ||
| {size !== undefined ? ( | ||
| <> | ||
| → Recommended space size: <code>{size}</code>. | ||
| </> | ||
|
VincentSmedinga marked this conversation as resolved.
|
||
| ) : ( | ||
| '→ We advise against this combination.' | ||
| )} | ||
| </p> | ||
| )} | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| /** | ||
| * @license EUPL-1.2+ | ||
| * Copyright Gemeente Amsterdam | ||
| */ | ||
|
|
||
| type SpaceSize = '0' | 'xs' | 's' | 'm' | 'l' | 'xl' | ||
|
|
||
| export const componentsAbove = [ | ||
| 'Heading 1', | ||
| 'Heading 2', | ||
| 'Heading 3', | ||
| 'Heading 4', | ||
| 'Blockquote', | ||
| '\u2026 List', | ||
| 'Paragraph', | ||
| 'Paragraph (large)', | ||
| 'Button / CTA Link', | ||
| 'Image', | ||
| 'Standalone Link', | ||
| 'Table', | ||
| ] | ||
|
|
||
| export const componentsBelow = [ | ||
| 'Heading 2', | ||
| 'Heading 3', | ||
| 'Heading 4', | ||
| 'Blockquote', | ||
| 'Button / CTA Link', | ||
| 'Image', | ||
| 'Standalone Link', | ||
| '\u2026 List', | ||
| 'Paragraph', | ||
| 'Paragraph (large)', | ||
| 'Table', | ||
| ] | ||
|
|
||
| export const spaceBetween: Record<string, SpaceSize> = { | ||
| '\u2026 List|\u2026 List': 'm', | ||
| '\u2026 List|Blockquote': 'l', | ||
| '\u2026 List|Button / CTA Link': 'm', | ||
| '\u2026 List|Heading 2': 'xl', | ||
| '\u2026 List|Heading 3': 'l', | ||
| '\u2026 List|Heading 4': 'm', | ||
| '\u2026 List|Image': 'l', | ||
| '\u2026 List|Paragraph': 'm', | ||
| '\u2026 List|Standalone Link': 'm', | ||
| '\u2026 List|Table': 'l', | ||
| 'Blockquote|\u2026 List': 'l', | ||
| 'Blockquote|Button / CTA Link': 'l', | ||
| 'Blockquote|Image': 'l', | ||
| 'Blockquote|Paragraph': 'l', | ||
| 'Blockquote|Standalone Link': 'l', | ||
| 'Blockquote|Table': 'l', | ||
| 'Button / CTA Link|\u2026 List': 'm', | ||
| 'Button / CTA Link|Blockquote': 'l', | ||
| 'Button / CTA Link|Button / CTA Link': 'l', | ||
| 'Button / CTA Link|Heading 2': 'xl', | ||
| 'Button / CTA Link|Heading 3': 'l', | ||
| 'Button / CTA Link|Heading 4': 'l', | ||
| 'Button / CTA Link|Image': 'l', | ||
| 'Button / CTA Link|Paragraph': 'm', | ||
| 'Button / CTA Link|Standalone Link': 'm', | ||
| 'Button / CTA Link|Table': 'l', | ||
| 'Heading 1|Heading 2': 'xl', | ||
| 'Heading 1|Paragraph': 'xs', | ||
| 'Heading 1|Paragraph (large)': 'm', | ||
| 'Heading 2|\u2026 List': 's', | ||
| 'Heading 2|Heading 3': 'm', | ||
| 'Heading 2|Paragraph': 's', | ||
| 'Heading 2|Table': 's', | ||
| 'Heading 3|\u2026 List': 'xs', | ||
| 'Heading 3|Heading 4': 'm', | ||
| 'Heading 3|Paragraph': 'xs', | ||
| 'Heading 3|Table': 'xs', | ||
| 'Heading 4|\u2026 List': '0', | ||
| 'Heading 4|Paragraph': '0', | ||
| 'Heading 4|Table': '0', | ||
| 'Image|\u2026 List': 'l', | ||
| 'Image|Blockquote': 'l', | ||
| 'Image|Button / CTA Link': 'l', | ||
| 'Image|Heading 2': 'xl', | ||
| 'Image|Heading 3': 'l', | ||
| 'Image|Heading 4': 'l', | ||
| 'Image|Image': 'l', | ||
| 'Image|Paragraph': 'l', | ||
| 'Image|Standalone Link': 'l', | ||
| 'Image|Table': 'l', | ||
| 'Paragraph (large)|\u2026 List': 'l', | ||
| 'Paragraph (large)|Blockquote': 'xl', | ||
| 'Paragraph (large)|Heading 2': 'xl', | ||
| 'Paragraph (large)|Image': 'xl', | ||
| 'Paragraph (large)|Paragraph': 'l', | ||
| 'Paragraph (large)|Standalone Link': 'l', | ||
| 'Paragraph (large)|Table': 'l', | ||
| 'Paragraph|\u2026 List': 'm', | ||
| 'Paragraph|Blockquote': 'l', | ||
| 'Paragraph|Button / CTA Link': 'm', | ||
| 'Paragraph|Heading 2': 'xl', | ||
| 'Paragraph|Heading 3': 'l', | ||
| 'Paragraph|Heading 4': 'm', | ||
| 'Paragraph|Image': 'l', | ||
| 'Paragraph|Paragraph': 'm', | ||
| 'Paragraph|Standalone Link': 'm', | ||
| 'Paragraph|Table': 'l', | ||
| 'Standalone Link|\u2026 List': 'm', | ||
| 'Standalone Link|Blockquote': 'l', | ||
| 'Standalone Link|Button / CTA Link': 'm', | ||
| 'Standalone Link|Heading 2': 'xl', | ||
| 'Standalone Link|Heading 3': 'l', | ||
| 'Standalone Link|Heading 4': 'l', | ||
| 'Standalone Link|Image': 'l', | ||
| 'Standalone Link|Paragraph': 'm', | ||
| 'Standalone Link|Standalone Link': 'm', | ||
| 'Standalone Link|Table': 'l', | ||
| 'Table|\u2026 List': 'l', | ||
| 'Table|Blockquote': 'l', | ||
| 'Table|Button / CTA Link': 'l', | ||
| 'Table|Heading 2': 'xl', | ||
| 'Table|Heading 3': 'l', | ||
| 'Table|Heading 4': 'l', | ||
| 'Table|Image': 'l', | ||
| 'Table|Paragraph': 'l', | ||
| 'Table|Standalone Link': 'l', | ||
| 'Table|Table': 'l', | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /** | ||
| * @license EUPL-1.2+ | ||
| * Copyright Gemeente Amsterdam | ||
| */ | ||
|
|
||
| ._ams-space-between-finder { | ||
| color: var(--ams-color-text); | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: var(--ams-space-m); | ||
| margin-block: var(--ams-space-m) !important; | ||
| } | ||
|
|
||
| ._ams-space-between-finder__fields { | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: var(--ams-space-m); | ||
| } | ||
|
|
||
| ._ams-space-between-finder label { | ||
| color: var(--ams-color-text); | ||
| display: block; | ||
| font-size: 1.125rem; | ||
| font-weight: var(--ams-typography-body-text-bold-font-weight); | ||
| margin-block-end: var(--ams-space-xs); | ||
| } | ||
|
|
||
| ._ams-space-between-finder select { | ||
| font-family: var(--ams-typography-font-family); | ||
| font-size: 1rem; | ||
| padding-block: var(--ams-space-xs); | ||
| padding-inline: var(--ams-space-xs); | ||
| } | ||
|
|
||
| ._ams-space-between-finder__result { | ||
| margin-block: 0 !important; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be good to point implementers to vertical spacing early in the Developer Guide. Not sure where exactly.
And would it be helpful or distracting to add a line to every single component (text, media, etc.) affected by Prose?