@siyahi/tools 0.1.9-next.17 → 0.1.9-next.18

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
@@ -8,6 +8,10 @@ Kept apart from the UI package so installing the UI does not pull in Playwright.
8
8
  An [oxlint](https://oxc.rs) JS plugin, `siyahi`. Every rule runs only in files
9
9
  that import `@siyahi/ui`.
10
10
 
11
+ ```sh
12
+ npm install --save-dev @siyahi/tools@next oxlint
13
+ ```
14
+
11
15
  | Rule | Rejects |
12
16
  | ----------------------------- | ------------------------------------------------------------------------------------------------------------------- |
13
17
  | `siyahi/no-color-literal` | hex, `rgb()`, `hsl()`, `oklch()` and other colour literals in strings |
@@ -21,7 +25,14 @@ that import `@siyahi/ui`.
21
25
  {
22
26
  "jsPlugins": ["@siyahi/tools/lint"],
23
27
  "settings": { "shadcn": { "ui": "@siyahi/ui", "themeImports": ["^@siyahi/ui/"] } },
24
- "rules": { "siyahi/no-color-literal": "error" }
28
+ "rules": {
29
+ "siyahi/no-color-literal": "error",
30
+ "siyahi/no-scheme-variant": "error",
31
+ "siyahi/no-numeric-font-size": "error",
32
+ "siyahi/no-chart-theme-map": "error",
33
+ "siyahi/no-raw-colors": "error",
34
+ "siyahi/no-arbitrary-values": "error"
35
+ }
25
36
  }
26
37
  ```
27
38
 
@@ -45,6 +56,12 @@ export default defineConfig({ plugins: [react(), tailwindcss(), preloadFonts()]
45
56
  It runs in `vite build` only. The fonts stay self-hosted: the tags point at
46
57
  the files Vite emitted from `@siyahi/ui`'s fontsource imports.
47
58
 
59
+ The theme's fallback fonts match Geist's metrics until it loads: Arial or its
60
+ Linux equivalents for body text, Courier New for mono. If an app renders its
61
+ own HTML, add preload tags for the build's hashed URLs of
62
+ `geist-latin-wght-normal.woff2`, `geist-latin-ext-wght-normal.woff2` and
63
+ `geist-mono-latin-wght-normal.woff2`.
64
+
48
65
  ## siyahi-cls
49
66
 
50
67
  ```sh
@@ -70,7 +87,7 @@ counted (`moved=` against `in-view=`) and listed, but do not fail it. Run `npx p
70
87
  ## siyahi-shots
71
88
 
72
89
  ```sh
73
- npx siyahi-shots http://localhost:5173/#/page [more URLs] [--out shots] [--gutter 16] [--font Geist] [--chrome '[data-slot="sidebar"]'] [--wait-for h1] [--report]
90
+ npx siyahi-shots http://localhost:5173/#/page [more URLs] [--out shots] [--gutter 16] [--font Geist] [--chrome '[data-slot="sidebar"]'] [--wait-for h1] [--report] [--widths 1920,390]
74
91
  ```
75
92
 
76
93
  Renders each URL in Chromium at 1920 and 390 pixels wide, light and dark, and
@@ -100,6 +117,12 @@ anything inside `--chrome` (by default the shadcn sidebar, which sits flush to
100
117
  the edge by design). The font default, Geist, is Siyahi's; pass another family
101
118
  for other pages. Run `npx playwright install chromium` once.
102
119
 
120
+ Pass `--widths 320,390,768,1366,1920` to include small phones, tablets and laptops;
121
+ each width runs in light and dark. The check also rejects a scrolling table whose
122
+ frozen columns occupy more than half its available width. These captures
123
+ complement interaction checks for scrolling, state retention, keyboard navigation
124
+ and touch readouts.
125
+
103
126
  ## siyahi-contrast
104
127
 
105
128
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siyahi/tools",
3
- "version": "0.1.9-next.17",
3
+ "version": "0.1.9-next.18",
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
@@ -37,15 +37,7 @@ import { parseArgs } from "node:util";
37
37
  import { chromium, errors } from "playwright";
38
38
 
39
39
  /** @typedef {{ name: string, width: number, height: number, dark: boolean }} Mode */
40
- /** @typedef {{ gutterLeft: number | null, gutterRight: number | null, overflow: number, overflowWithScrollbar: number, clipped: string[], font: string, fontLoaded: boolean, docHeight: number, h1: string | null }} Probe */
41
-
42
- /** @type {Mode[]} */
43
- const MODES = [
44
- { name: "1920-light", width: 1920, height: 1080, dark: false },
45
- { name: "1920-dark", width: 1920, height: 1080, dark: true },
46
- { name: "390-light", width: 390, height: 844, dark: false },
47
- { name: "390-dark", width: 390, height: 844, dark: true },
48
- ];
40
+ /** @typedef {{ gutterLeft: number | null, gutterRight: number | null, overflow: number, overflowWithScrollbar: number, clipped: string[], frozen: string[], font: string, fontLoaded: boolean, docHeight: number, h1: string | null }} Probe */
49
41
  // How long --wait-for may take to appear before the mode fails.
50
42
  const WAIT_FOR_TIMEOUT = 15_000;
51
43
  // Tallest PNG written. Headless Chromium renders taller pages, but many image
@@ -61,15 +53,30 @@ const { values, positionals } = parseArgs({
61
53
  chrome: { type: "string", default: '[data-slot="sidebar"]' },
62
54
  "wait-for": { type: "string", default: "h1" },
63
55
  report: { type: "boolean", default: false },
56
+ widths: { type: "string", default: "1920,390" },
64
57
  },
65
58
  });
66
59
  if (positionals.length === 0) {
67
60
  console.error(
68
- "usage: siyahi-shots <url>... [--out shots] [--gutter 16] [--font Geist] [--chrome <selector>] [--wait-for <selector>] [--report]",
61
+ "usage: siyahi-shots <url>... [--out shots] [--gutter 16] [--font Geist] [--chrome <selector>] [--wait-for <selector>] [--report] [--widths 1920,390]",
69
62
  );
70
63
  process.exit(2);
71
64
  }
72
65
  const outDir = path.resolve(values.out);
66
+ const widths = values.widths.split(",").map(Number);
67
+ if (widths.some((width) => !Number.isInteger(width) || width < 240 || width > 3840)) {
68
+ console.error("--widths must be comma-separated pixel widths between 240 and 3840");
69
+ process.exit(2);
70
+ }
71
+ /** @type {Mode[]} */
72
+ const MODES = [...new Set(widths)].flatMap((width) =>
73
+ [false, true].map((dark) => ({
74
+ name: `${width}-${dark ? "dark" : "light"}`,
75
+ width,
76
+ height: width < 768 ? 844 : 1080,
77
+ dark,
78
+ })),
79
+ );
73
80
  const minGutter = Number(values.gutter);
74
81
  const waitFor = values["wait-for"];
75
82
  mkdirSync(outDir, { recursive: true });
@@ -134,6 +141,18 @@ const PROBE = `((chrome) => {
134
141
  const overflowWithScrollbar = de.scrollWidth - de.clientWidth;
135
142
  de.style.overflowY = prev;
136
143
  const font = getComputedStyle(document.body).fontFamily;
144
+ const frozen = [];
145
+ for (const table of document.querySelectorAll("table")) {
146
+ let container = table.parentElement;
147
+ while (container && !["auto", "scroll"].includes(getComputedStyle(container).overflowX)) container = container.parentElement;
148
+ if (!container || container.scrollWidth <= container.clientWidth) continue;
149
+ const cells = [...(table.querySelector("tbody tr")?.children ?? [])];
150
+ const pinned = cells.filter((cell) => getComputedStyle(cell).position === "sticky");
151
+ const occupied = pinned.reduce((sum, cell) => sum + cell.getBoundingClientRect().width, 0);
152
+ if (pinned.length < cells.length && occupied > container.clientWidth / 2) {
153
+ frozen.push("frozen columns occupy " + Math.round(occupied) + " of " + container.clientWidth + "px; unpin them in narrow space");
154
+ }
155
+ }
137
156
  const first = font.split(",")[0].trim().replace(/^["']|["']$/g, "");
138
157
  const h1 = document.querySelector("h1");
139
158
  return {
@@ -142,6 +161,7 @@ const PROBE = `((chrome) => {
142
161
  overflow,
143
162
  overflowWithScrollbar,
144
163
  clipped,
164
+ frozen,
145
165
  font: first,
146
166
  fontLoaded: document.fonts.check('16px "' + first + '"') && [...document.fonts].some((f) => f.family.replace(/["']/g, "") === first && f.status === "loaded"),
147
167
  docHeight: de.scrollHeight,
@@ -224,6 +244,7 @@ function problemsOf(p, ready) {
224
244
  if (p.overflowWithScrollbar > 0)
225
245
  problems.push(`scrolls sideways by ${p.overflowWithScrollbar}px once a scrollbar takes its width`);
226
246
  for (const what of p.clipped.slice(0, 5)) problems.push(what);
247
+ problems.push(...p.frozen);
227
248
  if (p.clipped.length > 5) problems.push(`and ${p.clipped.length - 5} more cut off`);
228
249
  if (!p.font.startsWith(values.font)) problems.push(`body font is ${p.font}, not ${values.font}`);
229
250
  else if (!p.fontLoaded && !values.report) problems.push(`${p.font} is named but never loaded`);
package/types/shots.d.mts CHANGED
@@ -11,6 +11,7 @@ export type Probe = {
11
11
  overflow: number;
12
12
  overflowWithScrollbar: number;
13
13
  clipped: string[];
14
+ frozen: string[];
14
15
  font: string;
15
16
  fontLoaded: boolean;
16
17
  docHeight: number;