stitch2 0.12.1 → 0.13.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/README.md +5 -1
- package/dist/audit.js +159 -0
- package/dist/elements.js +26 -0
- package/package.json +1 -1
- package/skills/stitch2/SKILL.md +4 -1
- package/skills/stitch2/references/google.md +122 -0
- package/skills/stitch2/references/guides.md +4 -0
- package/skills/stitch2/references/principles.md +9 -3
- package/skills/stitch2/references/screens.md +5 -0
- package/skills/stitch2/references/typography.md +3 -0
package/README.md
CHANGED
|
@@ -46,7 +46,11 @@ file that already exists. From there the agent spends its effort on the design.
|
|
|
46
46
|
other text, labels cut short with an ellipsis, content under fixed bars, fixed bars that overlap each other, tap
|
|
47
47
|
targets (WCAG 2.2), text contrast in every theme, text that is not one of DESIGN.md's type levels, capitals without tracking,
|
|
48
48
|
large numbers in monospace, components built by hand, components that differ between screens, and colours
|
|
49
|
-
written as values instead of DESIGN.md tokens.
|
|
49
|
+
written as values instead of DESIGN.md tokens. Google's guidelines (Material 3, Android's app quality
|
|
50
|
+
guidelines, web.dev, Lighthouse) add warnings for text under 11 px, text closer than 16 px (24 px from 600 px)
|
|
51
|
+
to the edge, lines over 75 characters, targets under 48 px without room around them, placeholder-only labels,
|
|
52
|
+
icon-only controls without a name, more than five bottom-navigation destinations and controls without visible
|
|
53
|
+
focus. Each screen is also run in long and right-to-left
|
|
50
54
|
pseudo-languages and your locales (see below).
|
|
51
55
|
- **Consistency report** (`stitch2 consistency`): every shared component (tab bar, header, buttons, chips, …)
|
|
52
56
|
cropped from every screen, next to the catalog's version, with what differs (sizes, fonts, icon set, icons).
|
package/dist/audit.js
CHANGED
|
@@ -370,5 +370,164 @@ export async function audit(opts) {
|
|
|
370
370
|
if (ratio < need)
|
|
371
371
|
push(ratio < need - 1 ? 'error' : 'warning', 'contrast', el, `Low contrast ${ratio.toFixed(2)}:1 (needs ${need}:1)`);
|
|
372
372
|
}
|
|
373
|
+
// 6. Google's guidelines (Material 3, Android's app quality guidelines, web.dev, Lighthouse). Warnings: each names
|
|
374
|
+
// the rule it comes from, so a screen can break one on purpose (see skills/stitch2/references/google.md).
|
|
375
|
+
const textRects = (el) => {
|
|
376
|
+
const range = document.createRange();
|
|
377
|
+
range.selectNodeContents(el);
|
|
378
|
+
return [...range.getClientRects()].filter((r) => r.width > 1 && r.height > 1);
|
|
379
|
+
};
|
|
380
|
+
const scrollsSideways = (el) => {
|
|
381
|
+
for (let e = el.parentElement; e && e !== document.body; e = e.parentElement) {
|
|
382
|
+
const ox = getComputedStyle(e).overflowX;
|
|
383
|
+
if (ox === 'auto' || ox === 'scroll')
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
};
|
|
388
|
+
const margin = W < 600 ? 16 : 24;
|
|
389
|
+
for (const el of all) {
|
|
390
|
+
if (!ownText(el) || opacity(el) < 0.1)
|
|
391
|
+
continue;
|
|
392
|
+
const cs = getComputedStyle(el);
|
|
393
|
+
const size = parseFloat(cs.fontSize);
|
|
394
|
+
// 6a. Material's smallest type level is 11 (label small); body text starts at 12, which Lighthouse asks of
|
|
395
|
+
// most text on a page.
|
|
396
|
+
if (size < 11)
|
|
397
|
+
push('warning', 'text-small', el, 'Text at ' + size + "px: Material's smallest level is 11px, and body text starts at 12px");
|
|
398
|
+
const rects = textRects(el);
|
|
399
|
+
if (!rects.length)
|
|
400
|
+
continue;
|
|
401
|
+
// 6b. Margins: Material keeps content 16dp from the edge of a compact window, 24dp from medium ones up.
|
|
402
|
+
// Navigation bars and tab strips spread their items across the full width; the margin is for content.
|
|
403
|
+
if (!scrollsSideways(el) && !el.closest('nav, [role=navigation], [role=tablist]')) {
|
|
404
|
+
const left = Math.min(...rects.map((r) => r.left));
|
|
405
|
+
const right = Math.max(...rects.map((r) => r.right));
|
|
406
|
+
const gap = Math.min(left, W - right);
|
|
407
|
+
if (gap >= 0 && gap < margin - 0.5)
|
|
408
|
+
push('warning', 'edge-margin', el, 'Text ' + Math.round(gap) + "px from the screen edge (Material's margin is " + margin + 'px at this width)');
|
|
409
|
+
}
|
|
410
|
+
// 6c. Line length: 45–75 characters a line, in every language (Android's app quality guidelines, web.dev).
|
|
411
|
+
if (el.querySelector('p, div, li, ul, ol, section, article, br, h1, h2, h3, h4'))
|
|
412
|
+
continue;
|
|
413
|
+
// Measured word by word: each word's line, then the longest full line (the last one may be short).
|
|
414
|
+
if (rects.length < 2)
|
|
415
|
+
continue;
|
|
416
|
+
const lineChars = [];
|
|
417
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
418
|
+
for (let t = walker.nextNode(); t; t = walker.nextNode())
|
|
419
|
+
for (const m of t.textContent.matchAll(/\S+\s*/g)) {
|
|
420
|
+
const range = document.createRange();
|
|
421
|
+
range.setStart(t, m.index);
|
|
422
|
+
range.setEnd(t, m.index + m[0].trimEnd().length);
|
|
423
|
+
const top = range.getBoundingClientRect().top;
|
|
424
|
+
const line = lineChars.find((l) => Math.abs(l.top - top) < size * 0.6);
|
|
425
|
+
if (line)
|
|
426
|
+
line.n += m[0].replace(/\s+/g, ' ').length;
|
|
427
|
+
else
|
|
428
|
+
lineChars.push({ top, n: m[0].replace(/\s+/g, ' ').length });
|
|
429
|
+
}
|
|
430
|
+
if (lineChars.length < 2)
|
|
431
|
+
continue;
|
|
432
|
+
const longest = Math.max(...lineChars.sort((a, b) => a.top - b.top).slice(0, -1).map((l) => l.n));
|
|
433
|
+
if (longest > 80)
|
|
434
|
+
push('warning', 'measure', el, 'Lines of up to ' + longest + ' characters: keep prose to 45–75 (a max width of about 32em)');
|
|
435
|
+
}
|
|
436
|
+
// 6d. Touch targets: Material and Lighthouse want 48×48; a smaller one fails when the 48px square around its
|
|
437
|
+
// centre covers a quarter of its area of another target (about 8px apart is enough). Touch-sized windows only.
|
|
438
|
+
const named = (el) => text(el) || el.getAttribute('aria-label') || el.tagName.toLowerCase();
|
|
439
|
+
// Compared like overlaps (4b): behind an open modal only its own controls, and within one layer.
|
|
440
|
+
if (W < 600)
|
|
441
|
+
for (const el of solid) {
|
|
442
|
+
if (solid.some((o) => o !== el && o.contains(el)) || (el.matches('a') && el.closest('p')))
|
|
443
|
+
continue;
|
|
444
|
+
const r = el.getBoundingClientRect();
|
|
445
|
+
if (r.width >= 48 && r.height >= 48)
|
|
446
|
+
continue;
|
|
447
|
+
if (out.some((i) => i.type === 'tap-target' && i.selector === sel(el)))
|
|
448
|
+
continue;
|
|
449
|
+
const cx = r.left + r.width / 2;
|
|
450
|
+
const cy = r.top + r.height / 2;
|
|
451
|
+
const hit = solid.find((o) => {
|
|
452
|
+
if (o === el || o.contains(el) || el.contains(o) || layer(o) !== layer(el))
|
|
453
|
+
return false;
|
|
454
|
+
const q = o.getBoundingClientRect();
|
|
455
|
+
const ox = Math.max(0, Math.min(cx + 24, q.right) - Math.max(cx - 24, q.left));
|
|
456
|
+
const oy = Math.max(0, Math.min(cy + 24, q.bottom) - Math.max(cy - 24, q.top));
|
|
457
|
+
return ox * oy > 48;
|
|
458
|
+
});
|
|
459
|
+
if (hit)
|
|
460
|
+
push('warning', 'tap-spacing', el, Math.round(r.width) + '×' + Math.round(r.height) + 'px and close to “' + named(hit).slice(0, 24) + '”: make it 48px or leave about 8px around it (Material, Lighthouse)');
|
|
461
|
+
}
|
|
462
|
+
// 6e. Forms (web.dev): a placeholder is a hint, not a label; it disappears as soon as someone types.
|
|
463
|
+
const visibleText = (n) => !!n && shown(n) && !!(n.textContent ?? '').trim();
|
|
464
|
+
for (const el of all) {
|
|
465
|
+
if (!el.matches('input, textarea'))
|
|
466
|
+
continue;
|
|
467
|
+
if (el.matches('[type=hidden], [type=checkbox], [type=radio], [type=submit], [type=button], [type=range], [type=search], [role=searchbox]'))
|
|
468
|
+
continue;
|
|
469
|
+
if (el.closest('[role=search], search'))
|
|
470
|
+
continue;
|
|
471
|
+
const f = el;
|
|
472
|
+
if (!f.placeholder)
|
|
473
|
+
continue;
|
|
474
|
+
const labelled = [...(f.labels ?? [])].some(visibleText) ||
|
|
475
|
+
(f.getAttribute('aria-labelledby') ?? '').split(/\s+/).some((id) => visibleText(document.getElementById(id)));
|
|
476
|
+
if (!labelled)
|
|
477
|
+
push('warning', 'placeholder-label', el, 'Its placeholder is its only label, and it disappears while typing: add a visible label (web.dev)');
|
|
478
|
+
}
|
|
479
|
+
// 6f. Every control has a name (Android's content descriptions, WCAG 4.1.2): an icon-only button needs a label.
|
|
480
|
+
for (const el of interactive) {
|
|
481
|
+
if (el.matches('input, select, textarea'))
|
|
482
|
+
continue;
|
|
483
|
+
const name = (el.textContent ?? '').trim() ||
|
|
484
|
+
el.getAttribute('aria-label') ||
|
|
485
|
+
el.getAttribute('title') ||
|
|
486
|
+
[...el.querySelectorAll('img[alt], svg title')].map((i) => i.getAttribute('alt') ?? i.textContent).join('').trim() ||
|
|
487
|
+
(el.getAttribute('aria-labelledby') ?? '').split(/\s+/).map((id) => document.getElementById(id)?.textContent ?? '').join('').trim();
|
|
488
|
+
if (!name)
|
|
489
|
+
push('warning', 'unnamed-control', el, 'An icon-only control with no name: screen readers announce nothing (add aria-label)');
|
|
490
|
+
}
|
|
491
|
+
// 6g. Material's navigation bar holds three to five destinations; more belong in a rail, a drawer or a page.
|
|
492
|
+
for (const el of all) {
|
|
493
|
+
if (!['fixed', 'sticky'].includes(getComputedStyle(el).position))
|
|
494
|
+
continue;
|
|
495
|
+
const r = el.getBoundingClientRect();
|
|
496
|
+
if (r.bottom < innerHeight - 2 || r.width < W * 0.8)
|
|
497
|
+
continue;
|
|
498
|
+
if (!el.matches('nav, [role=navigation], [role=tablist]') && !el.querySelector('nav, [role=navigation], [role=tablist]'))
|
|
499
|
+
continue;
|
|
500
|
+
const items = [...el.querySelectorAll('a[href], button, [role=tab]')].filter((i) => shown(i) && !i.parentElement?.closest('a[href], button, [role=tab]'));
|
|
501
|
+
if (items.length > 5)
|
|
502
|
+
push('warning', 'nav-destinations', el, items.length + " destinations in the bottom navigation: Material's navigation bar holds 3–5");
|
|
503
|
+
}
|
|
504
|
+
// 6h. Keyboard focus is visible (Material's focus state, web.dev, WCAG 2.4.7): focusing a control changes how
|
|
505
|
+
// it or its frame looks. Transitions are paused so a ring that fades in still counts.
|
|
506
|
+
const still = document.createElement('style');
|
|
507
|
+
still.textContent = '*, *::before, *::after { transition: none !important; animation: none !important }';
|
|
508
|
+
document.head.append(still);
|
|
509
|
+
const look = (e) => {
|
|
510
|
+
if (!e)
|
|
511
|
+
return '';
|
|
512
|
+
const c = getComputedStyle(e);
|
|
513
|
+
return [c.outlineStyle === 'none' ? '' : c.outlineStyle + c.outlineWidth + c.outlineColor, c.boxShadow, c.backgroundColor, c.borderColor, c.color, c.textDecorationLine].join('|');
|
|
514
|
+
};
|
|
515
|
+
const frameOf = (e) => [e, e.parentElement, e.parentElement?.parentElement ?? null].map(look).join('#');
|
|
516
|
+
const before = document.activeElement;
|
|
517
|
+
for (const el of interactive) {
|
|
518
|
+
const h = el;
|
|
519
|
+
if (h.disabled || opacity(h) < 0.05)
|
|
520
|
+
continue;
|
|
521
|
+
const was = frameOf(h);
|
|
522
|
+
h.focus({ preventScroll: true, focusVisible: true });
|
|
523
|
+
if (document.activeElement !== h)
|
|
524
|
+
continue;
|
|
525
|
+
const now = frameOf(h);
|
|
526
|
+
h.blur();
|
|
527
|
+
if (was === now)
|
|
528
|
+
push('warning', 'focus-visible', el, 'No visible focus: someone using a keyboard cannot see where they are (add a :focus-visible ring)');
|
|
529
|
+
}
|
|
530
|
+
before?.focus?.({ preventScroll: true });
|
|
531
|
+
still.remove();
|
|
373
532
|
return out.sort((x, y) => (x.severity === y.severity ? 0 : x.severity === 'error' ? -1 : 1));
|
|
374
533
|
}
|
package/dist/elements.js
CHANGED
|
@@ -30,6 +30,31 @@ export function translate(key, fallback, args) {
|
|
|
30
30
|
}
|
|
31
31
|
/** The locale the screen is shown in ("en", "ru", "ar-XB" for the right-to-left pseudo-language), or undefined. */
|
|
32
32
|
export const screenLocale = () => globalThis.__stitch2?.lang;
|
|
33
|
+
/**
|
|
34
|
+
* Each element renders on its own, so ids a framework generates (React's useId) repeat from one element to the
|
|
35
|
+
* next, and a label's for= finds the first field on the page. An id already on the page is renamed, with the
|
|
36
|
+
* attributes inside the element that point to it.
|
|
37
|
+
*/
|
|
38
|
+
let idCount = 0;
|
|
39
|
+
const ID_REFS = ['for', 'aria-labelledby', 'aria-describedby', 'aria-controls', 'aria-owns', 'aria-activedescendant', 'list'];
|
|
40
|
+
function uniqueIds(content) {
|
|
41
|
+
const renamed = new Map();
|
|
42
|
+
for (const el of Array.from(content.querySelectorAll('[id]'))) {
|
|
43
|
+
if (!document.getElementById(el.id))
|
|
44
|
+
continue;
|
|
45
|
+
const next = el.id + '-s2-' + ++idCount;
|
|
46
|
+
renamed.set(el.id, next);
|
|
47
|
+
el.id = next;
|
|
48
|
+
}
|
|
49
|
+
if (!renamed.size)
|
|
50
|
+
return;
|
|
51
|
+
for (const el of Array.from(content.querySelectorAll('*')))
|
|
52
|
+
for (const attr of ID_REFS) {
|
|
53
|
+
const v = el.getAttribute(attr);
|
|
54
|
+
if (v)
|
|
55
|
+
el.setAttribute(attr, v.split(/\s+/).map((id) => renamed.get(id) ?? id).join(' '));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
33
58
|
const camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
34
59
|
export function defineElements(components, options) {
|
|
35
60
|
const { prefix, render } = options;
|
|
@@ -70,6 +95,7 @@ export function defineElements(components, options) {
|
|
|
70
95
|
if (typeof props[key] === 'string' && !root.hasAttribute(`data-${key}`))
|
|
71
96
|
root.setAttribute(`data-${key}`, props[key]);
|
|
72
97
|
}
|
|
98
|
+
uniqueIds(t.content);
|
|
73
99
|
this.replaceWith(t.content);
|
|
74
100
|
if (options.after && !pending++)
|
|
75
101
|
queueMicrotask(() => {
|
package/package.json
CHANGED
package/skills/stitch2/SKILL.md
CHANGED
|
@@ -25,6 +25,7 @@ References, read before the first screen:
|
|
|
25
25
|
- [typography.md](references/typography.md): type levels and how to combine them; most polish is here.
|
|
26
26
|
- [components.md](references/components.md): build from the shared components; the reuse ladder.
|
|
27
27
|
- [screens.md](references/screens.md): screen names, versions, statuses, the user's approval and notes, desktop.
|
|
28
|
+
- [google.md](references/google.md): Google's guidance (Material, Android, web.dev) as rules with numbers.
|
|
28
29
|
- [guides.md](references/guides.md): the public sources behind the rules.
|
|
29
30
|
|
|
30
31
|
## Workflow
|
|
@@ -49,7 +50,9 @@ References, read before the first screen:
|
|
|
49
50
|
fixed bars, fixed bars overlapping each other, tap targets, contrast, cramped text) and every warning about type (`type-*`), hand-built
|
|
50
51
|
components (`reuse`), components that differ from the catalog (`consistency`) and colour values (`color`).
|
|
51
52
|
Text running into other text (`text-overlap`) or out of its card (`overflow`) is an error; a short label cut
|
|
52
|
-
short with an ellipsis (`truncated`) is a warning to fix with room, not a smaller size.
|
|
53
|
+
short with an ellipsis (`truncated`) is a warning to fix with room, not a smaller size. Google's rules are
|
|
54
|
+
warnings too (`text-small`, `edge-margin`, `measure`, `tap-spacing`, `placeholder-label`, `unnamed-control`,
|
|
55
|
+
`nav-destinations`, `focus-visible`; see google.md): fix them, or say in the hand-over why the screen breaks one. When DESIGN.md has a
|
|
53
56
|
light and a dark theme, every screen is checked in both: an issue marked with a theme
|
|
54
57
|
("light theme: Low contrast…") is fixed in DESIGN.md's colours for that theme when it shows on many
|
|
55
58
|
screens, or in the screen when it is one element. Look at both renders (`--light.png`). With `i18n`, check
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Google's design guidance, as rules
|
|
2
|
+
|
|
3
|
+
Google publishes its app and web design guidance in several places: Material Design 3 and its component
|
|
4
|
+
libraries, Android's app quality guidelines, web.dev's Learn Design and Learn Forms courses, and Lighthouse's
|
|
5
|
+
audits. This file restates what applies to screens in our own words, with Google's numbers. Rules marked
|
|
6
|
+
**(check: name)** are measured by `stitch2 check` (warnings, so a screen can break one on purpose and say why);
|
|
7
|
+
the rest are judged from the render. Where Google and DESIGN.md disagree, DESIGN.md wins for the project.
|
|
8
|
+
|
|
9
|
+
## Layout and window sizes
|
|
10
|
+
|
|
11
|
+
- **Design for width classes, not devices.** Material and Android group available width into classes:
|
|
12
|
+
compact under 600 px (phones), medium 600–839 (tablets in portrait, unfolded foldables), expanded 840–1199
|
|
13
|
+
(tablets in landscape, small laptops), large 1200–1599 and extra large from 1600 (desktops). A window can
|
|
14
|
+
change class while open (rotation, split screen, a resized browser), so a layout follows the width it has.
|
|
15
|
+
- **Order of work:** the compact layout first, then expanded (the most room for change), then decide whether
|
|
16
|
+
medium needs its own layout or takes one of the two.
|
|
17
|
+
- **A small screen is not a shrunk large one, and a large screen is not a blown-up small one** (web.dev). On a
|
|
18
|
+
wide window, add columns, panes or a side navigation; do not just widen the phone column.
|
|
19
|
+
- **Navigation follows the class:** a bottom navigation bar in compact windows, a navigation rail at the side
|
|
20
|
+
in medium and expanded ones, a drawer (persistent) when there is room for labels and the app has many places.
|
|
21
|
+
- **Canonical layouts** (Material): *list-detail* shows both panes side by side from expanded widths and one
|
|
22
|
+
pane at a time below that (narrowing keeps the detail that was open); *supporting pane* puts secondary
|
|
23
|
+
content beside the main one when there is room; *feed* is a grid of cards that adds columns as width grows.
|
|
24
|
+
- **Margins:** content keeps 16 px from the window edge in compact windows and 24 px from medium up
|
|
25
|
+
**(check: edge-margin)**. Only deliberate horizontal scrollers (chip rows, carousels) run to the edge.
|
|
26
|
+
- **Spacing on a 4 px grid** (Material's 4dp increments): the spacing scale in DESIGN.md should be multiples
|
|
27
|
+
of 4, with 2 px only inside small components.
|
|
28
|
+
|
|
29
|
+
## Type
|
|
30
|
+
|
|
31
|
+
- **Material's scale** has fifteen levels in five roles, each in three sizes: display 57 / 45 / 36, headline
|
|
32
|
+
32 / 28 / 24, title 22 / 16 / 14, body 16 / 14 / 12, label 14 / 12 / 11 (sp, at 1x). Titles and labels are
|
|
33
|
+
medium weight, the rest regular. An *emphasized* set adds weight, never size, for selection and headlines.
|
|
34
|
+
Map DESIGN.md's levels onto these roles when choosing sizes; the names can stay the project's.
|
|
35
|
+
- **Smallest sizes:** nothing under 11 px (Material's label small), and body text from 12 px up; Lighthouse
|
|
36
|
+
wants at least 12 px for most of a page's text **(check: text-small)**. Prefer 14–16 for reading text.
|
|
37
|
+
- **Line length 45–75 characters, in every language the app ships** (Android's app quality guidelines; about
|
|
38
|
+
66 is ideal, per web.dev). Cap prose with a max width in a relative unit, never in pixels: about `32em` lands on
|
|
39
|
+
45–75 characters in most fonts. `ch` is the width of the font's zero, which is wider than an average letter,
|
|
40
|
+
so `66ch` holds 80–90 characters in many sans-serifs **(check: measure)**.
|
|
41
|
+
- **Line height** is unitless and follows line length: short lines can take more, long lines less (web.dev).
|
|
42
|
+
Material pairs 16/24 and 14/20 for body.
|
|
43
|
+
- **Text resizes:** sizes in rem so the user's font size setting works; the layout still holds at 200%.
|
|
44
|
+
- **Few fonts:** every web font file delays text; load what the type levels use and nothing else.
|
|
45
|
+
|
|
46
|
+
## Touch, pointer and keyboard
|
|
47
|
+
|
|
48
|
+
- **Touch targets 48 × 48 px** (Material, Android's quality guidelines, web.dev), about 9 mm; an icon of 24 px
|
|
49
|
+
gets padding up to 48. A smaller target needs about 8 px of space to its neighbours: Lighthouse fails a
|
|
50
|
+
target under 48 px when the 48 px square around its centre covers another target **(check: tap-spacing)**.
|
|
51
|
+
WCAG's 24 px floor is checked separately **(check: tap-target)**.
|
|
52
|
+
- **Fine pointers** may take smaller targets, but Fitts's law still applies: shrink with care.
|
|
53
|
+
- **Do not hide information behind hover.** Hover is optional (touch has none, keyboards focus instead); what
|
|
54
|
+
shows on hover must be reachable without it.
|
|
55
|
+
- **Visible focus** on every control, styled with `:focus-visible`, in the order the screen reads
|
|
56
|
+
**(check: focus-visible)**. Material's focus state is a ring plus a light state layer.
|
|
57
|
+
|
|
58
|
+
## Colour, states and themes
|
|
59
|
+
|
|
60
|
+
- **Roles in pairs:** text on a colour uses that colour's "on" partner (on-primary on primary, on-surface on
|
|
61
|
+
surface). Depth comes from tonal surface containers (lowest to highest) rather than shadows.
|
|
62
|
+
- **Contrast:** 4.5:1 for text under 18 px (14 px bold), 3:1 above **(check: contrast)**. Never colour alone:
|
|
63
|
+
a link also has an underline or weight; a status also has an icon or word.
|
|
64
|
+
- **Light and dark:** Android's quality guidelines ask for both themes, and for web content shown in the app
|
|
65
|
+
to follow them (stitch2 checks every theme DESIGN.md defines).
|
|
66
|
+
- **States as layers:** hover 8%, focus 10%, pressed 10%, dragged 16% of the content colour over the
|
|
67
|
+
container; disabled content at 38% and its container at 12%. Every interactive component defines them.
|
|
68
|
+
- **Shape scale:** corners from a short scale (Material: 4, 8, 12, 16, 20, 28, 32, 48, full). Pick each
|
|
69
|
+
component's corner from DESIGN.md's `rounded` and keep it the same everywhere.
|
|
70
|
+
|
|
71
|
+
## Components
|
|
72
|
+
|
|
73
|
+
- **Navigation bar:** three to five destinations, each an icon with a one-line label; more destinations go to a
|
|
74
|
+
rail, a drawer or a page **(check: nav-destinations; label-wraps)**.
|
|
75
|
+
- **Button emphasis:** filled for the one main action, tonal or outlined for secondary, text buttons for
|
|
76
|
+
low-emphasis; one floating action button at most.
|
|
77
|
+
- **Icon-only controls have a name** that says what they do ("Share", not "Share button"); repeated items in a
|
|
78
|
+
list have different names; decorative icons are hidden from screen readers **(check: unnamed-control)**.
|
|
79
|
+
- **Navigation on small screens** (web.dev): with a few links, show them; with many, let them scroll sideways
|
|
80
|
+
(the overflow pattern); hide them behind a toggle only as a last resort, and then label the toggle with a
|
|
81
|
+
word, not only an icon.
|
|
82
|
+
- **Carousels** hide content: on small screens show a part of the next item so it reads as scrollable; on
|
|
83
|
+
large screens show a grid. No autoplay.
|
|
84
|
+
|
|
85
|
+
## Forms (web.dev)
|
|
86
|
+
|
|
87
|
+
- **Visible labels.** A placeholder is a hint about the format and vanishes while typing; it is never the
|
|
88
|
+
label **(check: placeholder-label)**. Search fields with a search icon are the accepted exception.
|
|
89
|
+
- **The right control and keyboard:** `textarea` for several lines, `type="email"`, `tel`, `inputmode="decimal"`
|
|
90
|
+
so phones show the matching keyboard; a control looks like what it does (a link navigates, a button acts).
|
|
91
|
+
- **One column, in reading order,** with related fields grouped and 48 px controls with room between them.
|
|
92
|
+
|
|
93
|
+
## Motion
|
|
94
|
+
|
|
95
|
+
- **Durations** (Material): short 50–200 ms for small changes (a switch, a ripple), medium 250–400 ms for
|
|
96
|
+
elements entering or expanding, long 450–600 ms for large transitions, up to 1000 ms only for full-screen
|
|
97
|
+
ones. Longer distance or area, longer duration.
|
|
98
|
+
- **Easing:** standard `cubic-bezier(0.2, 0, 0, 1)` for movement within the screen; emphasized decelerate
|
|
99
|
+
`(0.05, 0.7, 0.1, 1)` for things entering, accelerate `(0.3, 0, 0.8, 0.15)` for things leaving.
|
|
100
|
+
|
|
101
|
+
## Responsiveness of the product (for the app, beyond the screens)
|
|
102
|
+
|
|
103
|
+
- Feedback within about two seconds: a progress indicator or skeleton when content takes longer.
|
|
104
|
+
- State survives leaving and coming back (a half-filled form, a running timer); back navigation never loses work.
|
|
105
|
+
- Logical properties (start/end) so right-to-left languages mirror; see the stitch2-i18n skill.
|
|
106
|
+
|
|
107
|
+
## Sources
|
|
108
|
+
|
|
109
|
+
- Material Components for Android, theming docs (type scale, shape, colour, motion):
|
|
110
|
+
github.com/material-components/material-components-android/tree/master/docs
|
|
111
|
+
- Material Design 3: m3.material.io (layout, window size classes, canonical layouts, states, accessibility)
|
|
112
|
+
- Android window size classes: developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes
|
|
113
|
+
- Android canonical layouts: developer.android.com/develop/ui/compose/layouts/adaptive/canonical-layouts
|
|
114
|
+
- Android core app quality: developer.android.com/docs/quality-guidelines/core-app-quality
|
|
115
|
+
- Android adaptive app quality: developer.android.com/docs/quality-guidelines/large-screen-app-quality
|
|
116
|
+
- Android accessibility: developer.android.com/guide/topics/ui/accessibility/apps
|
|
117
|
+
- web.dev Learn Design (typography, macro layouts, UI patterns, interaction, accessibility, theming,
|
|
118
|
+
internationalization): web.dev/learn/design
|
|
119
|
+
- web.dev Learn Forms, design basics: web.dev/learn/forms/design-basics
|
|
120
|
+
- web.dev accessible tap targets: web.dev/articles/accessible-tap-targets
|
|
121
|
+
- Lighthouse tap targets and font size audits: developer.chrome.com/docs/lighthouse/seo/tap-targets,
|
|
122
|
+
developer.chrome.com/docs/lighthouse/seo/font-size
|
|
@@ -18,3 +18,7 @@ are restated in principles.md.
|
|
|
18
18
|
| Vercel Geist typography (vercel.com/geist/typography) | yes | The type scale made for Geist by its authors: sizes, line heights and the tracking that suits it (and fonts like it) |
|
|
19
19
|
| Apple HIG, Typography (developer.apple.com/design/human-interface-guidelines/typography) | yes | iOS Dynamic Type sizes, the SF Pro tracking table, minimum sizes (11pt), watchOS sizes |
|
|
20
20
|
| Material 3 type scale (m3.material.io/styles/typography/type-scale-tokens) | yes | Role names (display, headline, title, body, label) and how labels open up at small sizes |
|
|
21
|
+
| Google's guidance, restated as rules ([google.md](google.md)) | — | Window size classes, canonical layouts, Material's type, shape, state and motion numbers, 48 px targets, line length, forms |
|
|
22
|
+
| Android app quality guidelines (developer.android.com/docs/quality-guidelines) | yes | What Google Play calls a quality app: 48dp targets, contrast, line length 45–75, light and dark, adaptive layouts |
|
|
23
|
+
| web.dev Learn Design and Learn Forms (web.dev/learn/design, web.dev/learn/forms) | yes | Responsive layout, typography, navigation patterns, interaction (pointer, hover), accessibility, forms |
|
|
24
|
+
| Lighthouse audits (developer.chrome.com/docs/lighthouse) | yes | The exact rules behind tap target spacing and legible font sizes |
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
In our own words; the ideas are common practice in interface design, drawn from Refactoring UI (Wathan &
|
|
4
4
|
Schoger), Material 3, Apple's Human Interface Guidelines, WCAG 2.2 and the Laws of UX (see
|
|
5
|
-
[guides.md](guides.md)); no text is copied from them.
|
|
5
|
+
[guides.md](guides.md)); no text is copied from them. Google's guidance (Material, Android's app quality
|
|
6
|
+
guidelines, web.dev, Lighthouse) is gathered with its numbers in [google.md](google.md). Items marked **(check)** are measured by `stitch2 check`;
|
|
6
7
|
the rest are judged from the render. Colour names below are roles: the project's DESIGN.md names the actual
|
|
7
8
|
colours (its accent, its primary and secondary text, its surface steps).
|
|
8
9
|
|
|
@@ -28,7 +29,8 @@ colours (its accent, its primary and secondary text, its surface steps).
|
|
|
28
29
|
- Align to a few vertical lines: the page edge, the card's inner edge, one column for numbers. Numbers in lists
|
|
29
30
|
are right-aligned so their digits line up.
|
|
30
31
|
- Fewer borders. Separate with spacing or a surface step first, a hairline second, never a card inside a card.
|
|
31
|
-
- Do not fill the width because it is there; prose reads best at
|
|
32
|
+
- Do not fill the width because it is there; prose reads best at 45–75 characters a line, in every language
|
|
33
|
+
the product ships (**check: measure**).
|
|
32
34
|
|
|
33
35
|
## Type
|
|
34
36
|
|
|
@@ -58,7 +60,11 @@ short labels never wrap **(check)**.
|
|
|
58
60
|
|
|
59
61
|
## Touch and mobile
|
|
60
62
|
|
|
61
|
-
- Tap targets
|
|
63
|
+
- Tap targets 48 px (Google: Material, Android, Lighthouse; Apple's minimum is 44); smaller ones only with
|
|
64
|
+
about 8 px of space around them **(check: tap-target, tap-spacing)**.
|
|
65
|
+
- Content keeps 16 px from the screen edge on phones, 24 px from 600 px up **(check: edge-margin)**.
|
|
66
|
+
- Every control shows keyboard focus and every icon-only control has a name **(check: focus-visible,
|
|
67
|
+
unnamed-control)**; form fields have visible labels, not placeholders **(check: placeholder-label)**.
|
|
62
68
|
- The primary action sits in the thumb zone (lower half or a docked bar); destructive actions far from it.
|
|
63
69
|
- Fixed bars never cover content: the scroll ends with enough bottom padding **(check)**.
|
|
64
70
|
- Nothing wider than the frame; only chip rows and tab strips scroll sideways **(check)**.
|
|
@@ -60,6 +60,11 @@ frame width 1280, the same components and type levels, the layout from DESIGN.md
|
|
|
60
60
|
of a tab bar, a content max width, two columns where it helps). Shared components get their desktop form in the
|
|
61
61
|
component, before the first desktop screen.
|
|
62
62
|
|
|
63
|
+
Widths follow Google's window size classes ([google.md](google.md)): compact under 600 px (the phone frame),
|
|
64
|
+
medium 600–839, expanded 840–1199, large from 1200 (the desktop frame). A desktop screen is a layout for the
|
|
65
|
+
width class, not a stretched phone: a side navigation instead of the bar, list-detail with both panes, extra
|
|
66
|
+
columns for feeds. When a project needs a tablet, it is a medium-width device of the same screen.
|
|
67
|
+
|
|
63
68
|
## The canvas
|
|
64
69
|
|
|
65
70
|
`stitch2 canvas` → http://localhost:4400. The sidebar lists DESIGN.md (drawn as tokens and rules), the
|
|
@@ -30,6 +30,9 @@ together. A typical app set (sizes for a 390 px phone):
|
|
|
30
30
|
utilities on text: that is how screens drift into twenty slightly different styles.
|
|
31
31
|
- Add a level only when a real use appears on two screens and nothing fits. Never solve a one-off with an
|
|
32
32
|
in-between size.
|
|
33
|
+
- Nothing under 11 px, body text from 12 px, reading text 14–16 px **(check: text-small)**; prose at 45–75
|
|
34
|
+
characters a line, a max width of about `32em` **(check: measure)**. Material's fifteen-level scale and its sizes are in
|
|
35
|
+
[google.md](google.md).
|
|
33
36
|
|
|
34
37
|
## 2. Hierarchy recipes
|
|
35
38
|
|