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 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. Each screen is also run in long and right-to-left
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitch2",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "A design canvas, layout and type checks, component consistency and screen versioning for agent-built UI, on Google's DESIGN.md format.",
5
5
  "license": "MIT",
6
6
  "author": "ifokeev",
@@ -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. When DESIGN.md has a
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. Items marked **(check)** are measured by `stitch2 check`;
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 about 60 characters a line.
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 at least 44 px for buttons; smaller ones only with spacing **(check)**.
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