stitch2 0.15.0 → 0.15.2

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
@@ -43,7 +43,7 @@ file that already exists. From there the agent spends its effort on the design.
43
43
  Pick mode (`P`) highlights any element of a screen and copies a reference with its source line, such as
44
44
  `home mobile v1 (design/screens/home/mobile-v1.html:36) <gg-button> “Start Push”`; Shift-click collects
45
45
  several to copy at once or add to the version's note.
46
- - **Checks** (`stitch2 check`): content past the frame edge or out of its card, labels that wrap, text running into
46
+ - **Checks** (`stitch2 check`): content past the frame edge or out of its card, navigation whose links scroll or are cut (in any language), labels that wrap, text running into
47
47
  other text, labels cut short with an ellipsis, content under fixed bars, fixed bars that overlap each other, tap
48
48
  targets (WCAG 2.2), all of it again at both ends of the window widths each layout covers (360 and 599 px for a
49
49
  phone, 1024 and 1920 for desktop, or your own ranges), text contrast in every theme, text that is not one of DESIGN.md's type levels, capitals without tracking,
package/dist/audit.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Layout errors that mean the same bug wherever they appear; compare reports these for the app too. */
2
- export const LAYOUT_ERRORS = ['overflow', 'fixed-overlap', 'control-overlap', 'text-overlap'];
2
+ export const LAYOUT_ERRORS = ['overflow', 'nav-overflow', 'fixed-overlap', 'control-overlap', 'text-overlap'];
3
3
  /** Runs inside the page. Plain JS on purpose: Playwright sends its source to the browser. */
4
4
  export async function audit(opts) {
5
5
  const W = document.documentElement.clientWidth;
@@ -31,6 +31,10 @@ export async function audit(opts) {
31
31
  const cs = getComputedStyle(el);
32
32
  if (cs.display === 'none' || cs.visibility === 'hidden')
33
33
  return false;
34
+ // A closed <details> shows only its summary (a menu's links until it opens).
35
+ const closed = el.closest('details:not([open])');
36
+ if (closed && !closed.querySelector(':scope > summary')?.contains(el))
37
+ return false;
34
38
  const r = el.getBoundingClientRect();
35
39
  return r.width > 0 && r.height > 0;
36
40
  };
@@ -74,6 +78,30 @@ export async function audit(opts) {
74
78
  else
75
79
  push('error', 'overflow', el, `Sticks out past the screen edge (${Math.round(r.right - W)}px)`);
76
80
  }
81
+ // 1b. Navigation fits: a nav's links never hide in a sideways scroll or run past its edge (in a longer language
82
+ // they go into a menu instead). Only chip rows scroll sideways.
83
+ for (const el of all) {
84
+ if (!el.matches('nav, [role=navigation]') && !(el.closest('header') && el.querySelector(':scope > a')))
85
+ continue;
86
+ const ox = getComputedStyle(el).overflowX;
87
+ if (ox === 'visible')
88
+ continue;
89
+ const box = el.getBoundingClientRect();
90
+ const cut = [...el.querySelectorAll('a')].filter((a) => {
91
+ if (!shown(a))
92
+ return false;
93
+ const r = a.getBoundingClientRect();
94
+ // Wholly outside (wrapped away into a menu's overflow) is a choice; partly outside is a cut link.
95
+ const inside = r.left >= box.left - 1 && r.right <= box.right + 1 && r.top >= box.top - 1 && r.bottom <= box.bottom + 1;
96
+ const outside = r.right <= box.left || r.left >= box.right || r.bottom <= box.top || r.top >= box.bottom;
97
+ return !inside && (!outside || ox === 'auto' || ox === 'scroll');
98
+ });
99
+ if (cut.length)
100
+ push('error', 'nav-overflow', el, `Navigation does not fit: ${cut.length} link${cut.length > 1 ? 's' : ''} cut off or scrolled out of view (${cut
101
+ .map(text)
102
+ .slice(0, 3)
103
+ .join(', ')})`);
104
+ }
77
105
  // 2. Short labels (buttons, chips, tabs) stay on one line.
78
106
  const isLabel = (el) => el.matches('button, a, label, [role=tab], [role=button], [role=switch], th') ||
79
107
  /(^|\s)(chip|badge|pill|tag)|rounded-full/.test(String(el.className ?? ''));
package/dist/canvas.html CHANGED
@@ -483,11 +483,15 @@ function zoomTo(f) {
483
483
  apply()
484
484
  }
485
485
 
486
+ /** The tallest a frame is drawn, in CSS px (the same limit as check's renders). */
487
+ const MAX_FRAME_HEIGHT = 20000
488
+
486
489
  function measure(f) {
487
490
  try {
488
491
  const doc = f.iframe.contentDocument
489
492
  const min = bigFrame(f.screen) ? 800 : 844
490
- const h = Math.min(6000, Math.max(min, doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0))
493
+ // As tall as the page, up to MAX_FRAME_HEIGHT: long landing pages are drawn whole.
494
+ const h = Math.min(MAX_FRAME_HEIGHT, Math.max(min, doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0))
491
495
  if (Math.abs(h - f.height) > 2) {
492
496
  f.height = h
493
497
  f.iframe.style.height = `${h}px`
package/dist/check.js CHANGED
@@ -36,6 +36,8 @@ const levels = typeLevels();
36
36
  const themes = designThemes();
37
37
  const collect = collectOptions();
38
38
  const server = await startServer(0);
39
+ /** The tallest render, in CSS px: a long landing page is shot whole (the canvas draws frames up to the same height). */
40
+ const MAX_RENDER_HEIGHT = 20000;
39
41
  const browser = await chromium.launch();
40
42
  const page = await browser.newPage({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2 });
41
43
  const results = {};
@@ -150,7 +152,7 @@ async function setTheme(theme) {
150
152
  /** A page-tall render; renders in another theme or language than the screen's own get a --<theme> suffix. */
151
153
  async function shoot(s, theme, locale, at) {
152
154
  const height = await page.evaluate(() => document.documentElement.scrollHeight);
153
- await page.setViewportSize({ width: s.width, height: Math.max(844, Math.min(height, 6000)) });
155
+ await page.setViewportSize({ width: s.width, height: Math.max(844, Math.min(height, MAX_RENDER_HEIGHT)) });
154
156
  mkdirSync(join(ROOT, 'renders'), { recursive: true });
155
157
  const suffix = theme ?? locale ?? at;
156
158
  const name = s.path.replaceAll('/', '__').replace(/\.html$/, suffix ? `--${suffix}.png` : '.png');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitch2",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
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,6 +68,8 @@ short labels never wrap **(check)**.
68
68
  - The primary action sits in the thumb zone (lower half or a docked bar); destructive actions far from it.
69
69
  - Fixed bars never cover content: the scroll ends with enough bottom padding **(check)**.
70
70
  - Nothing wider than the frame; only chip rows and tab strips scroll sideways **(check)**.
71
+ - Navigation fits in every language: a header shows the links that fit on one row and keeps all of them in a menu;
72
+ a link never scrolls sideways or gets cut **(check: nav-overflow)**.
71
73
 
72
74
  ## Review rubric (step 6)
73
75