Skip to content

Commit e2c8043

Browse files
committed
Merge branch 'feature/motion-utilities' into develop
2 parents bce027f + 223803c commit e2c8043

122 files changed

Lines changed: 1764 additions & 178 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎bin/gen-docs.js‎

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export const GENERATED_MARK = '<!-- Generated by bin/gen-docs.js';
1818
// order here is the order the docs site shows them: arrange the page, fill it,
1919
// let people move around it, let them act, tell them what happened.
2020
const GROUPS = { layout: 'Layouts', recipe: 'Recipes', component: 'Components', utility: 'Utilities' };
21-
export const NAV_GROUPS = ['Guides', 'Page Layouts', 'Grids and Rows', 'Boxes and Stacks', 'Content', 'Navigation', 'Forms and Actions', 'Feedback', 'Reference'];
21+
// Motion comes after Feedback and before Reference: it is the last thing a
22+
// page gets, once it is arranged, filled, navigable and answering back.
23+
export const NAV_GROUPS = ['Guides', 'Page Layouts', 'Grids and Rows', 'Boxes and Stacks', 'Content', 'Navigation', 'Forms and Actions', 'Feedback', 'Motion', 'Reference'];
2224
const groupOf = (m) => m.group ?? GROUPS[m.kind];
2325
const RESERVED_PAGES = new Set(['tokens']);
2426

