@siyahi/tools 0.1.9-next.2 → 0.1.9-next.21

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
@@ -8,17 +8,17 @@ file as copied; `packages/ui/registry.lock.json` lists them all and
8
8
 
9
9
  ## Copied into `@siyahi/ui`
10
10
 
11
- | What | Where in the package | Origin | Licence |
12
- | ------------------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
13
- | shadcn/ui core components (installed with `shadcn add`, never edited) | `src/components/ui/*`, `src/hooks/use-mobile.ts` | https://github.com/shadcn-ui/ui | MIT, Copyright (c) 2023 shadcn |
14
- | shadcn `tailwind.css` (variants and utilities the components use) | `styles/shadcn.css` | https://github.com/shadcn-ui/ui/tree/main/packages/shadcn | MIT, Copyright (c) 2023 shadcn |
15
- | shadcn block `sidebar-07` | `src/components/registry/sidebar-07/*` | https://ui.shadcn.com/r/styles/base-vega/sidebar-07.json | MIT, Copyright (c) 2023 shadcn |
16
- | shadcn block `dashboard-01`, `SectionCards` only | `src/components/registry/dashboard-01/section-cards.tsx` | https://ui.shadcn.com/r/styles/base-vega/dashboard-01.json | MIT, Copyright (c) 2023 shadcn |
17
- | Heatmaps: calendar, weekday by hour, status timeline | `src/components/registry/heatmap/*` | https://shadcn-heatmap.pages.dev/r/calendar-heatmap.json, https://shadcn-heatmap.pages.dev/r/weekday-heatmap.json, https://shadcn-heatmap.pages.dev/r/status-heatmap.json (source: https://github.com/rutopio/shadcn-heatmap) | MIT, Copyright (c) 2026 ChingRu |
18
- | niko-table data table on TanStack Table | `src/components/registry/niko-table/*` | https://niko-table.com/r/data-table.json, https://niko-table.com/r/data-table-core.json, https://niko-table.com/r/data-table-ui.json, https://niko-table.com/r/data-table-faceted-filter.json, https://niko-table.com/r/data-table-column-sort.json, https://niko-table.com/r/data-table-search-filter.json (source: https://github.com/Semkoo/niko-table-registry) | MIT, Copyright (c) 2025 Semir N.; original work Copyright (c) 2025 WebDevSimplified |
19
- | shadcn chart blocks `chart-bar-multiple`, `chart-area-stacked`, `chart-line-multiple` | `src/components/registry/charts/*` | https://ui.shadcn.com/r/styles/new-york-v4/chart-bar-multiple.json, https://ui.shadcn.com/r/styles/new-york-v4/chart-area-stacked.json, https://ui.shadcn.com/r/styles/new-york-v4/chart-line-multiple.json | MIT, Copyright (c) 2023 shadcn |
20
- | dashboardcn ranked list (`BarList`) and stacked bar (`DistributionBar`) | `src/components/registry/dashboardcn/*` | https://dashboardcn.com/r/bar-list.json, https://dashboardcn.com/r/distribution-bar.json (source: https://github.com/NoahGdev/dashboardcn) | MIT, Copyright (c) 2026 dashboardcn contributors |
21
- | MiniDev copy button (`CopyButton`) | `src/components/registry/minidev/copy-button.tsx` | https://ui.minidev.pro/r/copy-button.json (source: https://github.com/amankaushik481/minidev-ui) | MIT, Copyright (c) 2026 MiniDev |
11
+ | What | Where in the package | Origin | Licence |
12
+ | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
13
+ | shadcn/ui core components (installed with `shadcn add`, never edited) | `src/components/ui/*`, `src/hooks/use-mobile.ts` | https://github.com/shadcn-ui/ui | MIT, Copyright (c) 2023 shadcn |
14
+ | shadcn `tailwind.css` (variants and utilities the components use) | `styles/shadcn.css` | https://github.com/shadcn-ui/ui/tree/main/packages/shadcn | MIT, Copyright (c) 2023 shadcn |
15
+ | shadcn block `sidebar-07` | `src/components/registry/sidebar-07/*` | https://ui.shadcn.com/r/styles/base-vega/sidebar-07.json | MIT, Copyright (c) 2023 shadcn |
16
+ | shadcn block `dashboard-01`, `SectionCards` only | `src/components/registry/dashboard-01/section-cards.tsx` | https://ui.shadcn.com/r/styles/base-vega/dashboard-01.json | MIT, Copyright (c) 2023 shadcn |
17
+ | Heatmaps: calendar, weekday by hour, status timeline | `src/components/registry/heatmap/*` | https://shadcn-heatmap.pages.dev/r/calendar-heatmap.json, https://shadcn-heatmap.pages.dev/r/weekday-heatmap.json, https://shadcn-heatmap.pages.dev/r/status-heatmap.json (source: https://github.com/rutopio/shadcn-heatmap) | MIT, Copyright (c) 2026 ChingRu |
18
+ | niko-table data table on TanStack Table | `src/components/registry/niko-table/*` | https://niko-table.com/r/data-table.json, https://niko-table.com/r/data-table-core.json, https://niko-table.com/r/data-table-ui.json, https://niko-table.com/r/data-table-faceted-filter.json, https://niko-table.com/r/data-table-column-sort.json, https://niko-table.com/r/data-table-search-filter.json (source: https://github.com/Semkoo/niko-table-registry) | MIT, Copyright (c) 2025 Semir N.; original work Copyright (c) 2025 WebDevSimplified |
19
+ | shadcn chart blocks `chart-bar-multiple`, `chart-area-stacked`, `chart-line-multiple` | `src/components/registry/charts/*` | https://ui.shadcn.com/r/styles/new-york-v4/chart-bar-multiple.json, https://ui.shadcn.com/r/styles/new-york-v4/chart-area-stacked.json, https://ui.shadcn.com/r/styles/new-york-v4/chart-line-multiple.json | MIT, Copyright (c) 2023 shadcn |
20
+ | dashboardcn ranked list (`BarList`), stacked bar (`DistributionBar`), `Sparkline` and `MetricStrip` | `src/components/registry/dashboardcn/*` | https://dashboardcn.com/r/bar-list.json, https://dashboardcn.com/r/distribution-bar.json, https://dashboardcn.com/r/sparkline.json, https://dashboardcn.com/r/kpi-row-card.json (source: https://github.com/NoahGdev/dashboardcn) | MIT, Copyright (c) 2026 dashboardcn contributors |
21
+ | MiniDev copy button (`CopyButton`) | `src/components/registry/minidev/copy-button.tsx` | https://ui.minidev.pro/r/copy-button.json (source: https://github.com/amankaushik481/minidev-ui) | MIT, Copyright (c) 2026 MiniDev |
22
22
 
