@siyahi/tools 0.1.7-next.2 → 0.1.7-next.3

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
@@ -85,7 +85,11 @@ and the fonts are ready, so content a page renders after loading its data is
85
85
  in the shot. A mode fails when nothing matches within 15 seconds; pass a
86
86
  selector that only the finished page has, such as `--wait-for '[data-ready]'`. It fails when visible text or a
87
87
  card comes closer than `--gutter` pixels to either edge, when the page scrolls
88
- sideways, or when the body font is not a loaded `--font` family. Pass
88
+ sideways, when text or a control (a heading, paragraph, cell, link, button or
89
+ field) is cut off at the side by a box that hides its overflow, such as a card,
90
+ or when the body font is not a loaded `--font` family. The cut-off check names
91
+ each element and the pixels it loses, `button "Monthly return" by 56px`; the
92
+ page never scrolls sideways in that case, so the scroll check cannot see it. Pass
89
93
  `--report` for a static HTML report on `report.css`, which loads no font file
90
94
  by design: the body must still name `--font` first, but the viewer's installed
91
95
  font or the system fallback may draw it, and the run prints
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siyahi/tools",
3
- "version": "0.1.7-next.2",
3
+ "version": "0.1.7-next.3",
4
4
  "description": "Checks and build helpers for Siyahi pages: a lint plugin, a font preload plugin, a theme contrast check, and screenshot and layout shift scripts.",
5
5
  "homepage": "https://siyahi.jatinbansal.com",
6
6
  "license": "MIT",
package/shots.mjs CHANGED
@@ -15,7 +15,9 @@
15
15
  //
16
16
  // Checks: a gutter of at least --gutter px on both sides of visible text and
17
17
  // cards, no sideways document scroll (also with a classic scrollbar taking its
18
- // width), and --font as the loaded body font. --report is for static HTML
18
+ // width), no text or control cut off at the side by a box that hides its
19
+ // overflow (a card does; the document scroll check cannot see what it cuts),
20
+ // and --font as the loaded body font. --report is for static HTML
19
21
  // reports, which load no font file by design: the body must still name --font
20
22
  // first, but the viewer's installed font or the system fallback may draw it.
21
23
  // Exempt from the gutter: content
@@ -35,7 +37,7 @@ import { parseArgs } from "node:util";
35
37
  import { chromium, errors } from "playwright";
36
38
 
37
39
  /** @typedef {{ name: string, width: number, height: number, dark: boolean }} Mode */
38
- /** @typedef {{ gutterLeft: number | null, gutterRight: number | null, overflow: number, overflowWithScrollbar: number, font: string, fontLoaded: boolean, docHeight: number, h1: string | null }} Probe */
40
+ /** @typedef {{ gutterLeft: number | null, gutterRight: number | null, overflow: number, overflowWithScrollbar: number, clipped: string[], font: string, fontLoaded: boolean, docHeight: number, h1: string | null }} Probe */
39
41
 
40
42
  /** @type {Mode[]} */
