chore: update refs audit

This commit is contained in:
2026-08-28 21:27:13 +05:00
parent 0d95acba89
commit 795c03ac87
7 changed files with 4519 additions and 4242 deletions
+3408 -1426
View File
File diff suppressed because it is too large Load Diff
+681 -13
View File
@@ -1,17 +1,22 @@
# Style audit: http://127.0.0.1:5179/preview/demo
# Style audit (multi-route)
vs C:\+XBOCTuK\+life_projects\easy-png-tools\refs-html\demo.html
_2026-08-28T14:54:53.564Z_
_2026-08-28T15:27:27.893Z_
## Сводка
- Токены light: **10** расх.
- Токены dark: **16** расх.
- Элементы (стиль/геометрия): **47** расх.
- Только у нас: 18, только в рефе: 11
| route | ref | tokL | tokD | els | onlyOurs | onlyRef |
|---|---|---|---|---|---|---|
| `/preview/demo` | demo.html | 10 | 16 | 47 | 18 | 11 |
| `/preview/list-tools` | list-tools.html | 10 | 16 | 30 | 257 | 54 |
| `/preview/tools/linear-gradient-png` | gradient.html | 10 | 16 | 7 | 111 | 30 |
| `/preview/tools/remove-background-png` | background-remover.html | 10 | 16 | 8 | 110 | 28 |
## Токены — Light
## /preview/demo (vs demo.html)
- Токены light: **10**, dark: **16**
- Элементы: **47**, только у нас: 18, только в рефе: 11
### Токены — Light
| token | ours | ref |
|---|---|---|
@@ -26,7 +31,7 @@ _2026-08-28T14:54:53.564Z_
| `--radius-lg` | — | .5rem |
| `--text-sm` | — | .875rem |
## Токены — Dark
### Токены — Dark
| token | ours | ref |
|---|---|---|
@@ -47,7 +52,7 @@ _2026-08-28T14:54:53.564Z_
| `--radius-lg` | — | .5rem |
| `--text-sm` | — | .875rem |
## Расхождения элементов (топ 47)
### Расхождения элементов (топ 47)
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>div:nth-child(1)>div:nth-child(1)>div:nth-child(1)>div:nth-child(1)>div:nth-child(1)>span:nth-child(1)` — "/"
- **fontFamily**: `"IBM Plex Mono", monospace``"IBM Plex Sans", sans-serif`
@@ -259,7 +264,7 @@ _2026-08-28T14:54:53.564Z_
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>div:nth-child(2)>div:nth-child(1)>p:nth-child(3)` — "Output is generated in-browser. Your fil"
- **rect**: `799,2489 565x17``799,1425 565x17`
## Только у нас (структурно)
### Только у нас
- `/ DEMO`
- `+ Add tool`
@@ -280,7 +285,7 @@ _2026-08-28T14:54:53.564Z_
- `v0.1.0`
- `© 2026`
## Только в рефе (структурно)
### Только в рефе
- `/
WORKSPACE`
@@ -297,3 +302,666 @@ _2026-08-28T14:54:53.564Z_
px`
- `v2.4.0`
- `© 2024`
## /preview/list-tools (vs list-tools.html)
- Токены light: **10**, dark: **16**
- Элементы: **30**, только у нас: 257, только в рефе: 54
### Токены — Light
| token | ours | ref |
|---|---|---|
| `--danger` | #e5484d | — |
| `--success` | #25a96a | — |
| `--text-sm--line-height` | — | calc(1.25 / .875) |
| `--text-xs--line-height` | — | calc(1 / .75) |
| `--font-weight-medium` | — | 500 |
| `--spacing` | — | .25rem |
| `--text-xs` | — | .75rem |
| `--radius-md` | — | .375rem |
| `--radius-lg` | — | .5rem |
| `--text-sm` | — | .875rem |
### Токены — Dark
| token | ours | ref |
|---|---|---|
| `--panel` | #182129 | #f8fafb |
| `--blue` | #54a2ff | #1769d2 |
| `--muted` | #91a0ac | #6d7883 |
| `--background` | #11171d | #eef1f4 |
| `--danger` | #ff7479 | — |
| `--foreground` | #e8eef2 | #17212b |
| `--success` | #25a96a | — |
| `--line` | #33414c | #cbd3da |
| `--text-sm--line-height` | — | calc(1.25 / .875) |
| `--text-xs--line-height` | — | calc(1 / .75) |
| `--font-weight-medium` | — | 500 |
| `--spacing` | — | .25rem |
| `--text-xs` | — | .75rem |
| `--radius-md` | — | .375rem |
| `--radius-lg` | — | .5rem |
| `--text-sm` | — | .875rem |
### Расхождения элементов (топ 30)
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(2)` — "Catalog"
- **fontSize**: `12px` → `10px`
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
- **letterSpacing**: `0.48px` → `normal`
- **rect**: `621,24 54x15` → `832,26 39x12`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(1)` — "Workspace"
- **fontSize**: `12px` → `10px`
- **letterSpacing**: `0.48px` → `normal`
- **rect**: `534,24 69x15` → `761,26 49x12`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(3)` — "Gradient"
- **fontSize**: `12px` → `10px`
- **letterSpacing**: `0.48px` → `normal`
- **rect**: `693,24 61x15` → `893,26 44x12`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(4)` — "Background remover"
- **fontSize**: `12px` → `10px`
- **letterSpacing**: `0.48px` → `normal`
- **rect**: `773,24 138x15` → `959,26 99x12`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(2)>div:nth-child(2)>a:nth-child(1)>span:nth-child(2)>span:nth-child(1)` — "Convert JPG to PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `104,302 175x19` → `131,494 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(2)>div:nth-child(2)>a:nth-child(2)>span:nth-child(2)>span:nth-child(1)` — "Convert WebP to PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `364,311 175x19` → `131,610 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(2)>div:nth-child(2)>a:nth-child(7)>span:nth-child(2)>span:nth-child(1)` — "PNG to Base64"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `364,429 175x19` → `131,726 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(2)>div:nth-child(2)>a:nth-child(9)>span:nth-child(2)>span:nth-child(1)` — "PNG to Data URI"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `885,429 175x19` → `131,842 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(2)>div:nth-child(2)>a:nth-child(17)>span:nth-child(2)>span:nth-child(1)` — "Convert PNG to JPG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `364,717 175x19` → `131,958 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(3)>div:nth-child(2)>a:nth-child(8)>span:nth-child(2)>span:nth-child(1)` — "Round corners PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `625,1239 175x19` → `807,726 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(3)>div:nth-child(2)>a:nth-child(11)>span:nth-child(2)>span:nth-child(1)` — "Outline PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `104,1384 175x19` → `807,842 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(4)>div:nth-child(2)>a:nth-child(7)>span:nth-child(2)>span:nth-child(1)` — "Grayscale PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `364,2096 175x19` → `131,1276 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(4)>div:nth-child(2)>a:nth-child(8)>span:nth-child(2)>span:nth-child(1)` — "Invert colors PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `625,2104 175x19` → `131,1392 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(4)>div:nth-child(2)>a:nth-child(10)>span:nth-child(2)>span:nth-child(1)` — "Change PNG opacity"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `1146,2096 175x19` → `807,958 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(4)>div:nth-child(2)>a:nth-child(20)>span:nth-child(2)>span:nth-child(1)` — "Temperature PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `1146,2361 175x19` → `131,1624 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(5)>div:nth-child(2)>a:nth-child(1)>span:nth-child(2)>span:nth-child(1)` — "Resize PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `104,2731 175x19` → `807,1160 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(5)>div:nth-child(2)>a:nth-child(2)>span:nth-child(2)>span:nth-child(1)` — "Crop PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `364,2740 175x19` → `807,1276 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(5)>div:nth-child(2)>a:nth-child(3)>span:nth-child(2)>span:nth-child(1)` — "Rotate PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `625,2748 175x19` → `807,1392 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(5)>div:nth-child(2)>a:nth-child(4)>span:nth-child(2)>span:nth-child(1)` — "Flip PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `885,2757 175x19` → `807,1508 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(5)>div:nth-child(2)>a:nth-child(5)>span:nth-child(2)>span:nth-child(1)` — "Add padding to PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `1146,2748 175x19` → `807,1624 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(6)>div:nth-child(2)>a:nth-child(1)>span:nth-child(2)>span:nth-child(1)` — "Blur PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `104,3396 175x19` → `131,1826 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(6)>div:nth-child(2)>a:nth-child(2)>span:nth-child(2)>span:nth-child(1)` — "Sharpen PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `364,3387 175x19` → `131,1942 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(6)>div:nth-child(2)>a:nth-child(3)>span:nth-child(2)>span:nth-child(1)` — "Vignette PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `625,3404 175x19` → `131,2058 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(6)>div:nth-child(2)>a:nth-child(8)>span:nth-child(2)>span:nth-child(1)` — "JPEG artifacts"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `625,3540 175x19` → `131,2174 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(8)>div:nth-child(2)>a:nth-child(9)>span:nth-child(2)>span:nth-child(1)` — "PNG info"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `885,4142 175x19` → `807,1826 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(8)>div:nth-child(2)>a:nth-child(13)>span:nth-child(2)>span:nth-child(1)` — "PNG orientation"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `625,4287 175x19` → `807,2174 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>section:nth-child(1)>section:nth-child(9)>div:nth-child(2)>a:nth-child(7)>span:nth-child(2)>span:nth-child(1)` — "Create gradient PNG"
- **fontFamily**: `"IBM Plex Sans", sans-serif` → `"IBM Plex Mono", monospace`
- **fontSize**: `14.4px` → `13px`
- **rect**: `364,4632 175x19` → `131,1160 488x15`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(3)>div:nth-child(4)>button:nth-child(2)` — "EN"
- **letterSpacing**: `0.6px` → `normal`
- **rect**: `1349,19 32x25` → `1352,20 29x24`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(1)>span:nth-child(2)` — "easy-png-tools"
- **rect**: `98,23 118x18` → `98,23 108x17`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(3)>div:nth-child(4)>button:nth-child(1)` — "RU"
- **letterSpacing**: `0.6px` → `normal`
### Только у нас
- `/ LIST-TOOLS`
- `CATALOG`
- `All tools`
- `Каталог всех PNG-инструментов, сгруппиро`
- `convert`
- `Convert`
- `Opens a JPEG and saves it as lossless PN`
- `Re-encodes a WebP image into universal P`
- `Convert GIF to PNG`
- `Extracts the first frame of a GIF animat`
- `Convert BMP to PNG`
- `Re-encodes BMP into compact lossless PNG`
- `Convert ICO to PNG`
- `Turns an .ico icon into a regular PNG of`
- `Convert PNG to BMP`
- `Saves the image as 24-bit BMP without an`
- `Encodes the image into a base64 string f`
- `Base64 to PNG`
- `Decodes a base64 string or data-uri back`
- `Builds a full data-uri (data:image/png;b`
- `Data URI to PNG`
- `Decodes data:image/…;base64,… back into `
- `PNG to HEX pixels`
- `Shows all pixels as rrggbbaa hex values `
- `HEX pixels to PNG`
- `Assembles an image from rrggbbaa hex val`
- `PNG to Bytes`
- `Lists every pixel as four decimal bytes `
- `Bytes to PNG`
- `Assembles an image from decimal RGBA byt`
- `PNG to RGB Values`
- `Lists every pixel as rgba(r, g, b, a), o`
- `RGB Values to PNG`
- `Assembles an image from rgba(r, g, b, a)`
- `Transparency is composited over the chos`
- `Convert PNG to WebP`
- `Re-encodes the image into WebP with adju`
- `Compress PNG`
- `Shrinks the PNG by reducing its palette `
- `Reduce PNG to Size`
- `Binary-searches the palette size until t`
- `SVG to PNG`
- `Decodes SVG markup into a raster image. `
- `alpha`
- `Alpha & transparency`
- `Circle Mask PNG`
- `Cuts the image into a circle. Diameter i`
- `Square Mask PNG`
- `Cuts the image into a rectangle with sid`
- `Star Mask PNG`
- `Cuts the image into an n-pointed star wi`
- `Wavy Mask PNG`
- `Cuts the image into a wavy-edged circle:`
- `Remove alpha channel PNG`
- `Composites the image over a white backgr`
- `Set alpha channel PNG`
- `Assigns the same opacity to all pixels; `
- `Extract alpha mask PNG`
- `Turns transparency into a black-and-whit`
- `Clips corners by a radius set as a perce`
### Только в рефе
- `/
CATALOG`
- `/`
- `EASY-PNG-TOOLS / CATALOG`
- `Tool catalog`
- `Focused utilities for working with PNG. `
- `32`
- `ALL`
- `CONVERT`
- `TRANSPARENCY`
- `COLOR`
- `GEOMETRY`
- `FILTERS`
- `ANALYZE`
- `05
TOOLS`
- `Re-encode JPEG files as lossless PNG whi`
- `01`
- `Turn WebP images into a universal PNG fo`
- `02`
- `Encode an image as a base64 string for e`
- `03`
- `Build a complete data URI ready for HTML`
- `04`
- `Composite transparency over a selected b`
- `05`
- `Remove background PNG`
- `Remove a solid background by color, tole`
- `Extract alpha mask`
- `Turn the alpha channel into a clean blac`
- `Clip the image corners by a precise radi`
- `Add a colored ring around opaque content`
- `Multiply the alpha channel while keeping`
- `Generate a smooth transition between two`
- `Convert the image to luminance-based sha`
- `Invert every color channel while leaving`
- `Brightness & contrast`
- `Adjust brightness and contrast across a `
- `Make an image warmer or cooler with a si`
- `Scale an image with bilinear interpolati`
- `Cut a rectangular area with exact coordi`
- `Rotate by 90, 180, or 270 degrees withou`
- `Mirror the image horizontally or vertica`
- `Expand the canvas on all sides by a chos`
- `04
TOOLS`
- `Apply a fast Gaussian-style blur with tr`
- `Emphasize edges with an adjustable sharp`
- `Smoothly darken the image edges while pr`
- `Simulate low-quality JPEG recompression `
- `Inspect dimensions, alpha presence, and `
- `Check grayscale`
- `Report whether the image contains only s`
- `Check transparency`
- `Detect transparent and semi-transparent `
- `Classify the image as portrait, landscap`
- ``
## /preview/tools/linear-gradient-png (vs gradient.html)
- Токены light: **10**, dark: **16**
- Элементы: **7**, только у нас: 111, только в рефе: 30
### Токены — Light
| token | ours | ref |
|---|---|---|
| `--danger` | #e5484d | — |
| `--success` | #25a96a | — |
| `--text-sm--line-height` | — | calc(1.25 / .875) |
| `--text-xs--line-height` | — | calc(1 / .75) |
| `--font-weight-medium` | — | 500 |
| `--spacing` | — | .25rem |
| `--text-xs` | — | .75rem |
| `--radius-md` | — | .375rem |
| `--radius-lg` | — | .5rem |
| `--text-sm` | — | .875rem |
### Токены — Dark
| token | ours | ref |
|---|---|---|
| `--panel` | #182129 | #f8fafb |
| `--blue` | #54a2ff | #1769d2 |
| `--muted` | #91a0ac | #6d7883 |
| `--background` | #11171d | #eef1f4 |
| `--danger` | #ff7479 | — |
| `--foreground` | #e8eef2 | #17212b |
| `--success` | #25a96a | — |
| `--line` | #33414c | #cbd3da |
| `--text-sm--line-height` | — | calc(1.25 / .875) |
| `--text-xs--line-height` | — | calc(1 / .75) |
| `--font-weight-medium` | — | 500 |
| `--spacing` | — | .25rem |
| `--text-xs` | — | .75rem |
| `--radius-md` | — | .375rem |
| `--radius-lg` | — | .5rem |
| `--text-sm` | — | .875rem |
### Расхождения элементов (топ 7)
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(3)` — "Gradient"
- **fontSize**: `12px` → `10px`
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
- **letterSpacing**: `0.48px` → `normal`
- **rect**: `754,24 61x15` → `930,26 44x12`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(1)` — "Workspace"
- **fontSize**: `12px` → `10px`
- **letterSpacing**: `0.48px` → `normal`
- **rect**: `596,24 69x15` → `798,26 49x12`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(2)` — "Catalog"
- **fontSize**: `12px` → `10px`
- **letterSpacing**: `0.48px` → `normal`
- **rect**: `683,24 54x15` → `870,26 39x12`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(4)` — "Background remover"
- **fontSize**: `12px` → `10px`
- **letterSpacing**: `0.48px` → `normal`
- **rect**: `834,24 138x15` → `996,26 99x12`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(3)>div:nth-child(4)>button:nth-child(2)` — "EN"
- **letterSpacing**: `0.6px` → `normal`
- **rect**: `1349,19 32x25` → `1352,20 29x24`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(1)>span:nth-child(2)` — "easy-png-tools"
- **rect**: `98,23 118x18` → `98,23 108x17`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(3)>div:nth-child(4)>button:nth-child(1)` — "RU"
- **letterSpacing**: `0.6px` → `normal`
### Только у нас
- `/ TOOLS / LINEAR-GRADIENT-PNG`
- `STEPS`
- `Pipeline`
- `AUTO`
- `01`
- `generate`
- `Create gradient PNG`
- `Width`
- `Height`
- `Start color`
- `End color`
- `Direction`
- `Horizontal`
- `Vertical`
- `Add step`
- `Split PNG into HSL`
- `Split PNG into HSV`
- `Split PNG into HSI`
- `Convert PNG to CMYK Colors`
- `Convert PNG to YCbCr Colors`
- `Convert PNG to LAB Colors`
- `Show Transparent Areas PNG`
- `Show Grayscale Pixels PNG`
- `Show Color Pixels PNG`
- `Light Pixel Mask PNG`
- `Dark Pixel Mask PNG`
- `Unique Color Mask PNG`
- `Convert JPG to PNG`
- `Convert WebP to PNG`
- `Convert GIF to PNG`
- `Convert BMP to PNG`
- `Convert ICO to PNG`
- `Convert PNG to BMP`
- `Resize PNG`
- `Crop PNG`
- `Rotate PNG`
- `Flip PNG`
- `Add padding to PNG`
- `Add border to PNG`
- `Fit PNG onto background`
- `Tile PNG`
- `Circle Mask PNG`
- `Square Mask PNG`
- `Star Mask PNG`
- `Wavy Mask PNG`
- `Center PNG by content`
- `Blur PNG`
- `Sharpen PNG`
- `Grayscale PNG`
- `Invert colors PNG`
- `Brightness & contrast PNG`
- `Change PNG opacity`
- `Sepia effect`
- `Change hue PNG`
- `Extract channel PNG`
- `Swap channels PNG`
- `Black & white threshold PNG`
- `Posterize PNG`
- `Two colors PNG`
- `Convert PNG to JPG`
### Только в рефе
- `/
GRADIENT`
- `/`
- `Gradient background.`
- `Create a clean, export-ready gradient wi`
- `GRADIENT SETTINGS`
- `Configure output`
- `TOOL 01`
- `GRADIENT TYPE`
- `Linear`
- `Radial`
- `COLOR STOPS`
- ``
- `135
°`
- `0° →`
- `45° ↗`
- `90° ↑`
- `135° ↖`
- `180° ←`
- `225° ↙`
- `270° ↓`
- `315° ↘`
- `100
%`
- `OUTPUT PREVIEW`
- `gradient.png`
- `PNG`
- `GENERATED CSS`
- `background: linear-gradient(135deg, #176`
- `v2.4.0`
- `gradient tool · local-only`
- `© 2024`
## /preview/tools/remove-background-png (vs background-remover.html)
- Токены light: **10**, dark: **16**
- Элементы: **8**, только у нас: 110, только в рефе: 28
### Токены — Light
| token | ours | ref |
|---|---|---|
| `--danger` | #e5484d | — |
| `--success` | #25a96a | — |
| `--text-sm--line-height` | — | calc(1.25 / .875) |
| `--text-xs--line-height` | — | calc(1 / .75) |
| `--font-weight-medium` | — | 500 |
| `--spacing` | — | .25rem |
| `--text-xs` | — | .75rem |
| `--radius-md` | — | .375rem |
| `--radius-lg` | — | .5rem |
| `--text-sm` | — | .875rem |
### Токены — Dark
| token | ours | ref |
|---|---|---|
| `--panel` | #182129 | #f8fafb |
| `--blue` | #54a2ff | #1769d2 |
| `--muted` | #91a0ac | #6d7883 |
| `--background` | #11171d | #eef1f4 |
| `--danger` | #ff7479 | — |
| `--foreground` | #e8eef2 | #17212b |
| `--success` | #25a96a | — |
| `--line` | #33414c | #cbd3da |
| `--text-sm--line-height` | — | calc(1.25 / .875) |
| `--text-xs--line-height` | — | calc(1 / .75) |
| `--font-weight-medium` | — | 500 |
| `--spacing` | — | .25rem |
| `--text-xs` | — | .75rem |
| `--radius-md` | — | .375rem |
| `--radius-lg` | — | .5rem |
| `--text-sm` | — | .875rem |
### Расхождения элементов (топ 8)
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(4)` — "Background remover"
- **fontSize**: `12px` → `10px`
- **color**: `rgb(109, 120, 131)` → `rgb(23, 105, 210)`
- **letterSpacing**: `0.48px` → `normal`
- **rect**: `841,24 138x15` → `978,26 99x12`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(1)` — "Workspace"
- **fontSize**: `12px` → `10px`
- **letterSpacing**: `0.48px` → `normal`
- **rect**: `603,24 69x15` → `780,26 49x12`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(2)` — "Catalog"
- **fontSize**: `12px` → `10px`
- **letterSpacing**: `0.48px` → `normal`
- **rect**: `690,24 54x15` → `851,26 39x12`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>nav:nth-child(2)>a:nth-child(3)` — "Gradient"
- **fontSize**: `12px` → `10px`
- **letterSpacing**: `0.48px` → `normal`
- **rect**: `762,24 61x15` → `912,26 44x12`
### `body>div:nth-child(1)>div:nth-child(1)>div:nth-child(2)>div:nth-child(2)>div:nth-child(1)>div:nth-child(2)>section:nth-child(1)>header:nth-child(1)>div:nth-child(1)>span:nth-child(1)` — "RESULT"
- **letterSpacing**: `1px` → `normal`
- **lineHeight**: `12px` → `normal`
- **rect**: `726,125 108x12` → `954,467 33x12`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(3)>div:nth-child(4)>button:nth-child(2)` — "EN"
- **letterSpacing**: `0.6px` → `normal`
- **rect**: `1349,19 32x25` → `1352,20 29x24`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(1)>span:nth-child(2)` — "easy-png-tools"
- **rect**: `98,23 118x18` → `98,23 108x17`
### `body>div:nth-child(1)>div:nth-child(1)>header:nth-child(1)>div:nth-child(3)>div:nth-child(4)>button:nth-child(1)` — "RU"
- **letterSpacing**: `0.6px` → `normal`
### Только у нас
- `/ TOOLS / REMOVE-BACKGROUND-PNG`
- `STEPS`
- `Pipeline`
- `AUTO`
- `01`
- `alpha`
- `Remove background PNG (smart)`
- `Background color`
- `Similarity tolerance, %`
- `10`
- `Outer regions only`
- `Edge smoothing, passes`
- `1`
- `Add step`
- `Split PNG into HSL`
- `Split PNG into HSV`
- `Split PNG into HSI`
- `Convert PNG to CMYK Colors`
- `Convert PNG to YCbCr Colors`
- `Convert PNG to LAB Colors`
- `Show Transparent Areas PNG`
- `Show Grayscale Pixels PNG`
- `Show Color Pixels PNG`
- `Light Pixel Mask PNG`
- `Dark Pixel Mask PNG`
- `Unique Color Mask PNG`
- `Convert JPG to PNG`
- `Convert WebP to PNG`
- `Convert GIF to PNG`
- `Convert BMP to PNG`
- `Convert ICO to PNG`
- `Convert PNG to BMP`
- `Resize PNG`
- `Crop PNG`
- `Rotate PNG`
- `Flip PNG`
- `Add padding to PNG`
- `Add border to PNG`
- `Fit PNG onto background`
- `Tile PNG`
- `Circle Mask PNG`
- `Square Mask PNG`
- `Star Mask PNG`
- `Wavy Mask PNG`
- `Center PNG by content`
- `Blur PNG`
- `Sharpen PNG`
- `Grayscale PNG`
- `Invert colors PNG`
- `Brightness & contrast PNG`
- `Change PNG opacity`
- `Sepia effect`
- `Change hue PNG`
- `Extract channel PNG`
- `Swap channels PNG`
- `Black & white threshold PNG`
- `Posterize PNG`
- `Two colors PNG`
- `Convert PNG to JPG`
- `Convert PNG to WebP`
### Только в рефе
- `/
BACKGROUND REMOVER`
- `/`
- `Remove background.`
- `Select a background color and tune the e`
- `REMOVER SETTINGS`
- `Configure detection`
- `TOOL 02`
- `BACKGROUND COLOR`
- `72
%`
- `strict edges`
- `more removal`
- `OUTER COLOR ONLY`
- `Only remove connected background pixels `
- `SHOW MASK`
- `Preview the detected transparency mask.`
- `SOURCE / RESULT`
- `comparison.png`
- `SOURCE`
- `original.png`
- `OBJECT`
- `1200 × 800`
- `removed-bg.png`
- `PNG`
- `PNG-24`
- `ENABLED`
- `v2.4.0`
- `background remover · local-only`
- `© 2024`
+264 -146
View File
@@ -1,11 +1,12 @@
// Стиль-аудит: снимает design-токены (light/dark) и дерево вычисленных стилей
// нашей preview-страницы и refs-html страницы, сравнивает и пишет отчёт.
// Запуск: pnpm audit (опции: --ours <url> --ref <file> --no-serve --route <path>)
// Стиль-аудит: для каждого preview-маршрута снимает design-токены (light/dark)
// и дерево вычисленных стилей нашей страницы и соответствующей refs-html
// страницы, сравнивает и пишет сводный отчёт.
// Запуск: pnpm audit (опции: --route <path> --ref <file> --no-serve --ours <url>)
import { chromium } from "playwright";
import { spawn } from "node:child_process";
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
import { pathToFileURL } from "node:url";
import { resolve } from "node:path";
import { resolve, join } from "node:path";
const args = (() => {
const a = {};
@@ -21,9 +22,7 @@ const args = (() => {
const WEB = process.cwd();
const PORT = 5179;
const ROUTE = args.route || "/preview/demo";
const OURS = args.ours || `http://127.0.0.1:${PORT}${ROUTE}`;
const REF = args.ref || resolve(WEB, "../refs-html/demo.html");
const REFS_DIR = resolve(WEB, "../refs-html");
const FIELDS = [
"fontFamily",
@@ -41,6 +40,104 @@ const FIELDS = [
"gap",
];
// Динамические сегменты: явный список значений + сопоставление id → файл рефа
// (id инструмента в registry.ts не совпадает с именем refs-html-файла).
const DYNAMIC = {
"tools/[id]": {
ids: ["linear-gradient-png", "remove-background-png"],
refFor: (id) =>
id === "linear-gradient-png"
? "gradient.html"
: id === "remove-background-png"
? "background-remover.html"
: null,
},
};
// Маршруты, которые не с чем сравнивать (нет релевантного рефа). Например,
// `/preview` (индекс/воркспейс) в нашем приложении — своя страница, а
// `refs-html/index.html` — это зеркало demo из Next-рефа, не его реф.
const EXCLUDE = new Set(["/preview"]);
const refNameFor = (route) => {
if (route === "/preview") return "index.html";
const last = route.split("/").filter(Boolean).pop();
return `${last}.html`;
};
function discoverRoutes() {
const base = resolve(WEB, "src/routes/preview");
const out = [];
const walk = (dir) => {
let ents;
try {
ents = readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const e of ents) {
const p = join(dir, e.name);
if (e.isDirectory()) walk(p);
else if (e.name === "+page.svelte") {
const rel = p.slice(base.length).replace(/\\/g, "/");
const parts = rel.split("/").filter(Boolean);
parts.pop(); // +page.svelte
out.push(parts.length ? `/preview/${parts.join("/")}` : "/preview");
}
}
};
walk(base);
return out;
}
function buildTargets() {
if (args.route) {
const ref = args.ref || refNameFor(args.route);
return [{ route: args.route, ref }];
}
const targets = [];
const seen = new Set();
const add = (route, ref) => {
if (!ref || seen.has(route)) return;
if (!refExists(ref)) {
console.warn(`[audit] ref not found, skipped: ${route} -> ${ref}`);
return;
}
seen.add(route);
targets.push({ route, ref });
};
for (const route of discoverRoutes()) {
if (route.includes("[")) continue; // динамика — ниже
if (EXCLUDE.has(route)) {
console.log(`[audit] excluded (no relevant ref): ${route}`);
continue;
}
add(route, refNameFor(route));
}
for (const [seg, cfg] of Object.entries(DYNAMIC)) {
for (const id of cfg.ids) {
add(`/preview/${seg.replace("[id]", id)}`, cfg.refFor(id));
}
}
// Рефы без маршрута — просто сообщаем, не падаем.
const routed = new Set(targets.map((t) => t.ref));
for (const f of refsList()) {
if (!routed.has(f)) console.warn(`[audit] ref без маршрута: ${f}`);
}
return targets;
}
function refsList() {
try {
return readdirSync(REFS_DIR).filter((f) => f.endsWith(".html"));
} catch {
return [];
}
}
function refExists(name) {
return refsList().includes(name);
}
const waitFor = async (url, ms = 60000) => {
const t = Date.now();
while (Date.now() - t < ms) {
@@ -133,7 +230,156 @@ const snapshot = (page, url) =>
return { tokensLight, tokensDark, tree: walk(document.body, "body") };
}, url);
const TOK_NOISE = /^(--tw-|--lightningcss-|--default-)/;
function diffPair(ours, ref) {
const tokDiff = (a, b) => {
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
const rows = [];
for (const k of keys) {
if (TOK_NOISE.test(k)) continue;
const av = a[k] ?? "—";
const bv = b[k] ?? "—";
if (av !== bv) rows.push({ key: k, ours: av, ref: bv });
}
return rows;
};
const tokenDiffs = {
light: tokDiff(ours.tokensLight, ref.tokensLight),
dark: tokDiff(ours.tokensDark, ref.tokensDark),
};
const byText = (tree) => {
const m = new Map();
for (const e of tree) if (e.text) if (!m.has(e.text)) m.set(e.text, e);
return m;
};
const txtO = byText(ours.tree);
const txtR = byText(ref.tree);
const onlyOurs = [];
const onlyRef = [];
const elementDiffs = [];
for (const [k, o] of txtO) {
const r = txtR.get(k);
if (!r) {
onlyOurs.push(k);
continue;
}
const deltas = {};
for (const f of FIELDS) {
const ov = (o.style[f] ?? "").toString();
const rv = (r.style[f] ?? "").toString();
if (ov !== rv) deltas[f] = { ours: ov, ref: rv };
}
const dr = o.rect;
const rr = r.rect;
if (
Math.abs(dr.x - rr.x) > 2 ||
Math.abs(dr.y - rr.y) > 2 ||
Math.abs(dr.w - rr.w) > 2 ||
Math.abs(dr.h - rr.h) > 2
)
deltas.rect = { ours: dr, ref: rr };
if (Object.keys(deltas).length)
elementDiffs.push({ path: o.path, tag: o.tag, text: k, deltas });
}
for (const [k] of txtR) if (!txtO.has(k)) onlyRef.push(k);
return {
tokenDiffs,
elementDiffs,
onlyOurs: onlyOurs.slice(0, 60),
onlyRef: onlyRef.slice(0, 60),
counts: {
tokenLight: tokenDiffs.light.length,
tokenDark: tokenDiffs.dark.length,
elements: elementDiffs.length,
onlyOurs: onlyOurs.length,
onlyRef: onlyRef.length,
},
};
}
async function auditOne(page, target) {
const oursUrl = args.ours || `http://127.0.0.1:${PORT}${target.route}`;
const refUrl = pathToFileURL(resolve(REFS_DIR, target.ref)).href;
await page.goto(oursUrl, { waitUntil: "load" });
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(300);
const ours = await snapshot(page, oursUrl);
await page.goto(refUrl, { waitUntil: "load" });
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(300);
const ref = await snapshot(page, refUrl);
const diff = diffPair(ours, ref);
return { route: target.route, ref: target.ref, ...diff };
}
function toMarkdown(reports) {
const tokTable = (rows) =>
rows.length
? `| token | ours | ref |\n|---|---|---|\n` +
rows.map((r) => `| \`${r.key}\` | ${r.ours} | ${r.ref} |`).join("\n")
: "_все совпадают_";
const elBlock = (el) => {
const d = Object.entries(el.deltas)
.map(([k, v]) => {
if (k === "rect") {
const f = (r) => `${r.x},${r.y} ${r.w}x${r.h}`;
return ` - **rect**: \`${f(v.ours)}\`\`${f(v.ref)}\``;
}
return ` - **${k}**: \`${v.ours}\`\`${v.ref}\``;
})
.join("\n");
return `### \`${el.path}\`${el.text ? ` — "${el.text}"` : ""}\n${d}`;
};
const summary =
`# Style audit (multi-route)\n\n_${new Date().toISOString()}_\n\n` +
`## Сводка\n\n| route | ref | tokL | tokD | els | onlyOurs | onlyRef |\n|---|---|---|---|---|---|---|\n` +
reports
.map(
(r) =>
`| \`${r.route}\` | ${r.ref} | ${r.counts.tokenLight} | ${r.counts.tokenDark} | ${r.counts.elements} | ${r.counts.onlyOurs} | ${r.counts.onlyRef} |`,
)
.join("\n");
const sections = reports
.map((r) => {
const sorted = [...r.elementDiffs].sort(
(x, y) => Object.keys(y.deltas).length - Object.keys(x.deltas).length,
);
const els = sorted.length
? sorted.slice(0, 100).map(elBlock).join("\n\n")
: "_расхождений нет_";
return (
`\n\n## ${r.route} (vs ${r.ref})\n\n` +
`- Токены light: **${r.counts.tokenLight}**, dark: **${r.counts.tokenDark}**\n` +
`- Элементы: **${r.counts.elements}**, только у нас: ${r.counts.onlyOurs}, только в рефе: ${r.counts.onlyRef}\n\n` +
`### Токены — Light\n\n${tokTable(r.tokenDiffs.light)}\n\n` +
`### Токены — Dark\n\n${tokTable(r.tokenDiffs.dark)}\n\n` +
`### Расхождения элементов (топ ${Math.min(100, sorted.length)})\n\n${els}\n\n` +
`### Только у нас\n\n` +
(r.onlyOurs.length ? r.onlyOurs.map((p) => `- \`${p}\``).join("\n") : "_—_") +
`\n\n### Только в рефе\n\n` +
(r.onlyRef.length ? r.onlyRef.map((p) => `- \`${p}\``).join("\n") : "_—_")
);
})
.join("\n");
return summary + sections + "\n";
}
async function main() {
const targets = buildTargets();
if (!targets.length) {
console.error("no audit targets (refs-html/*.html missing?)");
process.exit(1);
}
console.log(
`[audit] targets: ${targets.map((t) => t.route).join(", ")}`,
);
let server;
if (!args.noServe) {
const bin = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
@@ -143,7 +389,7 @@ async function main() {
shell: true,
detached: process.platform !== "win32",
});
await waitFor(OURS);
await waitFor(args.ours || `http://127.0.0.1:${PORT}${targets[0].route}`);
}
const browser = await chromium.launch();
@@ -151,104 +397,21 @@ async function main() {
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
});
const reports = [];
try {
await page.goto(OURS, { waitUntil: "load" });
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(400);
const ours = await snapshot(page, OURS);
await page.goto(pathToFileURL(REF).href, { waitUntil: "load" });
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(400);
const ref = await snapshot(page, pathToFileURL(REF).href);
// token diff (игнорируем служебный мусор Tailwind/lightningcss — он
// ref-only и не относится к нашим смысловым токенам)
const TOK_NOISE = /^(--tw-|--lightningcss-|--default-)/;
const tokDiff = (a, b) => {
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
const rows = [];
for (const k of keys) {
if (TOK_NOISE.test(k)) continue;
const av = a[k] ?? "—";
const bv = b[k] ?? "—";
if (av !== bv) rows.push({ key: k, ours: av, ref: bv });
}
return rows;
};
const tokenDiffs = {
light: tokDiff(ours.tokensLight, ref.tokensLight),
dark: tokDiff(ours.tokensDark, ref.tokensDark),
};
// element diff: совпадающие по тексту элементы (структуры разные,
// поэтому nth-child-пути не совпадают — матчим по видимому тексту)
const byText = (tree) => {
const m = new Map();
for (const e of tree) if (e.text) if (!m.has(e.text)) m.set(e.text, e);
return m;
};
const txtO = byText(ours.tree);
const txtR = byText(ref.tree);
const onlyOurs = [];
const onlyRef = [];
const elementDiffs = [];
for (const [k, o] of txtO) {
const r = txtR.get(k);
if (!r) {
onlyOurs.push(k);
continue;
}
const deltas = {};
for (const f of FIELDS) {
const ov = (o.style[f] ?? "").toString();
const rv = (r.style[f] ?? "").toString();
if (ov !== rv) deltas[f] = { ours: ov, ref: rv };
}
const dr = o.rect;
const rr = r.rect;
if (
Math.abs(dr.x - rr.x) > 2 ||
Math.abs(dr.y - rr.y) > 2 ||
Math.abs(dr.w - rr.w) > 2 ||
Math.abs(dr.h - rr.h) > 2
)
deltas.rect = { ours: dr, ref: rr };
if (Object.keys(deltas).length)
elementDiffs.push({ path: o.path, tag: o.tag, text: k, deltas });
for (const target of targets) {
const rep = await auditOne(page, target);
reports.push(rep);
console.log(
`[audit] ${rep.route}: tokL=${rep.counts.tokenLight} tokD=${rep.counts.tokenDark} els=${rep.counts.elements} onlyOurs=${rep.counts.onlyOurs} onlyRef=${rep.counts.onlyRef}`,
);
}
for (const [k] of txtR) if (!txtO.has(k)) onlyRef.push(k);
const report = {
generatedAt: new Date().toISOString(),
ours: OURS,
ref: REF,
tokenDiffs,
elementDiffs,
onlyOurs: onlyOurs.slice(0, 60),
onlyRef: onlyRef.slice(0, 60),
counts: {
tokenLight: tokenDiffs.light.length,
tokenDark: tokenDiffs.dark.length,
elements: elementDiffs.length,
onlyOurs: onlyOurs.length,
onlyRef: onlyRef.length,
},
};
mkdirSync(resolve(WEB, "audit"), { recursive: true });
writeFileSync(
resolve(WEB, "audit/audit-report.json"),
JSON.stringify(report, null, 2),
);
writeFileSync(
resolve(WEB, "audit/audit-report.md"),
toMarkdown(report, ours, ref),
);
console.log(
`audit done: tokens light/dark=${tokenDiffs.light.length}/${tokenDiffs.dark.length}, ` +
`elements=${elementDiffs.length}, onlyOurs=${onlyOurs.length}, onlyRef=${onlyRef.length}`,
JSON.stringify({ generatedAt: new Date().toISOString(), reports }, null, 2),
);
writeFileSync(resolve(WEB, "audit/audit-report.md"), toMarkdown(reports));
console.log(`report: web/audit/audit-report.md`);
} finally {
await browser.close();
@@ -264,51 +427,6 @@ async function main() {
}
}
function toMarkdown(report, ours, ref) {
const tokTable = (rows, a, b) =>
rows.length
? `| token | ours | ref |\n|---|---|---|\n` +
rows.map((r) => `| \`${r.key}\` | ${r.ours} | ${r.ref} |`).join("\n")
: "_все совпадают_";
const sorted = [...report.elementDiffs].sort(
(x, y) => Object.keys(y.deltas).length - Object.keys(x.deltas).length,
);
const elRows = sorted
.slice(0, 100)
.map((e) => {
const d = Object.entries(e.deltas)
.map(([k, v]) => {
if (k === "rect") {
const f = (r) => `${r.x},${r.y} ${r.w}x${r.h}`;
return ` - **rect**: \`${f(v.ours)}\`\`${f(v.ref)}\``;
}
return ` - **${k}**: \`${v.ours}\`\`${v.ref}\``;
})
.join("\n");
return `### \`${e.path}\`${e.text ? ` — "${e.text}"` : ""}\n${d}`;
})
.join("\n\n");
return (
`# Style audit: ${report.ours}\n\nvs ${report.ref}\n\n_${report.generatedAt}_\n\n` +
`## Сводка\n\n- Токены light: **${report.counts.tokenLight}** расх.\n` +
`- Токены dark: **${report.counts.tokenDark}** расх.\n` +
`- Элементы (стиль/геометрия): **${report.counts.elements}** расх.\n` +
`- Только у нас: ${report.counts.onlyOurs}, только в рефе: ${report.counts.onlyRef}\n\n` +
`## Токены — Light\n\n${tokTable(report.tokenDiffs.light)}\n\n` +
`## Токены — Dark\n\n${tokTable(report.tokenDiffs.dark)}\n\n` +
`## Расхождения элементов (топ ${Math.min(100, sorted.length)})\n\n${elRows}\n\n` +
`## Только у нас (структурно)\n\n` +
(report.onlyOurs.length
? report.onlyOurs.map((p) => `- \`${p}\``).join("\n")
: "_—_") +
`\n\n## Только в рефе (структурно)\n\n` +
(report.onlyRef.length
? report.onlyRef.map((p) => `- \`${p}\``).join("\n")
: "_—_") +
`\n`
);
}
main().catch((e) => {
console.error(e);
process.exit(1);