23
23
  ## Runtime dependencies with attribution terms
24
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siyahi/tools",
3
- "version": "0.1.9-next.2",
3
+ "version": "0.1.9-next.21",
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 });
@@ -93,7 +100,21 @@ const PROBE = `((chrome) => {
93
100
  };
94
101
  const shown = (el) => {
95
102
  const r = el.getBoundingClientRect();
96
- return r.width > 0 && r.height > 0 && !el.closest(chrome) && getComputedStyle(el).visibility !== "hidden";
103
+ const style = getComputedStyle(el);
104
+ // Base UI keeps clipped controls for form submission; aria-hidden alone
105
+ // does not mean a control (or its descendants) is visually hidden.
106
+ const hiddenControl = el.matches('input[aria-hidden="true"],select[aria-hidden="true"],textarea[aria-hidden="true"]') &&
107
+ (style.clipPath === "inset(50%)" || style.clip === "rect(0px, 0px, 0px, 0px)");
108
+ return r.width > 0 && r.height > 0 && !el.closest(chrome) && style.visibility !== "hidden" && !hiddenControl;
109
+ };
110
+ // Tailwind's sr-only (and a closed dialog's title kept for screen readers) clips to nothing, so it has no place
111
+ // in the gutter; the cut-off check still sees it, as a clipped visible control must not slip through.
112
+ const screenReaderOnly = (el) => {
113
+ for (let node = el; node; node = node.parentElement) {
114
+ const style = getComputedStyle(node);
115
+ if (style.clip === "rect(0px, 0px, 0px, 0px)" || style.clipPath === "inset(50%)") return true;
116
+ }
117
+ return false;
97
118
  };
98
119
  // Pixels of el cut off at the side by an ancestor that hides its overflow,
99
120
  // and how it hides it, up to the nearest one that scrolls, which shows the
@@ -112,7 +133,7 @@ const PROBE = `((chrome) => {
112
133
  };
113
134
  const visible = [...document.querySelectorAll('h1,h2,h3,h4,p,li,td,th,[data-slot="card"]')].filter((el) => {
114
135
  const r = el.getBoundingClientRect();
115
- return shown(el) && r.right > 0 && r.left < de.clientWidth && !scrollsSideways(el);
136
+ return shown(el) && !screenReaderOnly(el) && r.right > 0 && r.left < de.clientWidth && !scrollsSideways(el);
116
137
  });
117
138
  // Badges, card titles and descriptions are spans and divs, which the tag list misses.
118
139
  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"]')]
@@ -129,6 +150,18 @@ const PROBE = `((chrome) => {
129
150
  const overflowWithScrollbar = de.scrollWidth - de.clientWidth;
130
151
  de.style.overflowY = prev;
131
152
  const font = getComputedStyle(document.body).fontFamily;
153
+ const frozen = [];
154
+ for (const table of document.querySelectorAll("table")) {
155
+ let container = table.parentElement;
156
+ while (container && !["auto", "scroll"].includes(getComputedStyle(container).overflowX)) container = container.parentElement;
157
+ if (!container || container.scrollWidth <= container.clientWidth) continue;
158
+ const cells = [...(table.querySelector("tbody tr")?.children ?? [])];
159
+ const pinned = cells.filter((cell) => getComputedStyle(cell).position === "sticky");
160
+ const occupied = pinned.reduce((sum, cell) => sum + cell.getBoundingClientRect().width, 0);
161
+ if (pinned.length < cells.length && occupied > container.clientWidth / 2) {
162
+ frozen.push("frozen columns occupy " + Math.round(occupied) + " of " + container.clientWidth + "px; unpin them in narrow space");
163
+ }
164
+ }
132
165
  const first = font.split(",")[0].trim().replace(/^["']|["']$/g, "");
133
166
  const h1 = document.querySelector("h1");
134
167
  return {
@@ -137,6 +170,7 @@ const PROBE = `((chrome) => {
137
170
  overflow,
138
171
  overflowWithScrollbar,
139
172
  clipped,
173
+ frozen,
140
174
  font: first,
141
175
  fontLoaded: document.fonts.check('16px "' + first + '"') && [...document.fonts].some((f) => f.family.replace(/["']/g, "") === first && f.status === "loaded"),
142
176
  docHeight: de.scrollHeight,
@@ -219,6 +253,7 @@ function problemsOf(p, ready) {
219
253
  if (p.overflowWithScrollbar > 0)
220
254
  problems.push(`scrolls sideways by ${p.overflowWithScrollbar}px once a scrollbar takes its width`);
221
255
  for (const what of p.clipped.slice(0, 5)) problems.push(what);
256
+ problems.push(...p.frozen);
222
257
  if (p.clipped.length > 5) problems.push(`and ${p.clipped.length - 5} more cut off`);
223
258
  if (!p.font.startsWith(values.font)) problems.push(`body font is ${p.font}, not ${values.font}`);
224
259
  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;