@krapwoo/ds-viewer 0.11.0 → 0.12.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 +13 -1
- package/dist/cli/render.js +93 -0
- package/native/catalog/SectionBlock.tsx +10 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -695,10 +695,22 @@ and adds what only a real render shows:
|
|
|
695
695
|
cells are not visited at all. Usually fixed with a wider `specimenSize`, by stacking the blocks so
|
|
696
696
|
the example gets the full row, or by dropping `itemsFill` from a small instance that should sit
|
|
697
697
|
centered.
|
|
698
|
+
- `page-head-overlap` (error): something is painting on top of a page's own heading. This is the
|
|
699
|
+
one check measured at **800x600** — Puppeteer's default viewport, and so the size a smoke check
|
|
700
|
+
judges your catalog at — because the overlap it catches does not appear at 1280 at all: a page
|
|
701
|
+
title that is a single unbroken identifier (a long Swift type name, say) has no break
|
|
702
|
+
opportunity for the browser to use, so it keeps its full intrinsic width and paints past its own
|
|
703
|
+
column into the previous/next buttons. It measures every leaf element carrying text, clips each
|
|
704
|
+
rect to any ancestor that scrolls or clips (so something scrolled out of view is not reported as
|
|
705
|
+
covering anything), and reports a pair only when it overlaps the heading by more than 1px in
|
|
706
|
+
both directions, so a shared edge between neighbours is not a finding. Only the heading is
|
|
707
|
+
checked: specimens legitimately stack text, and reporting those would bury the one overlap that
|
|
708
|
+
is always a defect.
|
|
698
709
|
- `render-unavailable` / `render-failed` (errors): no browser, or the catalog didn't start.
|
|
699
710
|
|
|
700
711
|
It needs `puppeteer` in your project (`npm install --save-dev puppeteer`); ds-viewer doesn't
|
|
701
|
-
install a browser. It takes about two minutes for 50 pages
|
|
712
|
+
install a browser. It takes about two minutes for 50 pages — plus a moment per page for the
|
|
713
|
+
narrow-viewport heading pass — and works with `--json` (progress
|
|
702
714
|
goes to stderr), `--ci` and `doctor.strict`, so CI can run `npx ds-viewer doctor --render --ci`.
|
|
703
715
|
|
|
704
716
|
It starts its own catalog on the first free port from 5181. `--port <n>` pins it instead, which
|
package/dist/cli/render.js
CHANGED
|
@@ -121,6 +121,83 @@ export function identicalVariantIssues(page, pairs) {
|
|
|
121
121
|
fix: "Make the difference visible — a wider specimenSize when the cell is too narrow to show it, or the prop the name claims — or drop one of the two examples.",
|
|
122
122
|
}));
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* The width the heading check measures at. 800x600 is Puppeteer's own default viewport, which is
|
|
126
|
+
* what a consumer's smoke check uses unless it says otherwise, so it is the width their catalog is
|
|
127
|
+
* judged at. Every other check here runs at 1280x800, where this class of overlap does not appear
|
|
128
|
+
* at all — which is exactly why issue #51 reached a release.
|
|
129
|
+
*/
|
|
130
|
+
export const NARROW_VIEWPORT = { width: 800, height: 600 };
|
|
131
|
+
/**
|
|
132
|
+
* Runs in the browser: finds anything painting on top of the page's own heading.
|
|
133
|
+
*
|
|
134
|
+
* The page head puts the title in a `flex: 1` column beside a non-shrinking pager row. A title
|
|
135
|
+
* that is one unbroken identifier — a Swift type name, say — has no space for the browser's
|
|
136
|
+
* default line breaking to use, so without an explicit break opportunity it keeps its full
|
|
137
|
+
* intrinsic width and paints straight through the column into the pager (issue #51:
|
|
138
|
+
* `TranscriptMediaUnavailableChip` covered the `←` button by 14x22 at an 800px window). The
|
|
139
|
+
* reader sees the title and the control drawn over each other, and the title is the page's own
|
|
140
|
+
* heading, so it is the one thing on the page that must stay readable.
|
|
141
|
+
*
|
|
142
|
+
* Measured the way a consumer's own smoke check measures it: every leaf element that carries
|
|
143
|
+
* text, its rect clipped to each ancestor that handles its own overflow (so a scrolled-away rect
|
|
144
|
+
* is not reported as covering anything), and a pair counted only when it overlaps by more than
|
|
145
|
+
* 1px in BOTH directions — a shared edge or a single-pixel seam between neighbours is not an
|
|
146
|
+
* overlap.
|
|
147
|
+
*
|
|
148
|
+
* Only the heading is checked, not every pair on the page. Host specimens legitimately stack text
|
|
149
|
+
* — a badge over a thumbnail, a caption over a surface — and reporting those would bury the one
|
|
150
|
+
* overlap that is always a defect. The heading's own descendants are skipped for the same reason:
|
|
151
|
+
* a heading's `<Text>` child sits inside its `<View>` by construction.
|
|
152
|
+
*/
|
|
153
|
+
export const HEAD_OVERLAP_SCRIPT = `(() => {
|
|
154
|
+
const doc = document;
|
|
155
|
+
const CONTAINS = /^(auto|scroll|hidden|clip)$/;
|
|
156
|
+
const heading = doc.querySelector('[role="heading"]');
|
|
157
|
+
if (!heading) return [];
|
|
158
|
+
const clip = (el) => {
|
|
159
|
+
const r = el.getBoundingClientRect();
|
|
160
|
+
let x1 = r.left, y1 = r.top, x2 = r.right, y2 = r.bottom;
|
|
161
|
+
for (let n = el.parentElement; n; n = n.parentElement) {
|
|
162
|
+
const cs = getComputedStyle(n);
|
|
163
|
+
if (CONTAINS.test(cs.overflowX) || CONTAINS.test(cs.overflowY)) {
|
|
164
|
+
const b = n.getBoundingClientRect();
|
|
165
|
+
if (b.left > x1) x1 = b.left;
|
|
166
|
+
if (b.top > y1) y1 = b.top;
|
|
167
|
+
if (b.right < x2) x2 = b.right;
|
|
168
|
+
if (b.bottom < y2) y2 = b.bottom;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { x1: x1, y1: y1, x2: x2, y2: y2 };
|
|
172
|
+
};
|
|
173
|
+
const head = clip(heading);
|
|
174
|
+
if (head.x2 - head.x1 <= 0 || head.y2 - head.y1 <= 0) return [];
|
|
175
|
+
const out = [];
|
|
176
|
+
for (const el of doc.querySelectorAll('*')) {
|
|
177
|
+
if (el.children.length) continue;
|
|
178
|
+
if (el === heading || heading.contains(el)) continue;
|
|
179
|
+
const text = (el.textContent || '').trim();
|
|
180
|
+
if (!text) continue;
|
|
181
|
+
const r = el.getBoundingClientRect();
|
|
182
|
+
if (r.width <= 0 || r.height <= 0) continue;
|
|
183
|
+
const b = clip(el);
|
|
184
|
+
if (b.x2 - b.x1 <= 0 || b.y2 - b.y1 <= 0) continue;
|
|
185
|
+
const w = Math.min(head.x2, b.x2) - Math.max(head.x1, b.x1);
|
|
186
|
+
const h = Math.min(head.y2, b.y2) - Math.max(head.y1, b.y1);
|
|
187
|
+
if (w > 1 && h > 1) out.push({ other: text.slice(0, 40), w: Math.round(w), h: Math.round(h) });
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
})()`;
|
|
191
|
+
export function headOverlapIssues(ref, overlaps) {
|
|
192
|
+
return overlaps.map((o) => ({
|
|
193
|
+
id: 'page-head-overlap',
|
|
194
|
+
severity: 'error',
|
|
195
|
+
page: ref.id,
|
|
196
|
+
...(ref.file ? { file: ref.file } : {}),
|
|
197
|
+
message: `At ${NARROW_VIEWPORT.width}px the page heading is covered by "${o.other}" over ${o.w}x${o.h}px.`,
|
|
198
|
+
fix: 'Let the heading wrap or truncate inside its own column, or move or clip whatever paints over it.',
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
124
201
|
/**
|
|
125
202
|
* Runs in the browser: finds specimen content that paints outside its cell and cannot be reached.
|
|
126
203
|
*
|
|
@@ -213,6 +290,8 @@ const DEV_START_TIMEOUT_MS = 120_000;
|
|
|
213
290
|
const FIRST_LOAD_TIMEOUT_MS = 300_000;
|
|
214
291
|
const PAGE_TIMEOUT_MS = 120_000;
|
|
215
292
|
const SETTLE_MS = 900;
|
|
293
|
+
/** After a viewport change, long enough for react-native-web to reflow before anything is measured. */
|
|
294
|
+
const RESIZE_SETTLE_MS = 250;
|
|
216
295
|
/** Stops `dev` and everything it started (Expo/Metro): the whole process tree on Windows, the
|
|
217
296
|
* whole process group elsewhere (Expo stops through dev's own signal handler). Releases the pipes
|
|
218
297
|
* so this process can exit. `os` is injectable for tests. */
|
|
@@ -436,6 +515,20 @@ export async function renderCheck(projectRoot, options = {}) {
|
|
|
436
515
|
}
|
|
437
516
|
if (Array.isArray(clipped))
|
|
438
517
|
issues.push(...clippedSpecimenIssues({ id, file: options.files?.get(id) }, clipped));
|
|
518
|
+
// Last, because it is the only check that changes the viewport: the overlap it guards
|
|
519
|
+
// appears only at a narrow width, and every check above is calibrated at 1280. Advisory
|
|
520
|
+
// like the two passes above — a failing script must not cost this page its other findings.
|
|
521
|
+
let headOverlaps;
|
|
522
|
+
try {
|
|
523
|
+
await page.setViewport({ width: NARROW_VIEWPORT.width, height: NARROW_VIEWPORT.height });
|
|
524
|
+
await new Promise((r) => setTimeout(r, RESIZE_SETTLE_MS));
|
|
525
|
+
headOverlaps = (await page.evaluate(HEAD_OVERLAP_SCRIPT));
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
headOverlaps = undefined;
|
|
529
|
+
}
|
|
530
|
+
if (Array.isArray(headOverlaps))
|
|
531
|
+
issues.push(...headOverlapIssues({ id, file: options.files?.get(id) }, headOverlaps));
|
|
439
532
|
await page.close();
|
|
440
533
|
}
|
|
441
534
|
catch (error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { View, Text, Pressable, StyleSheet, type LayoutChangeEvent } from 'react-native';
|
|
2
|
+
import { View, Text, Pressable, StyleSheet, type LayoutChangeEvent, type TextStyle } from 'react-native';
|
|
3
3
|
import { CATALOG_COLOR, CATALOG_LAYOUT, CATALOG_RADIUS, CATALOG_SPACE, CATALOG_TEXT } from './tokens';
|
|
4
4
|
import { ComparisonGrid } from './ComparisonGrid';
|
|
5
5
|
import { ComparisonGroups } from './ComparisonGroups';
|
|
@@ -391,7 +391,15 @@ const styles = StyleSheet.create({
|
|
|
391
391
|
backgroundColor: CATALOG_COLOR.surface,
|
|
392
392
|
},
|
|
393
393
|
osBadgeText: { ...CATALOG_TEXT.microLabel, color: CATALOG_COLOR.text },
|
|
394
|
-
|
|
394
|
+
// `wordBreak` isn't declared on React Native's own `TextStyle` (react-native-web's own
|
|
395
|
+
// `Text/types.js` declares it; a browser applies any valid CSS property name it recognizes
|
|
396
|
+
// regardless — same caveat ReferenceDetails.tsx's own `mono` style documents), so this is cast
|
|
397
|
+
// the same way. Without it, an unbroken page title (a component identifier with no spaces,
|
|
398
|
+
// e.g. `TranscriptVideoPreviewPlayerView`) has no space for the browser's default line-breaking
|
|
399
|
+
// to use: it stays one unbroken run at its full intrinsic width — wide enough to push past
|
|
400
|
+
// `titleText`'s own column and into the pager — instead of wrapping inside the column like any
|
|
401
|
+
// title with spaces already does.
|
|
402
|
+
title: { ...CATALOG_TEXT.pageTitle, color: CATALOG_COLOR.text, marginBottom: 5, wordBreak: 'break-word' } as TextStyle,
|
|
395
403
|
desc: { ...CATALOG_TEXT.body, color: CATALOG_COLOR.textMuted, maxWidth: 700 },
|
|
396
404
|
pager: { flexDirection: 'row', gap: 6, flexShrink: 0 },
|
|
397
405
|
pagerButton: {
|