@@ -59,11 +61,18 @@ export function escapeAttribute(text) {
5961
// at the stylesheet's own folder, since a srcdoc frame otherwise resolves
6062
// them against the docs page.
6163
export function renderDemo({ title, exampleHtml, stylesheet, height = 'lg', width, resize }) {
62-
const base = `${path.posix.dirname(stylesheet)}/`.replace(/\/+$/, '/');
63-
// The frame is a whole Yeti page: the stylesheet and, beside it, the bundle
64-
// of every module, so a framed dialog opens, framed tabs switch, and an
65-
// example that composes components gets all of their scripts, not one.
66-
const doc = `<base href="${base}"><link rel="stylesheet" href="${stylesheet}"><script type="module" src="${base}yeti.js"></script><body style="margin:0;padding:var(--yeti-space-md)">${exampleHtml.trim()}`;
64+
// More than one stylesheet is allowed, and is how a themed host should do
65+
// it: the page has already fetched yeti.css, so naming that same URL first
66+
// and a small theme after it costs the frame one short file rather than a
67+
// second copy of the framework. The first one places the base and the
68+
// module bundle beside it.
69+
const sheets = Array.isArray(stylesheet) ? stylesheet : [stylesheet];
70+
const base = `${path.posix.dirname(sheets[0])}/`.replace(/\/+$/, '/');
71+
const links = sheets.map((href) => `<link rel="stylesheet" href="${href}">`).join('');
72+
// The frame is a whole Yeti page: the stylesheets and, beside the first,
73+
// the bundle of every module, so a framed dialog opens, framed tabs switch,
74+
// and an example that composes components gets all of their scripts.
75+
const doc = `<base href="${base}">${links}<script type="module" src="${base}yeti.js"></script><body style="margin:0;padding:var(--yeti-space-md)">${exampleHtml.trim()}`;
6776
const srcdoc = escapeAttribute(doc).replace(/\r?\n/g, '&#10;');
6877
return [
6978
`<figure class="demo" data-height="${height}"${width ? ` data-width="${width}"` : ''}${resize ? ` data-resize="${resize}"` : ''}>`,
@@ -96,7 +105,10 @@ export function renderPage({ manifest: m, exampleHtml, navOrder, docsMd = '', de
96105

97106
// trimEnd so the join below adds exactly one newline before the generated mark
98107
out.push(frontMatter({ raw: true, title, description: m.description, nav_group: groupOf(m), nav_order: navOrder }).trimEnd());
99-
out.push(`${GENERATED_MARK} from src/${dir}/${m.name}/manifest.json. Do not edit. -->`, '', `# ${title}`, '', m.description, '');
108+
// The description is the page's lede, and says so with a class rather than
109+
// by being the paragraph after the heading: a host that wraps the two in a
110+
// layout would otherwise lose it silently.
111+
out.push(`${GENERATED_MARK} from src/${dir}/${m.name}/manifest.json. Do not edit. -->`, '', `# ${title}`, '', `<p class="lede">${m.description}</p>`, '');
100112

101113
out.push('## Example', '', renderDemo({ title, exampleHtml, stylesheet: demoStylesheet, height: m.demo?.height, width: m.demo?.width, resize: m.demo?.resize }), '');
102114
if (docsMd.trim()) out.push(docsMd.trim(), '');
@@ -186,9 +198,15 @@ function countInternalTokens(tokensDir) {
186198
return names.size;
187199
}
188200

189-
export function generateDocs({ root, demoStylesheet }) {
201+
// outDir is where the pages land, and defaults to this repo's own docs/. A
202+
// host that themes Yeti passes its own: foundationcss.com generates the pages
203+
// it serves straight into its src/pages/yeti/, with demo frames pointing at
204+
// its themed, cache-stamped stylesheet. That used to be done by generating
205+
// over docs/ here and restoring it afterwards, which left the stamped pages in
206+
// this repo whenever the import did not reach its last line.
207+
export function generateDocs({ root, demoStylesheet, outDir }) {
190208
const srcDir = path.join(root, 'src');
191-
const docsDir = path.join(root, 'docs');
209+
const docsDir = outDir ? path.resolve(outDir) : path.join(root, 'docs');
192210
const schema = loadSchema(path.join(root, 'schema', 'manifest.schema.json'));
193211
const vocabFile = path.join(root, 'schema', 'vocabulary.json');
194212
const vocabulary = fs.existsSync(vocabFile) ? loadVocabulary(vocabFile) : {};
@@ -274,12 +292,17 @@ if (isMain) {
274292
// so foundationcss.com can point them at its themed build rather than the
275293
// framework's defaults. Absent, the frames load the site's plain yeti.css.
276294
const flag = process.argv.indexOf('--stylesheet');
277-
const demoStylesheet = flag === -1 ? undefined : process.argv[flag + 1];
278-
if (flag !== -1 && !demoStylesheet) {
279-
console.error('usage: node bin/gen-docs.js [--stylesheet <path>]');
295+
// Comma-separated, so a themed host can name the framework and its theme.
296+
const demoStylesheet = flag === -1 ? undefined : process.argv[flag + 1]?.split(',').map((s) => s.trim()).filter(Boolean);
297+
// Where the pages are written; docs/ when absent. A themed host generates
298+
// into its own tree rather than over this one.
299+
const outFlag = process.argv.indexOf('--out');
300+
const outDir = outFlag === -1 ? undefined : process.argv[outFlag + 1];
301+
if ((flag !== -1 && !demoStylesheet?.length) || (outFlag !== -1 && !outDir)) {
302+
console.error('usage: node bin/gen-docs.js [--stylesheet <path>] [--out <dir>]');
280303
process.exit(2);
281304
}
282-
const { written, deleted, errors } = generateDocs({ root, demoStylesheet });
305+
const { written, deleted, errors } = generateDocs({ root, demoStylesheet, outDir });
283306
for (const e of errors) console.error(formatError(root, e));
284307
if (errors.length) {
285308
console.error('docs: aborted');

‎bin/validate.js‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ export function validateElementTree(root, merged, file, lineOffset = 0, allowed
4747
const checkedMarkers = new Map();
4848

4949
walkElements(root, (el) => {
50+
// An element may carry more than one identity class: a utility on a
51+
// layout, `.cluster.enter`, or on a component, `.card.lift`. Each
52+
// component's own contract is still checked on its own below, but an
53+
// attribute only has to be declared by ONE of the identities present,
54+
// or a staggered cluster would be told that data-gap is unknown to
55+
// .enter and data-stagger unknown to .cluster, both of which are the
56+
// validator's confusion and not the page's.
57+
const onAnyIdentity = new Set(classList(el).flatMap((c) => (byClass.get(c)?.attributes ?? []).map((a) => a.name)));
5058
for (const cls of classList(el)) {
5159
const m = byClass.get(cls);
5260
if (!m) continue;
@@ -59,7 +67,7 @@ export function validateElementTree(root, merged, file, lineOffset = 0, allowed
5967
if (!name.startsWith('data-')) continue;
6068
const decl = declared.get(name);
6169
if (!decl) {
62-
if (!childMarkers.has(name)) push(`unknown attribute ${name}`);
70+
if (!childMarkers.has(name) && !onAnyIdentity.has(name)) push(`unknown attribute ${name}`);
6371
continue;
6472
}
6573
if (decl.type === 'enum' && !decl.values.includes(value)) push(`${name}="${value}" is not one of ${decl.values.join(', ')}`);
@@ -471,7 +479,7 @@ const MOTION_RE = new RegExp(`(?<![a-z-])(${MOTION_PROPS.join('|')})\\s*:\\s*([^
471479
*/
472480
export function validateMotion(srcDir) {
473481
const errors = [];
474-
const dirs = ['layouts', 'components'].map((d) => path.join(srcDir, d)).filter((d) => fs.existsSync(d));
482+
const dirs = ['layouts', 'components', 'utilities'].map((d) => path.join(srcDir, d)).filter((d) => fs.existsSync(d));
475483
for (const file of dirs.flatMap((d) => walkFiles(d)).filter((f) => f.endsWith('.css'))) {
476484
const text = cssText(file);
477485
for (const m of text.matchAll(MOTION_RE)) {
@@ -559,7 +567,10 @@ const MAPPED = {
559567
};
560568

561569
// Read directly by their own layout's CSS, so they have no attributes.css rule.
562-
const READ_DIRECTLY = new Set(['data-side', 'data-limit', 'data-emphasis', 'data-shape', 'data-edge', 'data-panel', 'data-orientation', 'data-placement', 'data-trigger', 'data-resize']);
570+
// data-enter and data-attention are here for the same reason: each value names
571+
// an animation on the utility's own selector, and a mapped property would be a
572+
// keyframe name in a custom property that nothing else could ever read.
573+
const READ_DIRECTLY = new Set(['data-side', 'data-limit', 'data-emphasis', 'data-shape', 'data-edge', 'data-panel', 'data-orientation', 'data-placement', 'data-trigger', 'data-resize', 'data-enter', 'data-attention']);
563574

564575
/** Every value of every mapped vocabulary must have a rule in layouts/attributes.css,
565576
* and every manifest attribute that references a vocabulary must be checked against

‎docs/accordion.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ nav_order: 1
99

1010
# Accordion
1111

12-
A column of disclosures built on details and summary, each opening to show its panel, with the browser doing the opening, the keyboard, and the announcing.
12+
<p class="lede">A column of disclosures built on details and summary, each opening to show its panel, with the browser doing the opening, the keyboard, and the announcing.</p>
1313

1414
## Example
1515

1616
<figure class="demo" data-height="lg">
17-
<div data-preview="Accordion"><iframe title="Accordion, live" srcdoc="&lt;base href=&quot;/yeti/&quot;&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;/yeti/yeti.css&quot;&gt;&lt;script type=&quot;module&quot; src=&quot;/yeti/yeti.js&quot;&gt;&lt;/script&gt;&lt;body style=&quot;margin:0;padding:var(--yeti-space-md)&quot;&gt;&lt;div class=&quot;accordion&quot;&gt;&#10; &lt;details name=&quot;faq&quot;&gt;&#10; &lt;summary&gt;Does Yeti need JavaScript?&lt;/summary&gt;&#10; &lt;p&gt;Almost never. A handful of optional modules exist and nothing depends on them.&lt;/p&gt;&#10; &lt;/details&gt;&#10; &lt;details name=&quot;faq&quot;&gt;&#10; &lt;summary&gt;Can I use my own class names?&lt;/summary&gt;&#10; &lt;p&gt;Yes. Anything Yeti does not declare is ignored.&lt;/p&gt;&#10; &lt;/details&gt;&#10;&lt;/div&gt;"></iframe></div>
17+
<div data-preview="Accordion"><iframe title="Accordion, live" srcdoc="&lt;base href=&quot;/yeti/&quot;&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;/yeti/frame.css&quot;&gt;&lt;script type=&quot;module&quot; src=&quot;/yeti/yeti.js&quot;&gt;&lt;/script&gt;&lt;body style=&quot;margin:0;padding:var(--yeti-space-md)&quot;&gt;&lt;div class=&quot;accordion&quot;&gt;&#10; &lt;details name=&quot;faq&quot;&gt;&#10; &lt;summary&gt;Does Yeti need JavaScript?&lt;/summary&gt;&#10; &lt;p&gt;Almost never. A handful of optional modules exist and nothing depends on them.&lt;/p&gt;&#10; &lt;/details&gt;&#10; &lt;details name=&quot;faq&quot;&gt;&#10; &lt;summary&gt;Can I use my own class names?&lt;/summary&gt;&#10; &lt;p&gt;Yes. Anything Yeti does not declare is ignored.&lt;/p&gt;&#10; &lt;/details&gt;&#10;&lt;/div&gt;"></iframe></div>
1818

1919
<details markdown="1">
2020
<summary>View Code</summary>

‎docs/affix.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ nav_order: 1
99

1010
# Affix
1111

12-
Joins a control with what belongs beside it, a unit, a symbol, a button, or another control, into one thing with a shared border and height.
12+
<p class="lede">Joins a control with what belongs beside it, a unit, a symbol, a button, or another control, into one thing with a shared border and height.</p>
1313

1414
## Example
1515

1616
<figure class="demo" data-height="sm">
17-
<div data-preview="Affix"><iframe title="Affix, live" srcdoc="&lt;base href=&quot;/yeti/&quot;&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;/yeti/yeti.css&quot;&gt;&lt;script type=&quot;module&quot; src=&quot;/yeti/yeti.js&quot;&gt;&lt;/script&gt;&lt;body style=&quot;margin:0;padding:var(--yeti-space-md)&quot;&gt;&lt;div class=&quot;field&quot;&gt;&#10; &lt;label for=&quot;price&quot;&gt;Price&lt;/label&gt;&#10; &lt;div class=&quot;affix&quot;&gt;&#10; &lt;span id=&quot;price-unit&quot;&gt;$&lt;/span&gt;&#10; &lt;input id=&quot;price&quot; type=&quot;number&quot; min=&quot;0&quot; step=&quot;0.01&quot; aria-describedby=&quot;price-unit&quot;&gt;&#10; &lt;button class=&quot;button&quot; type=&quot;button&quot; data-emphasis=&quot;medium&quot;&gt;Apply&lt;/button&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;"></iframe></div>
17+
<div data-preview="Affix"><iframe title="Affix, live" srcdoc="&lt;base href=&quot;/yeti/&quot;&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;/yeti/frame.css&quot;&gt;&lt;script type=&quot;module&quot; src=&quot;/yeti/yeti.js&quot;&gt;&lt;/script&gt;&lt;body style=&quot;margin:0;padding:var(--yeti-space-md)&quot;&gt;&lt;div class=&quot;field&quot;&gt;&#10; &lt;label for=&quot;price&quot;&gt;Price&lt;/label&gt;&#10; &lt;div class=&quot;affix&quot;&gt;&#10; &lt;span id=&quot;price-unit&quot;&gt;$&lt;/span&gt;&#10; &lt;input id=&quot;price&quot; type=&quot;number&quot; min=&quot;0&quot; step=&quot;0.01&quot; aria-describedby=&quot;price-unit&quot;&gt;&#10; &lt;button class=&quot;button&quot; type=&quot;button&quot; data-emphasis=&quot;medium&quot;&gt;Apply&lt;/button&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;"></iframe></div>
1818

1919
<details markdown="1">
2020
<summary>View Code</summary>

‎docs/alert.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ nav_order: 1
99

1010
# Alert
1111

12-
A message in a hue: a tinted box with a coloured edge, an optional icon, and an optional close button that the module wires up.
12+
<p class="lede">A message in a hue: a tinted box with a coloured edge, an optional icon, and an optional close button that the module wires up.</p>
1313

1414
## Example
1515

1616
<figure class="demo" data-height="sm">
17-
<div data-preview="Alert"><iframe title="Alert, live" srcdoc="&lt;base href=&quot;/yeti/&quot;&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;/yeti/yeti.css&quot;&gt;&lt;script type=&quot;module&quot; src=&quot;/yeti/yeti.js&quot;&gt;&lt;/script&gt;&lt;body style=&quot;margin:0;padding:var(--yeti-space-md)&quot;&gt;&lt;div class=&quot;alert&quot; role=&quot;status&quot; data-variant=&quot;success&quot;&gt;&#10; &lt;svg aria-hidden=&quot;true&quot; viewBox=&quot;0 0 16 16&quot;&gt;&lt;path d=&quot;M3 8.5l3 3 7-7&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;/&gt;&lt;/svg&gt;&#10; &lt;div&gt;&lt;strong&gt;Saved.&lt;/strong&gt; Your changes are live.&lt;/div&gt;&#10; &lt;button type=&quot;button&quot; data-dismiss aria-label=&quot;Dismiss&quot;&gt;×&lt;/button&gt;&#10;&lt;/div&gt;"></iframe></div>
17+
<div data-preview="Alert"><iframe title="Alert, live" srcdoc="&lt;base href=&quot;/yeti/&quot;&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;/yeti/frame.css&quot;&gt;&lt;script type=&quot;module&quot; src=&quot;/yeti/yeti.js&quot;&gt;&lt;/script&gt;&lt;body style=&quot;margin:0;padding:var(--yeti-space-md)&quot;&gt;&lt;div class=&quot;alert&quot; role=&quot;status&quot; data-variant=&quot;success&quot;&gt;&#10; &lt;svg aria-hidden=&quot;true&quot; viewBox=&quot;0 0 16 16&quot;&gt;&lt;path d=&quot;M3 8.5l3 3 7-7&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;/&gt;&lt;/svg&gt;&#10; &lt;div&gt;&lt;strong&gt;Saved.&lt;/strong&gt; Your changes are live.&lt;/div&gt;&#10; &lt;button type=&quot;button&quot; data-dismiss aria-label=&quot;Dismiss&quot;&gt;×&lt;/button&gt;&#10;&lt;/div&gt;"></iframe></div>
1818

1919
<details markdown="1">
2020
<summary>View Code</summary>

‎docs/attention.md‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
---
2+
raw: true
3+
title: "Attention"
4+
description: "Plays one pulse or one shake on load, to point at something that has just changed."
5+
nav_group: "Motion"
6+
nav_order: 1
7+
---
8+
<!-- Generated by bin/gen-docs.js from src/utilities/attention/manifest.json. Do not edit. -->
9+
10+
# Attention
11+
12+
<p class="lede">Plays one pulse or one shake on load, to point at something that has just changed.</p>
13+
14+
## Example
15+
16+
<figure class="demo" data-height="sm">
17+
<div data-preview="Attention"><iframe title="Attention, live" srcdoc="&lt;base href=&quot;/yeti/&quot;&gt;&lt;link rel=&quot;stylesheet&quot; href=&quot;/yeti/frame.css&quot;&gt;&lt;script type=&quot;module&quot; src=&quot;/yeti/yeti.js&quot;&gt;&lt;/script&gt;&lt;body style=&quot;margin:0;padding:var(--yeti-space-md)&quot;&gt;&lt;div class=&quot;alert attention&quot; data-variant=&quot;success&quot; role=&quot;status&quot;&gt;&#10; &lt;div&gt;&lt;strong&gt;Saved.&lt;/strong&gt; Your changes are live.&lt;/div&gt;&#10;&lt;/div&gt;"></iframe></div>
18+
19+
<details markdown="1">
20+
<summary>View Code</summary>
21+
22+
```html
23+
<div class="alert attention" data-variant="success" role="status">
24+
<div><strong>Saved.</strong> Your changes are live.</div>
25+
</div>
26+
```
27+
28+
</details>
29+
</figure>
30+
31+
## When to use it
32+
33+
On something that has just changed and might otherwise be missed: the alert that appeared after a save, the total that moved when a quantity changed, the field the server rejected. It plays on load, so in practice it arrives with the element, whether that element came from a navigation or was written into the page.
34+
35+
Not as an ornament, and not on anything permanent. A pulse on a heading that has always been there is an instruction to look at something that has nothing to say.
36+
37+
```html
38+
<p class="badge attention" data-attention="shake" data-variant="alert" role="status">Card declined</p>
39+
```
40+
41+
## How it works
42+
43+
One run of one animation, over `--yeti-attention-duration`. `pulse` swells the element to `--yeti-attention-scale` at the halfway point and settles back; the peak is in the middle so the gesture is symmetrical, since something that grows and snaps back reads as a twitch. `shake` throws it `--yeti-attention-distance` to each side and back twice, because a single sideways move is a slide and it takes the return trip to read as a shake.
44+
45+
It runs once, and the iteration count is deliberately left at the initial `1` rather than reading `--yeti-motion-iterations`, which exists for the animations that never end. A gesture that keeps repeating stops being a moment that has passed and becomes a state the page is in, and the reader has no way to dismiss it.
46+
47+
No keyframe leaves the element anywhere but where it started, so no fill mode is needed and nothing here can strand an element off its mark.
48+
49+
## Accessibility
50+
51+
Motion is not an announcement. A screen reader hears nothing at all here, so a change worth pointing at is a change worth saying: put the element in a live region, or give it `role="status"`, and the gesture becomes the visual half of something that is already spoken.
52+
53+
Under reduced motion `--yeti-attention-duration` collapses and the element simply sits still, which is the whole of the accommodation — there is nothing left to see, and the live region is still heard.
54+
55+
## Attributes
56+
57+
| Attribute | Type | Values | Default | Description |
58+
| --- | --- | --- | --- | --- |
59+
| `data-attention` | enum | `pulse`, `shake` | `pulse` | Which gesture: a swell and settle, or a shake from side to side. |
60+
61+
## Children
62+
63+
No structural requirements.
64+
65+
## Tokens
66+
67+
| Token | Description |
68+
| --- | --- |
69+
| `--yeti-attention-duration` | How long the gesture takes; reduced motion collapses it. |
70+
| `--yeti-attention-distance` | How far a shake throws the element to each side. |
71+
| `--yeti-attention-scale` | How large a pulse swells the element at its peak. |
72+
| `--yeti-ease` | The easing of the gesture. |
73+
74+
## Accessibility
75+
76+
- Motion is not an announcement. A change worth pointing at is worth saying, so put the element in a live region, or give it role="status", and the gesture becomes the visual half of something a screen reader already hears. It plays once and never repeats, so nothing here keeps moving under a reader who cannot dismiss it, and under reduced motion the duration collapses and the element simply sits still.
77+
78+
## Browser support
79+
80+
- Used without guards: individual transform properties
81+
- Behind `@supports`: nothing
82+
83+
## JavaScript
84+
85+
None. This component is CSS only.
86+
87+
Available since 7.0.0.

0 commit comments

Comments
 (0)