41
43
  const MODES = [
@@ -89,14 +91,35 @@ const PROBE = `((chrome) => {
89
91
  }
90
92
  return false;
91
93
  };
94
+ const shown = (el) => {
95
+ const r = el.getBoundingClientRect();
96
+ return r.width > 0 && r.height > 0 && !el.closest(chrome) && getComputedStyle(el).visibility !== "hidden";
97
+ };
98
+ // Pixels of el cut off at the side by an ancestor that hides its overflow,
99
+ // and how it hides it, up to the nearest one that scrolls, which shows the
100
+ // rest on scroll.
101
+ const cutOff = (el) => {
102
+ const r = el.getBoundingClientRect();
103
+ for (let a = el.parentElement; a; a = a.parentElement) {
104
+ const o = getComputedStyle(a).overflowX;
105
+ if (o === "auto" || o === "scroll") return 0;
106
+ if (o !== "hidden" && o !== "clip") continue;
107
+ const left = a.getBoundingClientRect().left + a.clientLeft;
108
+ const cut = Math.max(left - r.left, r.right - (left + a.clientWidth));
109
+ if (cut >= 1) return { px: Math.round(cut), overflow: o };
110
+ }
111
+ return null;
112
+ };
92
113
  const visible = [...document.querySelectorAll('h1,h2,h3,h4,p,li,td,th,[data-slot="card"]')].filter((el) => {
93
114
  const r = el.getBoundingClientRect();
94
- if (r.width === 0 || r.height === 0) return false;
95
- if (r.right <= 0 || r.left >= de.clientWidth) return false;
96
- if (el.closest(chrome)) return false;
97
- const s = getComputedStyle(el);
98
- return s.visibility !== "hidden" && !scrollsSideways(el);
115
+ return shown(el) && r.right > 0 && r.left < de.clientWidth && !scrollsSideways(el);
99
116
  });
117
+ // Badges, card titles and descriptions are spans and divs, which the tag list misses.
118
+ const clipped = [...document.querySelectorAll('h1,h2,h3,h4,p,li,td,th,a,button,input,select,textarea,label,[data-slot="badge"],[data-slot="card-title"],[data-slot="card-description"]')]
119
+ .filter(shown)
120
+ .map((el) => ({ el, cut: cutOff(el) }))
121
+ .filter(({ cut }) => cut)
122
+ .map(({ el, cut }) => el.tagName.toLowerCase() + " " + JSON.stringify(el.textContent.trim().slice(0, 40)) + " by " + cut.px + "px is cut off by an overflow: " + cut.overflow + " box");
100
123
  const lefts = visible.map((el) => el.getBoundingClientRect().left);
101
124
  const rights = visible.map((el) => de.clientWidth - el.getBoundingClientRect().right);
102
125
  const overflow = de.scrollWidth - de.clientWidth;
@@ -113,6 +136,7 @@ const PROBE = `((chrome) => {
113
136
  gutterRight: rights.length ? Math.round(Math.min(...rights)) : null,
114
137
  overflow,
115
138
  overflowWithScrollbar,
139
+ clipped,
116
140
  font: first,
117
141
  fontLoaded: document.fonts.check('16px "' + first + '"') && [...document.fonts].some((f) => f.family.replace(/["']/g, "") === first && f.status === "loaded"),
118
142
  docHeight: de.scrollHeight,
@@ -194,6 +218,8 @@ function problemsOf(p, ready) {
194
218
  if (p.overflow > 0) problems.push(`document scrolls sideways by ${p.overflow}px`);
195
219
  if (p.overflowWithScrollbar > 0)
196
220
  problems.push(`scrolls sideways by ${p.overflowWithScrollbar}px once a scrollbar takes its width`);
221
+ for (const what of p.clipped.slice(0, 5)) problems.push(what);
222
+ if (p.clipped.length > 5) problems.push(`and ${p.clipped.length - 5} more cut off`);
197
223
  if (!p.font.startsWith(values.font)) problems.push(`body font is ${p.font}, not ${values.font}`);
198
224
  else if (!p.fontLoaded && !values.report) problems.push(`${p.font} is named but never loaded`);
199
225
  return problems;
@@ -209,7 +235,7 @@ for (const url of positionals) {
209
235
  const font = values.report && !probe.fontLoaded ? `${probe.font} (report, system fallback allowed)` : probe.font;
210
236
  console.log(
211
237
  `${problems.length ? "FAIL" : "ok "} ${mode.name.padEnd(10)} gutter=${probe.gutterLeft}/${probe.gutterRight} ` +
212
- `overflow=${probe.overflow}/${probe.overflowWithScrollbar} font=${font} h1=${JSON.stringify(probe.h1)} ` +
238
+ `overflow=${probe.overflow}/${probe.overflowWithScrollbar} clipped=${probe.clipped.length} font=${font} h1=${JSON.stringify(probe.h1)} ` +
213
239
  `height=${height} ${files.join(" ")}`,
214
240
  );
215
241
  for (const problem of problems) console.log(` ${problem}`);
package/types/shots.d.mts CHANGED
@@ -10,6 +10,7 @@ export type Probe = {
10
10
  gutterRight: number | null;
11
11
  overflow: number;
12
12
  overflowWithScrollbar: number;
13
+ clipped: string[];
13
14
  font: string;
14
15
  fontLoaded: boolean;
15
16
  docHeight: number;