stitch2 0.26.0 → 0.27.1

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/dist/audit.d.ts CHANGED
@@ -9,6 +9,7 @@ export declare const LAYOUT_ERRORS: string[];
9
9
  export declare function audit(opts: {
10
10
  comfortable: number;
11
11
  durations?: number[];
12
+ secondary?: string;
12
13
  }): Promise<Issue[]>;
13
14
  /**
14
15
  * Runs inside the page with prefers-reduced-motion: reduce. What still moves (an animation of transform,
package/dist/audit.js CHANGED
@@ -772,6 +772,43 @@ export async function audit(opts) {
772
772
  if (longest > 80)
773
773
  push('warning', 'measure', el, 'Lines of up to ' + longest + ' characters: keep prose to 45–75 (a max width of about 32em)');
774
774
  }
775
+ // 6e. Text tiers: a sentence to read belongs in the body colour (skills/stitch2/references/typography.md, section 3). A block of reading
776
+ // copy, drawn in the secondary colour, is the usual reason a page reads dull although every contrast ratio passes: the strokes of small
777
+ // text reach only their own colour. A block is reading copy when it holds sixty characters and eight words of its own text at 14px or
778
+ // more (a caption or a field's help text is smaller, and the secondary colour is for it) and sits in the page's content: not in a bar, a
779
+ // table, a menu, a label, a control, a link or a code sample. It needs the secondary colour, which
780
+ // check takes from DESIGN.md's text-muted and passes as a CSS colour the page can resolve (var(--<prefix>-text-muted)).
781
+ if (opts.secondary) {
782
+ const canvas = document.createElement('canvas');
783
+ canvas.width = canvas.height = 1;
784
+ const g = canvas.getContext('2d', { willReadFrequently: true });
785
+ const rgba = (value) => {
786
+ g.clearRect(0, 0, 1, 1);
787
+ g.fillStyle = '#000';
788
+ g.fillStyle = value;
789
+ g.fillRect(0, 0, 1, 1);
790
+ const d = g.getImageData(0, 0, 1, 1).data;
791
+ return [d[0], d[1], d[2], d[3] / 255];
792
+ };
793
+ const probe = document.createElement('span');
794
+ probe.style.color = opts.secondary;
795
+ document.body.append(probe);
796
+ const want = rgba(getComputedStyle(probe).color);
797
+ probe.remove();
798
+ const quiet = (el) => {
799
+ const have = rgba(getComputedStyle(el).color);
800
+ return Math.abs(have[0] - want[0]) <= 2 && Math.abs(have[1] - want[1]) <= 2 && Math.abs(have[2] - want[2]) <= 2 && Math.abs(have[3] - want[3]) <= 0.02;
801
+ };
802
+ const apart = 'nav, footer, table, button, summary, label, a, code, pre, figcaption, caption, [role=navigation], [role=tablist], [role=tab], [role=menu], [role=listbox], [role=tooltip], [role=button], [aria-hidden=true]';
803
+ for (const el of all) {
804
+ if (!ownText(el) || opacity(el) < 0.1 || el.closest(apart))
805
+ continue;
806
+ const own = [...el.childNodes].filter((n) => n.nodeType === 3).map((n) => n.textContent).join(' ').replace(/\s+/g, ' ').trim();
807
+ if (own.length < 60 || own.split(' ').length < 8 || parseFloat(getComputedStyle(el).fontSize) < 14 || !quiet(el))
808
+ continue;
809
+ push('warning', 'text-tier', el, 'Reading copy in the secondary colour: a sentence to read takes the body colour, and the secondary one is for hints, captions and counts');
810
+ }
811
+ }
775
812
  // 6d. Touch targets: Material and Lighthouse want 48×48; a smaller one fails when the 48px square around its
776
813
  // centre covers a quarter of its area of another target (about 8px apart is enough). Touch-sized windows only.
777
814
  const named = (el) => text(el) || el.getAttribute('aria-label') || el.tagName.toLowerCase();
package/dist/check.js CHANGED
@@ -32,7 +32,7 @@ import { fingerprintPaths } from './cache.js';
32
32
  import { colourIssues } from './colours.js';
33
33
  import { CONFIG_FILE, checkWidths, config, modes } from './config.js';
34
34
  import { collectComponents, collectOptions, consistencyIssues, reuseIssues, } from './consistency.js';
35
- import { designThemes, motionDurations, typeLevels } from './design-md.js';
35
+ import { designThemes, motionDurations, readDesign, typeLevels } from './design-md.js';
36
36
  import { checkLocales } from './i18n.js';
37
37
  import { listScreens, ROOT } from './screens.js';
38
38
  import { startServer } from './serve.js';
@@ -56,13 +56,22 @@ const levels = typeLevels();
56
56
  const themes = designThemes();
57
57
  const appModes = modes();
58
58
  const collect = collectOptions();
59
- /** What the layout audit needs: the comfortable tap size and DESIGN.md's motion durations. */
60
- const auditOpts = { comfortable: config.comfortableTarget, durations: (() => { try {
59
+ /** What the layout audit needs: the comfortable tap size, DESIGN.md's motion durations and its secondary text colour (text-muted, for text-tier). */
60
+ const auditOpts = {
61
+ comfortable: config.comfortableTarget,
62
+ durations: (() => { try {
61
63
  return motionDurations();
62
64
  }
63
65
  catch {
64
66
  return [];
65
- } })() };
67
+ } })(),
68
+ secondary: (() => { try {
69
+ return 'text-muted' in readDesign().colors ? 'var(--' + config.prefix + '-text-muted)' : undefined;
70
+ }
71
+ catch {
72
+ return undefined;
73
+ } })(),
74
+ };
66
75
  const server = await startServer(0);
67
76
  /** The tallest render, in CSS px: a long landing page is shot whole (the canvas draws frames up to the same height). */
68
77
  const MAX_RENDER_HEIGHT = 20000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitch2",
3
- "version": "0.26.0",
3
+ "version": "0.27.1",
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",
@@ -68,7 +68,7 @@ References, read before the first screen:
68
68
  Text running into other text (`text-overlap`) or out of its card (`overflow`) is an error; a short label cut
69
69
  short with an ellipsis (`truncated`) is a warning to fix with room, not a smaller size. Google's rules are
70
70
  warnings too (`text-small`, `edge-margin`, `measure`, `tap-spacing`, `placeholder-label`, `unnamed-control`,
71
- `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
71
+ `nav-destinations`, `focus-visible`, `text-tier`; see google.md): `text-tier` is a block of reading copy (sixty characters and eight words at 14px or more, in the page's content and not in a bar, table, control or link) drawn in DESIGN.md's `text-muted`; give it the body colour (typography.md, section 3): fix them, or say in the hand-over why the screen breaks one. When DESIGN.md has a
72
72
  light and a dark theme, every screen is checked in both: an issue marked with a theme
73
73
  ("light theme: Low contrast…") is fixed in DESIGN.md's colours for that theme when it shows on many
74
74
  screens, or in the screen when it is one element. Look at both renders (`--light.png`). With `i18n`, check
@@ -34,6 +34,11 @@ the rest are judged from the render. Where Google and DESIGN.md disagree, DESIGN
34
34
  Map DESIGN.md's levels onto these roles when choosing sizes; the names can stay the project's.
35
35
  - **Smallest sizes:** nothing under 11 px (Material's label small), and body text from 12 px up; Lighthouse
36
36
  wants at least 12 px for most of a page's text **(check: text-small)**. Prefer 14–16 for reading text.
37
+ - **Reading copy is a body, not a caption.** A sentence a person reads (an introduction, a card's description, an
38
+ answer) is Material's body: 14/20 at the smallest (body medium), 16/24 for long passages (body large). Material 3
39
+ gives the 12 and 11 sizes to labels and captions: buttons, chips, the unit under a number, "very small text in the
40
+ content body, such as captions". The Lighthouse font-size audit (12 px for 60% of a page's text; retired in
41
+ Lighthouse 13) is a floor, not a recommendation. Body line height is about 1.5 times the size.
37
42
  - **Line length 45–75 characters, in every language the app ships** (Android's app quality guidelines; about
38
43
  66 is ideal, per web.dev). Cap prose with a max width in a relative unit, never in pixels: about `32em` lands on
39
44
  45–75 characters in most fonts. `ch` is the width of the font's zero, which is wider than an average letter,
@@ -61,6 +66,12 @@ the rest are judged from the render. Where Google and DESIGN.md disagree, DESIGN
61
66
  surface). Depth comes from tonal surface containers (lowest to highest) rather than shadows.
62
67
  - **Contrast:** 4.5:1 for text under 18 px (14 px bold), 3:1 above **(check: contrast)**. Never colour alone:
63
68
  a link also has an underline or weight; a status also has an icon or word.
69
+ - **Text tiers on a dark theme:** the default text colour is "on surface", and "on surface variant" is the lower-emphasis
70
+ alternative (Material 3). In its baseline dark scheme they are #E6E1E5 and #CAC4D0 on #1C1B1F: about 89% and 77%
71
+ of white. Material 2's dark theme used 87% (high emphasis), 60% (medium emphasis and hints) and 38% (disabled); the
72
+ 60% tier reads dim for anything a person reads. Neither uses pure white for text, which vibrates and bleeds on a
73
+ dark surface. A secondary text colour below about 75% of white is a defect for text that carries meaning, and a
74
+ sentence of reading copy in the secondary colour is flagged by `check` **(check: text-tier)**.
64
75
  - **Light and dark:** Android's quality guidelines ask for both themes, and for web content shown in the app
65
76
  to follow them (stitch2 checks every theme DESIGN.md defines).
66
77
  - **States as layers:** hover 8%, focus 10%, pressed 10%, dragged 16% of the content colour over the
@@ -121,3 +132,6 @@ the rest are judged from the render. Where Google and DESIGN.md disagree, DESIGN
121
132
  - web.dev accessible tap targets: web.dev/articles/accessible-tap-targets
122
133
  - Lighthouse tap targets and font size audits: developer.chrome.com/docs/lighthouse/seo/tap-targets,
123
134
  developer.chrome.com/docs/lighthouse/seo/font-size
135
+ - Material 3 typography, applying type (body, label, line height): m3.material.io/styles/typography/applying-type
136
+ - Material 2 dark theme (text emphasis, no pure white): m2.material.io/design/color/dark-theme.html and
137
+ codelabs.developers.google.com/codelabs/design-material-darktheme/
@@ -42,7 +42,8 @@ short labels never wrap **(check)**.
42
42
  ## Colour and depth
43
43
 
44
44
  - One accent with one meaning ("act here"). Two accent-filled elements in one region: one of them is wrong.
45
- - Greys carry the structure: a few surface steps and two text colours do almost all the work.
45
+ - Greys carry the structure: a few surface steps and two text colours do almost all the work, and the reading copy
46
+ is in the stronger of the two (typography.md, section 3).
46
47
  - Status colours appear as small text, icons or thin bars, never as large fills.
47
48
  - Text contrast at least 4.5:1, 3:1 for large text **(check)**. Secondary text on raised surfaces is where this
48
49
  fails.
@@ -36,6 +36,9 @@ together. A typical app set (sizes for a 390 px phone):
36
36
  - Nothing under 11 px, body text from 12 px, reading text 14–16 px **(check: text-small)**; prose at 45–75
37
37
  characters a line, a max width of about `32em` **(check: measure)**. Material's fifteen-level scale and its sizes are in
38
38
  [google.md](google.md).
39
+ - A meta or caption level (12–13 px) is for counts, dates, units and hints. A sentence a person reads (an
40
+ introduction, a card's description, an answer) takes a body level, 14 px at the smallest: a caption size that
41
+ carries a sentence is the usual cause of a page that reads small.
39
42
 
40
43
  ## 2. Hierarchy recipes
41
44
 
@@ -53,8 +56,34 @@ To make something stand out inside a row, change its weight or colour, not its s
53
56
 
54
57
  ## 3. Text colour
55
58
 
56
- - Primary text for what the user reads first or acts on; secondary for everything else; a third, faint grey
57
- only for disabled text and separators (it usually fails 4.5:1, so it never carries information).
59
+ - Three tiers, by what the text does and not by how it looks: **strong** for headings and values, **body** for reading
60
+ copy, **secondary** for what the eye may skip (hints, captions, counts, dates, labels); a faint grey only for
61
+ disabled text and separators (it usually fails 4.5:1, so it never carries information).
62
+ - Reading copy takes the body colour, whatever its size. On a dark theme a good set is about 95%, 85–90% and 75–80%
63
+ of white for the three tiers (Material 3's two text roles are about 89% and 77%; see [google.md](google.md)); on a
64
+ light theme the same steps in the ink colour.
65
+ - **A contrast ratio does not show a dull page.** Every tier can pass 4.5:1 and a page still read grey, because the
66
+ strokes of small text reach only their own colour: a thin 13 px stroke in a 70% grey is what the eye sees, not the
67
+ ratio. The glyph edges are not the problem either (they render as sharp as a clean page); the colour and the tier
68
+ are. Measure the share of characters in each tier: when most of a page's characters are in the secondary colour,
69
+ or a paragraph of two sentences is, the copy is in the wrong tier. `check` warns about the second case **(check: text-tier)**: a block of
70
+ reading copy (sixty characters and eight words of its own text at 14 px or more, in the page's content and not in a bar,
71
+ table, menu, label, control or link) drawn in DESIGN.md's `text-muted`. It needs that colour in DESIGN.md and is silent without it.
72
+
73
+ ```js
74
+ // characters per computed colour and size, from the rendered page (run it with Playwright's page.evaluate)
75
+ const share = () => {
76
+ const tally = {}; let total = 0;
77
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
78
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
79
+ const text = node.textContent.trim(), element = node.parentElement;
80
+ if (!text || !element.checkVisibility()) continue;
81
+ const style = getComputedStyle(element), key = style.color + ' ' + style.fontSize;
82
+ tally[key] = (tally[key] ?? 0) + text.length; total += text.length;
83
+ }
84
+ return Object.entries(tally).map(([key, count]) => [key, Math.round(100 * count / total) + '%']).sort((a, b) => parseInt(b[1]) - parseInt(a[1]));
85
+ };
86
+ ```
58
87
  - Accent-coloured text means "active" or "act here". Check its contrast on every surface it sits on: accent text
59
88
  on a tinted accent background over a raised surface is a common failure.
60
89
  - Status colours colour short values and words, never sentences.
@@ -110,3 +139,5 @@ Devanagari; `tokens.css` falls back to Noto and system fonts.
110
139
  3. Secondary text differs by colour and weight, not just size.
111
140
  4. Numbers are tabular; large ones proportional; units smaller and secondary.
112
141
  5. At most three levels per region; capitals only in section labels; nothing wraps that should not.
142
+ 6. `check` shows no `text-tier` warning: reading copy is in the body colour and at 14 px or more, and most of the page's
143
+ characters are not in the secondary colour (section 3).
@@ -32,7 +32,9 @@ The generated prose is correct but generic. Make it the product's:
32
32
  1. **Overview**: the product, its users, the mood, in two or three sentences.
33
33
  2. **Colors**: keep the role names (other skills and the components use them); give each a short brand name in
34
34
  the prose if the team has one. Extracted colours are guesses where the page was ambiguous: confirm text,
35
- surfaces and the primary with the user.
35
+ surfaces and the primary with the user. Define three text colours (strong, body, secondary) and write which text
36
+ takes which: reading copy is the body colour, the secondary one is for hints, captions, counts and dates (on a
37
+ dark theme about 95%, 85–90% and 75–80% of white; typography.md, section 3 of the stitch2 skill).
36
38
  3. **Typography**: keep the levels; adjust sizes only together with the user and only on the scale.
37
39
  4. **Components**: name the product's real components (its tab bar items, its core list row) and their states.
38
40
  Each interactive one gets hover, pressed, focus, selected and disabled in the prose, in values ("darkens to