designlang 12.0.0 → 12.1.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/.claude/launch.json +6 -0
- package/CHANGELOG.md +22 -0
- package/README.md +65 -471
- package/bin/design-extract.js +76 -0
- package/package.json +1 -1
- package/src/formatters/grade.js +404 -0
package/.claude/launch.json
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
"runtimeExecutable": "npm",
|
|
7
7
|
"runtimeArgs": ["--prefix", "website", "run", "dev"],
|
|
8
8
|
"port": 3000
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "grade-preview",
|
|
12
|
+
"runtimeExecutable": "npx",
|
|
13
|
+
"runtimeArgs": ["--yes", "http-server", "design-extract-output", "-p", "4173", "-c-1", "--silent"],
|
|
14
|
+
"port": 4173
|
|
9
15
|
}
|
|
10
16
|
]
|
|
11
17
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [12.1.0] — 2026-04-29
|
|
4
|
+
|
|
5
|
+
**Design Report Card — a shareable audit page, generated from any URL.**
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`designlang grade <url>`** — produces a standalone, self-contained HTML
|
|
10
|
+
"Design Report Card" alongside JSON and Markdown variants. Letter grade
|
|
11
|
+
(A–F) hero, eight scored dimensions with arc gauges, evidence pulled from
|
|
12
|
+
the audited site itself (palette swatches, type specimen, spacing rhythm),
|
|
13
|
+
and a strengths / what-to-fix ledger. Editorial layout, paper/ink theme
|
|
14
|
+
with a dark toggle, print-ready, OG-meta for shareable links.
|
|
15
|
+
- New formatter: **`src/formatters/grade.js`** with `formatGrade` (HTML) and
|
|
16
|
+
`formatGradeMarkdown` exports. Reuses the existing `scoreDesignSystem`
|
|
17
|
+
output — no scoring changes.
|
|
18
|
+
- Flags: `--format html|md|json|all`, `--open` to launch the report in your
|
|
19
|
+
browser when it finishes.
|
|
20
|
+
|
|
21
|
+
Why this exists: html.to.design, Locofy, Builder, Polypane all ship
|
|
22
|
+
extraction or layout cloning. None of them grade the design system itself
|
|
23
|
+
in a form you can post on Twitter or email to a client. This is that.
|
|
24
|
+
|
|
3
25
|
## [10.5.0] — 2026-04-22
|
|
4
26
|
|
|
5
27
|
**The states LLMs always botch.**
|
package/README.md
CHANGED
|
@@ -18,454 +18,76 @@
|
|
|
18
18
|
|
|
19
19
|
[](https://www.npmjs.com/package/designlang)
|
|
20
20
|
|
|
21
|
-
**designlang**
|
|
21
|
+
**designlang** points a headless browser at any URL and reads the design system off the live DOM. One command emits 17+ files — DTCG tokens, Tailwind config, shadcn theme, Figma variables, motion tokens, typed component anatomy, brand voice, page-intent labels, and a paste-ready prompt pack for v0 / Lovable / Cursor / Claude Artifacts.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
It also goes where extractors don't: **layout patterns**, **responsive behavior across 4 breakpoints**, **hover / focus / active states**, **WCAG contrast scoring**, **multi-page consistency**, **drift checks against a live source-of-truth**, **visual-diffs**, and a **shareable graded report card**.
|
|
24
24
|
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
Everything else captures *how* a site looks. v10 captures *what it is* — the semantic signal an LLM needs to rebuild a site faithfully instead of restyling a generic scaffold.
|
|
28
|
-
|
|
29
|
-
- **Page Intent** — classifier labels the URL as `landing` / `pricing` / `docs` / `blog` / `blog-post` / `product` / `about` / `dashboard` / `auth` / `legal`, with a confidence score and rival alternates. URL + title + meta + DOM-shape signals. Heuristic-only by default; opt into `--smart` for LLM refinement.
|
|
30
|
-
- **Section Roles** — every semantic region gets a role (`hero`, `feature-grid`, `logo-wall`, `stats`, `testimonial`, `pricing-table`, `faq`, `steps`, `comparison`, `gallery`, `bento`, `cta`, `footer`), plus reading order and extracted slot copy (headings, lede, CTA counts).
|
|
31
|
-
- **Multi-Page Crawl** — `--full` (or `--pages <n>`) auto-discovers the site's own canonical pages from its nav (pricing/docs/blog/about/product) and runs the full pipeline on each, then emits a cross-page consistency report — shared tokens, per-page uniques, and pairwise Jaccard scores. LLMs get a real design language, not just a homepage snapshot.
|
|
32
|
-
- **Material Language** — classifies the visual vocabulary as `glassmorphism` / `neumorphism` / `flat` / `brutalist` / `skeuomorphic` / `material-you` / `soft-ui` / `mixed` from shadow complexity, backdrop-filter usage, saturation, and geometry.
|
|
33
|
-
- **Imagery Style** — fingerprints the images: `photography` / `3d-render` / `isometric` / `flat-illustration` / `gradient-mesh` / `icon-only` / `screenshot` / `mixed`, plus dominant aspect ratio and image-radius profile.
|
|
34
|
-
- **Component Library Detection** — identifies `shadcn/ui`, `radix-ui`, `headlessui`, `mui`, `chakra-ui`, `mantine`, `ant-design`, `bootstrap`, `heroui`, `tailwind-ui`, `vuetify`, or plain `tailwindcss`, with evidence and alternates.
|
|
35
|
-
- **Logo Extraction** — `--full` writes `*-logo.svg` (or `.png`) plus `*-logo.json` with dimensions, aspect, and sampled clearspace.
|
|
36
|
-
- **Prompt Pack** — a `*-prompts/` directory with `v0.txt`, `lovable.txt`, `cursor.md`, `claude-artifacts.md`, and atomic `recipe-<component>.md` cards — tokens, section order, voice, and library inlined so one paste is enough.
|
|
37
|
-
- **`--smart` mode** — when a heuristic classifier returns low confidence, fall back to a small LLM call (uses `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` from env). Completely optional — no key, no behavior change.
|
|
38
|
-
|
|
39
|
-
## What's New in v9 — The Motion & Voice Release
|
|
40
|
-
|
|
41
|
-
- **Motion Language** — durations bucketed into semantic tokens (`instant`/`xs`/`sm`/`md`/`lg`/`xl`), easings classified into families (ease-out, spring-overshoot, steps), scroll-linked animation detection (`animation-timeline`, `view-timeline-name`), keyframe kind classification (slide / fade / reveal / rotate / scale / pulse), and a `feel` fingerprint — *springy*, *responsive*, *smooth*, *mechanical*, or *mixed*.
|
|
42
|
-
- **Component Anatomy v2** — every component cluster is now an *anatomy tree* with slots (label, icon, badge, heading, media), variant × size × state matrices, and an emitted `*-anatomy.tsx` file of typed React stubs you can wire into your design system.
|
|
43
|
-
- **Brand Voice** — extracts tone (friendly / formal / technical / playful / neutral), pronoun posture (`we→you` / `you-only` / `we-only` / `third-person`), heading style (Title Case / Sentence case / all-lowercase), top CTA verbs, and a microcopy inventory. Feeds LLMs the *voice*, not just the paint.
|
|
44
|
-
- **`designlang lint`** — audit your own `design-tokens.json` (DTCG or flat) or `variables.css` for color sprawl, spacing-scale drift, radius/shadow bloat, and WCAG fg/bg contrast fails. Exits non-zero on errors — CI-ready.
|
|
45
|
-
- **`designlang drift`** — point at a live site, pass your local token file, and get a verdict: `in-sync` / `minor-drift` / `notable-drift` / `major-drift`. Integrates cleanly with the existing GitHub Action.
|
|
46
|
-
- **`designlang visual-diff`** — capture two URLs side-by-side and emit a single-file HTML report with component screenshots, file-size deltas, and changed color tokens. No heavy pixel-diff dependencies — runs in pure Node + Playwright.
|
|
47
|
-
|
|
48
|
-
## Quick Start
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
npx designlang https://stripe.com
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Get everything at once:
|
|
25
|
+
## Quick start
|
|
55
26
|
|
|
56
27
|
```bash
|
|
57
|
-
npx designlang https://stripe.com
|
|
28
|
+
npx designlang https://stripe.com # extract everything
|
|
29
|
+
npx designlang grade https://stripe.com # shareable HTML report card ← v12.1
|
|
30
|
+
npx designlang clone https://stripe.com # working Next.js starter
|
|
31
|
+
npx designlang --full https://stripe.com # screenshots + responsive + interactions
|
|
58
32
|
```
|
|
59
33
|
|
|
60
|
-
## What You Get (11+ Files)
|
|
61
|
-
|
|
62
|
-
| File | What it is |
|
|
63
|
-
|------|------------|
|
|
64
|
-
| `*-design-language.md` | AI-optimized markdown — feed it to any LLM to recreate the design |
|
|
65
|
-
| `*-preview.html` | Visual report with swatches, type scale, shadows, a11y score |
|
|
66
|
-
| `*-design-tokens.json` | [W3C Design Tokens](https://design-tokens.github.io/community-group/format/) format |
|
|
67
|
-
| `*-tailwind.config.js` | Drop-in Tailwind CSS theme |
|
|
68
|
-
| `*-variables.css` | CSS custom properties |
|
|
69
|
-
| `*-figma-variables.json` | Figma Variables import (with dark mode support) |
|
|
70
|
-
| `*-theme.js` | React/CSS-in-JS theme (Chakra, Stitches, Vanilla Extract) |
|
|
71
|
-
| `*-shadcn-theme.css` | shadcn/ui globals.css variables |
|
|
72
|
-
| `*-motion-tokens.json` | **(v9)** Motion tokens — durations, easings, springs, scroll-linked flag |
|
|
73
|
-
| `*-anatomy.tsx` | **(v9)** Typed React stubs for every detected component + variants |
|
|
74
|
-
| `*-voice.json` | **(v9)** Brand voice fingerprint — tone, CTA verbs, heading style |
|
|
75
|
-
|
|
76
|
-
The markdown output has **19 sections**: Color Palette, Typography, Spacing, Border Radii, Box Shadows, CSS Custom Properties, Breakpoints, Transitions & Animations, Component Patterns (with full CSS snippets), Layout System, Responsive Design, Interaction States, Accessibility (WCAG 2.1), Gradients, Z-Index Map, SVG Icons, Font Files, Image Style Patterns, and Quick Start.
|
|
77
|
-
|
|
78
|
-
In v7 a companion `*-mcp.json` file is also written alongside the 8 outputs so that `designlang mcp` can serve regions, components, and health data from disk on later invocations. Opting into `--platforms <csv>` additively emits `ios/`, `android/`, `flutter/`, and/or `wordpress-theme/` directories in the output folder, and `--emit-agent-rules` adds a `.cursor/`, `.claude/`, `CLAUDE.md.fragment`, and `agents.md` set.
|
|
79
|
-
|
|
80
34
|
## Install
|
|
81
35
|
|
|
82
36
|
```bash
|
|
83
|
-
|
|
84
|
-
npx
|
|
85
|
-
|
|
86
|
-
# Or install globally
|
|
87
|
-
npm install -g designlang
|
|
88
|
-
|
|
89
|
-
# As an agent skill (Claude Code, Cursor, Codex, 40+ agents)
|
|
90
|
-
npx skills add Manavarya09/design-extract
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## What Makes This Different
|
|
94
|
-
|
|
95
|
-
Most design extraction tools give you colors and fonts. That's it. designlang fills 5 market gaps that no other tool addresses:
|
|
96
|
-
|
|
97
|
-
### 1. Layout System Extraction
|
|
98
|
-
|
|
99
|
-
Extracts the structural skeleton — grid column patterns, flex direction usage, container widths, gap values, and justify/align patterns.
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
Layout: 55 grids, 492 flex containers
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Every other tool gives you the paint. designlang gives you the architecture.
|
|
106
|
-
|
|
107
|
-
### 2. Responsive Multi-Breakpoint Capture
|
|
108
|
-
|
|
109
|
-
Crawls the site at 4 viewports (mobile, tablet, desktop, wide) and maps exactly what changes:
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
designlang https://vercel.com --responsive
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
```
|
|
116
|
-
Responsive: 4 viewports, 3 breakpoint changes
|
|
117
|
-
375px → 768px: Nav visibility hidden → visible, Hamburger shown → hidden
|
|
118
|
-
768px → 1280px: Max grid columns 1 → 3, H1 size 32px → 48px
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
No other tool captures how the design *adapts*, just how it looks at one size.
|
|
122
|
-
|
|
123
|
-
### 3. Interaction State Capture
|
|
124
|
-
|
|
125
|
-
Programmatically hovers and focuses interactive elements, capturing the actual style transitions:
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
designlang https://stripe.com --interactions
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
```css
|
|
132
|
-
/* Button Hover */
|
|
133
|
-
background-color: rgb(83, 58, 253) → rgb(67, 47, 202);
|
|
134
|
-
box-shadow: none → 0 4px 12px rgba(83, 58, 253, 0.4);
|
|
135
|
-
|
|
136
|
-
/* Input Focus */
|
|
137
|
-
border-color: rgb(200, 200, 200) → rgb(83, 58, 253);
|
|
138
|
-
outline: none → 2px solid rgb(83, 58, 253);
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### 4. Live Site Sync
|
|
142
|
-
|
|
143
|
-
Treat the deployed site as your source of truth, not Figma:
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
designlang sync https://stripe.com --out ./src/tokens
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
Detects design changes and auto-updates your local `design-tokens.json`, `tailwind.config.js`, and `variables.css`.
|
|
150
|
-
|
|
151
|
-
### 5. Multi-Brand Comparison
|
|
152
|
-
|
|
153
|
-
Compare N brands side-by-side:
|
|
154
|
-
|
|
155
|
-
```bash
|
|
156
|
-
designlang brands stripe.com vercel.com github.com linear.app
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Generates a matrix with color overlap analysis, typography comparison, spacing systems, and accessibility scores. Outputs both `brands.md` and `brands.html`.
|
|
160
|
-
|
|
161
|
-
### 6. Clone Command
|
|
162
|
-
|
|
163
|
-
Generate a working Next.js app with the extracted design applied:
|
|
164
|
-
|
|
165
|
-
```bash
|
|
166
|
-
designlang clone https://stripe.com
|
|
167
|
-
cd cloned-design && npm install && npm run dev
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
One command → a running app with the site's colors, fonts, spacing, and component patterns.
|
|
171
|
-
|
|
172
|
-
### 7. Design System Scoring
|
|
173
|
-
|
|
174
|
-
Rate any site's design quality across 7 categories:
|
|
175
|
-
|
|
176
|
-
```bash
|
|
177
|
-
designlang score https://vercel.com
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
```
|
|
181
|
-
68/100 Grade: D
|
|
182
|
-
|
|
183
|
-
Color Discipline ██████████░░░░░░░░░░ 50
|
|
184
|
-
Typography ██████████████░░░░░░ 70
|
|
185
|
-
Spacing System ████████████████░░░░ 80
|
|
186
|
-
Shadows ██████████░░░░░░░░░░ 50
|
|
187
|
-
Border Radii ████████░░░░░░░░░░░░ 40
|
|
188
|
-
Accessibility ███████████████████░ 94
|
|
189
|
-
Tokenization ████████████████████ 100
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### 8. Watch Mode
|
|
193
|
-
|
|
194
|
-
Monitor a site for design changes:
|
|
195
|
-
|
|
196
|
-
```bash
|
|
197
|
-
designlang watch https://stripe.com --interval 60
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
Checks hourly and alerts when colors, fonts, or accessibility scores change.
|
|
201
|
-
|
|
202
|
-
### 9. Apply Command (NEW in v5)
|
|
203
|
-
|
|
204
|
-
Extract a site's design and write tokens directly into your project — auto-detects your framework:
|
|
205
|
-
|
|
206
|
-
```bash
|
|
207
|
-
designlang apply https://stripe.com --dir ./my-app
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Detects Tailwind, shadcn/ui, or plain CSS and writes to the right config files automatically.
|
|
211
|
-
|
|
212
|
-
### 10. Auth Extraction (NEW in v5)
|
|
213
|
-
|
|
214
|
-
Extract from authenticated or protected pages with cookies and custom headers:
|
|
215
|
-
|
|
216
|
-
```bash
|
|
217
|
-
designlang https://internal-app.com --cookie "session=abc123" --header "Authorization:Bearer token"
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### 11. Gradient Extraction (NEW in v5)
|
|
221
|
-
|
|
222
|
-
Detects all CSS gradients — type (linear/radial/conic), direction, color stops, and classifies them as subtle, brand, bold, or complex.
|
|
223
|
-
|
|
224
|
-
### 12. Z-Index Map (NEW in v5)
|
|
225
|
-
|
|
226
|
-
Builds a layer hierarchy from all z-index values, groups them into layers (base, sticky, dropdown, modal, etc.), and flags z-index wars or excessive values (>9999).
|
|
227
|
-
|
|
228
|
-
### 13. SVG Icon Extraction (NEW in v5)
|
|
229
|
-
|
|
230
|
-
Finds and deduplicates all inline SVGs, classifies them by size and style (outline/solid/duotone), and extracts the icon color palette.
|
|
231
|
-
|
|
232
|
-
### 14. Font File Detection (NEW in v5)
|
|
233
|
-
|
|
234
|
-
Identifies every font source — Google Fonts, self-hosted, CDN, or system — and generates ready-to-use `@font-face` CSS.
|
|
235
|
-
|
|
236
|
-
### 15. Image Style Patterns (NEW in v5)
|
|
237
|
-
|
|
238
|
-
Detects image aspect ratios, border treatments, filters, and classifies patterns like avatar, hero, thumbnail, and gallery.
|
|
239
|
-
|
|
240
|
-
### 16. Dark Mode Diffing (NEW in v5)
|
|
241
|
-
|
|
242
|
-
Compare light and dark mode side-by-side — see exactly which colors change and which CSS variables are overridden:
|
|
243
|
-
|
|
244
|
-
```bash
|
|
245
|
-
designlang https://vercel.com --dark
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
### 17. MCP Server (NEW in v7)
|
|
249
|
-
|
|
250
|
-
One-command integration with any MCP-aware AI agent (Cursor, Claude Code, Windsurf, and more):
|
|
251
|
-
|
|
252
|
-
```bash
|
|
253
|
-
designlang mcp --output-dir ./design-extract-output
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
Launches a stdio JSON-RPC server that exposes the extracted design as MCP resources and tools.
|
|
257
|
-
|
|
258
|
-
**Resources:**
|
|
259
|
-
|
|
260
|
-
- `designlang://tokens/primitive` — primitive token layer
|
|
261
|
-
- `designlang://tokens/semantic` — semantic token layer (with DTCG alias references)
|
|
262
|
-
- `designlang://regions` — classified page regions (nav, hero, pricing, etc.)
|
|
263
|
-
- `designlang://components` — reusable component clusters with variants
|
|
264
|
-
- `designlang://health` — CSS health audit
|
|
265
|
-
|
|
266
|
-
**Tools:**
|
|
267
|
-
|
|
268
|
-
- `search_tokens` — query tokens by name, value, or type
|
|
269
|
-
- `find_nearest_color` — snap any color to the nearest palette token
|
|
270
|
-
- `get_region` — fetch a classified region by name
|
|
271
|
-
- `get_component` — fetch a component cluster by id
|
|
272
|
-
- `list_failing_contrast_pairs` — list every WCAG-failing fg/bg pair with remediation suggestions
|
|
273
|
-
|
|
274
|
-
### 18. Multi-Platform Output (NEW in v7)
|
|
275
|
-
|
|
276
|
-
Emit iOS SwiftUI, Android Compose, Flutter, and WordPress block-theme files in a single run, in addition to the default web output:
|
|
277
|
-
|
|
278
|
-
```bash
|
|
279
|
-
designlang https://stripe.com --platforms all
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
Resulting tree:
|
|
283
|
-
|
|
284
|
-
```
|
|
285
|
-
design-extract-output/
|
|
286
|
-
├── stripe-com-*.{md,json,css,js,html} (default web output)
|
|
287
|
-
├── ios/
|
|
288
|
-
│ └── DesignTokens.swift
|
|
289
|
-
├── android/
|
|
290
|
-
│ ├── Theme.kt
|
|
291
|
-
│ ├── colors.xml
|
|
292
|
-
│ └── dimens.xml
|
|
293
|
-
├── flutter/
|
|
294
|
-
│ └── design_tokens.dart (+ buildDesignlangTheme())
|
|
295
|
-
└── wordpress-theme/
|
|
296
|
-
├── theme.json
|
|
297
|
-
├── style.css
|
|
298
|
-
├── functions.php
|
|
299
|
-
├── index.php
|
|
300
|
-
└── templates/index.html
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
Values for `--platforms` are any comma-separated subset of `web,ios,android,flutter,wordpress,all`. The flag is additive — the default web output is always emitted.
|
|
304
|
-
|
|
305
|
-
### 19. Agent Rules Emitter (NEW in v7)
|
|
306
|
-
|
|
307
|
-
Write agent-facing rule files generated from the resolved semantic tokens:
|
|
308
|
-
|
|
309
|
-
```bash
|
|
310
|
-
designlang https://stripe.com --emit-agent-rules
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
Writes:
|
|
314
|
-
|
|
315
|
-
- `.cursor/rules/designlang.mdc` — Cursor rule
|
|
316
|
-
- `.claude/skills/designlang/SKILL.md` — Claude Code skill
|
|
317
|
-
- `CLAUDE.md.fragment` — snippet you can paste into your project's CLAUDE.md
|
|
318
|
-
- `agents.md` — generic, vendor-neutral agent guidance
|
|
319
|
-
|
|
320
|
-
Each file is templated from the semantic layer of the extracted token set, so the agent sees real token names and values — not placeholders.
|
|
321
|
-
|
|
322
|
-
### 20. Stack + Tailwind Fingerprint (NEW in v7)
|
|
323
|
-
|
|
324
|
-
Automatic framework, utility-class, and analytics detection surfaced on `design.stack`:
|
|
325
|
-
|
|
326
|
-
- **Framework**: Next.js, Nuxt, Gatsby, Remix, Astro, Shopify, WordPress, Framer, Webflow, and more.
|
|
327
|
-
- **Tailwind**: when Tailwind is in use, records utility-class frequency so you see which utilities drive the design.
|
|
328
|
-
- **Analytics**: inventory of analytics scripts — GA4, Plausible, PostHog, Segment, Mixpanel, Amplitude, and friends.
|
|
329
|
-
|
|
330
|
-
### 21. CSS Health Audit (NEW in v7)
|
|
331
|
-
|
|
332
|
-
A dedicated audit pass surfaced on `design.cssHealth`:
|
|
333
|
-
|
|
334
|
-
- Specificity graph (distribution, hotspots)
|
|
335
|
-
- `!important` count
|
|
336
|
-
- Duplicate declarations
|
|
337
|
-
- Unused CSS via the Playwright Coverage API
|
|
338
|
-
- `@keyframes` catalog
|
|
339
|
-
- Vendor-prefix audit
|
|
340
|
-
|
|
341
|
-
Also contributes a `cssHealth` dimension to the overall design score.
|
|
342
|
-
|
|
343
|
-
### 22. Chrome Extension (NEW in v7.1)
|
|
344
|
-
|
|
345
|
-
A Manifest-v3 popup lives in [`chrome-extension/`](chrome-extension/). One click on any tab opens `designlang.manavaryasingh.com` with the URL prefilled — no copy-paste, no context switch. There is also a **Copy CLI** button that puts `npx designlang <url>` in your clipboard.
|
|
346
|
-
|
|
347
|
-
- **Permissions:** `activeTab` only, plus host access to the hosted extractor.
|
|
348
|
-
- **Install:** toggle developer mode at `chrome://extensions`, click *Load unpacked*, pick the `chrome-extension/` folder.
|
|
349
|
-
- **Firefox + Edge** work with the same MV3 manifest.
|
|
350
|
-
|
|
351
|
-
### 24. Motion Language (NEW in v9)
|
|
352
|
-
|
|
353
|
-
Extracts the full motion fingerprint, not just transition strings:
|
|
354
|
-
|
|
355
|
-
```bash
|
|
356
|
-
designlang https://linear.app
|
|
357
|
-
# emits linear-app-motion-tokens.json
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
```
|
|
361
|
-
Motion: feel = springy, 2 spring easings, scroll-linked = yes
|
|
362
|
-
Durations: instant (80ms), xs (150ms), sm (220ms), md (380ms)
|
|
363
|
-
Easings: ease-out (61%), spring-overshoot (18%), ease-in-out (21%)
|
|
364
|
-
Keyframes: fade-up (slide-y, used 18x), scale-in (reveal, used 4x)
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
### 25. Component Anatomy v2 (NEW in v9)
|
|
368
|
-
|
|
369
|
-
Every detected component becomes an anatomy tree with typed React stubs:
|
|
370
|
-
|
|
371
|
-
```bash
|
|
372
|
-
designlang https://stripe.com
|
|
373
|
-
# emits stripe-com-anatomy.tsx
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
```tsx
|
|
377
|
-
export interface ButtonProps {
|
|
378
|
-
variant?: 'primary' | 'secondary' | 'ghost';
|
|
379
|
-
size?: 'sm' | 'md' | 'lg';
|
|
380
|
-
disabled?: boolean;
|
|
381
|
-
leadingIcon?: React.ReactNode;
|
|
382
|
-
badge?: React.ReactNode;
|
|
383
|
-
children?: React.ReactNode;
|
|
384
|
-
}
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
### 26. Brand Voice (NEW in v9)
|
|
388
|
-
|
|
389
|
-
Pulls the voice alongside the visual:
|
|
390
|
-
|
|
391
|
-
```bash
|
|
392
|
-
designlang https://vercel.com
|
|
393
|
-
# emits vercel-com-voice.json + a Brand Voice section in the markdown
|
|
37
|
+
npm i -g designlang # global
|
|
38
|
+
npx skills add Manavarya09/design-extract # as an agent skill (40+ agents)
|
|
394
39
|
```
|
|
395
40
|
|
|
396
|
-
|
|
397
|
-
Tone: technical · Pronoun: we→you · Headings: Sentence case (tight)
|
|
398
|
-
Top CTA verbs: start (14), get (8), deploy (5), try (3)
|
|
399
|
-
Sample headings:
|
|
400
|
-
> Develop. Preview. Ship.
|
|
401
|
-
> The React framework for the web.
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
### 27. `designlang lint` — Token Quality Linter (NEW in v9)
|
|
41
|
+
## What you get
|
|
405
42
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
```bash
|
|
409
|
-
designlang lint ./src/tokens/design-tokens.json
|
|
410
|
-
```
|
|
43
|
+
Each run writes 17+ files to `./design-extract-output/`. The headline outputs:
|
|
411
44
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
### 28. `designlang drift` — Codebase ↔ Live Site Sync Check (NEW in v9)
|
|
428
|
-
|
|
429
|
-
Point at a deployed site, pass your local tokens, and get a verdict:
|
|
430
|
-
|
|
431
|
-
```bash
|
|
432
|
-
designlang drift https://yourapp.com --tokens ./src/tokens.json --tolerance 8
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
```
|
|
436
|
-
Verdict: notable-drift (drift ratio: 0.24)
|
|
437
|
-
|
|
438
|
-
| token | local | nearest live | Δ |
|
|
439
|
-
|----------------|----------|--------------------|----|
|
|
440
|
-
| color.primary | #4338CA | #5B4CF5 (primary) | 22 |
|
|
441
|
-
| color.border | #D4D4D8 | #E5E5EA (surface) | 18 |
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
Configurable `--fail-on <level>` for CI: `minor-drift` / `notable-drift` / `major-drift`.
|
|
445
|
-
|
|
446
|
-
### 29. `designlang visual-diff` — Two-URL Side-by-Side (NEW in v9)
|
|
447
|
-
|
|
448
|
-
Capture screenshots + token deltas for two URLs in a single self-contained HTML report:
|
|
449
|
-
|
|
450
|
-
```bash
|
|
451
|
-
designlang visual-diff https://staging.app.com https://app.com
|
|
452
|
-
```
|
|
45
|
+
| File | What it is |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `*-design-language.md` | 19-section markdown — feed any LLM to recreate the design |
|
|
48
|
+
| `*-design-tokens.json` | W3C DTCG tokens (primitive + semantic + composite layers) |
|
|
49
|
+
| `*-tailwind.config.js` | Drop-in Tailwind theme |
|
|
50
|
+
| `*-shadcn-theme.css` | shadcn/ui `globals.css` variables |
|
|
51
|
+
| `*-figma-variables.json` | Figma Variables import (light + dark) |
|
|
52
|
+
| `*-variables.css` | CSS custom properties |
|
|
53
|
+
| `*-anatomy.tsx` | Typed React stubs for every detected component + variants |
|
|
54
|
+
| `*-motion-tokens.json` | Durations, easings, springs, scroll-linked flag |
|
|
55
|
+
| `*-voice.json` | Brand voice — tone, pronoun posture, CTA verbs |
|
|
56
|
+
| `*-prompts/` | Paste-ready prompts for v0, Lovable, Cursor, Claude Artifacts |
|
|
57
|
+
| `*-mcp.json` | Disk-backed MCP server payload |
|
|
58
|
+
| `*-grade.html` | **v12.1** Shareable Design Report Card (letter grade + evidence) |
|
|
453
59
|
|
|
454
|
-
|
|
60
|
+
Multi-platform (`--platforms web,ios,android,flutter,wordpress,all`) adds `ios/`, `android/`, `flutter/`, and a WordPress block theme. `--emit-agent-rules` adds Cursor / Claude Code / generic agent rule files.
|
|
455
61
|
|
|
456
|
-
|
|
62
|
+
## Why designlang vs anything else
|
|
457
63
|
|
|
458
|
-
|
|
64
|
+
Other tools give you the paint. designlang reads the architecture:
|
|
459
65
|
|
|
460
|
-
-
|
|
461
|
-
-
|
|
462
|
-
-
|
|
66
|
+
- **Layout system** — grids, flex containers, container widths, gaps — not just tokens.
|
|
67
|
+
- **Responsive** — crawls 4 breakpoints and reports what changes (`--responsive`).
|
|
68
|
+
- **Interaction states** — programmatically hovers and focuses, captures the deltas (`--interactions`, `--deep-interact`).
|
|
69
|
+
- **Motion language** — durations, easing families, spring detection, scroll-linked flag, `feel` fingerprint (springy / smooth / mechanical / mixed).
|
|
70
|
+
- **Component anatomy** — slot trees with variant × size × state matrices, emitted as typed `.tsx`.
|
|
71
|
+
- **Brand voice** — tone, pronoun posture, heading style, CTA verb inventory.
|
|
72
|
+
- **Page intent + section roles** — `landing` / `pricing` / `docs` etc., with semantic regions (`hero`, `feature-grid`, `pricing-table`, `cta`…).
|
|
73
|
+
- **Multi-page consistency** — auto-discovers canonical pages, reconciles shared vs per-route tokens.
|
|
74
|
+
- **WCAG** — every fg/bg pair scored, with a remediation palette suggesting nearest passing colors.
|
|
75
|
+
- **Drift + lint + visual-diff** — `designlang drift`, `lint`, `visual-diff` all CI-ready, exit non-zero on failure.
|
|
76
|
+
- **Live-site sync** — treat the deployed site as source of truth (`designlang sync`).
|
|
77
|
+
- **MCP server** — `designlang mcp` exposes tokens, regions, components, and contrast pairs to any MCP-aware agent.
|
|
463
78
|
|
|
464
79
|
```bash
|
|
465
|
-
designlang https://
|
|
80
|
+
designlang grade https://stripe.com # ← v12.1: shareable report card
|
|
81
|
+
designlang clone https://stripe.com # → working Next.js app
|
|
82
|
+
designlang apply https://stripe.com -d ./app # auto-detect framework, write tokens
|
|
83
|
+
designlang brands stripe.com vercel.com linear.app # N-brand matrix
|
|
84
|
+
designlang drift https://yourapp.com --tokens ./src/tokens.json
|
|
85
|
+
designlang lint ./src/tokens/design-tokens.json # CI-ready linter
|
|
86
|
+
designlang visual-diff https://staging.app https://app # single-file HTML diff
|
|
87
|
+
designlang mcp # stdio MCP server for Cursor / Claude Code
|
|
466
88
|
```
|
|
467
89
|
|
|
468
|
-
## All
|
|
90
|
+
## All features
|
|
469
91
|
|
|
470
92
|
| Feature | Flag / Command | Description |
|
|
471
93
|
|---------|---------------|-------------|
|
|
@@ -492,6 +114,7 @@ designlang https://staging.internal --cookie-file ./session.json --insecure
|
|
|
492
114
|
| Apply | `designlang apply <url>` | Auto-detect framework and write tokens to your project |
|
|
493
115
|
| Clone | `designlang clone <url>` | Generate a working Next.js starter with extracted design |
|
|
494
116
|
| Score | `designlang score <url>` | Rate design quality with visual bar chart breakdown |
|
|
117
|
+
| Grade (NEW v12.1) | `designlang grade <url>` | Generate a shareable HTML "Design Report Card" — letter grade, 8 dimensions, evidence (palette, type, rhythm), strengths + fixes |
|
|
495
118
|
| Watch | `designlang watch <url>` | Monitor for design changes on interval |
|
|
496
119
|
| Diff | `designlang diff <A> <B>` | Compare two sites (MD + HTML) |
|
|
497
120
|
| Multi-brand | `designlang brands <urls...>` | N-site comparison matrix |
|
|
@@ -544,6 +167,7 @@ Commands:
|
|
|
544
167
|
apply <url> Extract and apply design directly to your project
|
|
545
168
|
clone <url> Generate a working Next.js starter from extracted design
|
|
546
169
|
score <url> Rate design quality (7 categories, A-F, bar chart)
|
|
170
|
+
grade <url> Generate a shareable HTML Design Report Card (--format html|md|json|all, --open)
|
|
547
171
|
watch <url> Monitor for design changes on interval
|
|
548
172
|
diff <urlA> <urlB> Compare two sites' design languages
|
|
549
173
|
brands <urls...> Multi-brand comparison matrix
|
|
@@ -555,56 +179,26 @@ Commands:
|
|
|
555
179
|
visual-diff <before> <after> (v9) Side-by-side HTML diff of two URLs
|
|
556
180
|
```
|
|
557
181
|
|
|
558
|
-
## Example
|
|
182
|
+
## Example output
|
|
559
183
|
|
|
560
|
-
|
|
184
|
+
`designlang https://vercel.com --full` →
|
|
561
185
|
|
|
562
186
|
```
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
✓ vercel-com-variables.css (18.6KB)
|
|
571
|
-
✓ vercel-com-preview.html (31.8KB)
|
|
572
|
-
✓ vercel-com-figma-variables.json (12.4KB)
|
|
573
|
-
✓ vercel-com-theme.js (1.4KB)
|
|
574
|
-
✓ vercel-com-shadcn-theme.css (477B)
|
|
575
|
-
✓ screenshots/button.png
|
|
576
|
-
✓ screenshots/card.png
|
|
577
|
-
✓ screenshots/nav.png
|
|
578
|
-
✓ screenshots/hero.png
|
|
579
|
-
✓ screenshots/full-page.png
|
|
580
|
-
|
|
581
|
-
Summary:
|
|
582
|
-
Colors: 27 unique colors
|
|
583
|
-
Fonts: Geist, Geist Mono
|
|
584
|
-
Spacing: 18 values (base: 2px)
|
|
585
|
-
Shadows: 11 unique shadows
|
|
586
|
-
Radii: 10 unique values
|
|
587
|
-
Breakpoints: 45 breakpoints
|
|
588
|
-
Components: 11 types detected (with CSS snippets)
|
|
589
|
-
CSS Vars: 407 custom properties
|
|
590
|
-
Layout: 55 grids, 492 flex containers
|
|
591
|
-
Gradients: 4 unique gradients
|
|
592
|
-
Z-Index: 8 layers mapped
|
|
593
|
-
Icons: 23 unique SVGs
|
|
594
|
-
Font Files: 4 font sources detected
|
|
595
|
-
Images: 6 style patterns
|
|
596
|
-
Responsive: 4 viewports, 3 breakpoint changes
|
|
597
|
-
Interactions: 8 state changes captured
|
|
598
|
-
A11y: 94% WCAG score (7 failing pairs)
|
|
599
|
-
Design Score: 68/100 (D) — 4 issues
|
|
187
|
+
Colors: 27 · Fonts: Geist + Geist Mono · Spacing: 18 (base 2px)
|
|
188
|
+
Shadows: 11 · Radii: 10 · CSS vars: 407 · Layout: 55 grids / 492 flex
|
|
189
|
+
Responsive: 4 viewports, 3 breakpoint changes · Interactions: 8 transitions
|
|
190
|
+
A11y: 94% WCAG · Score: 68/100 (D) · 4 issues
|
|
191
|
+
|
|
192
|
+
→ 17 files written to ./design-extract-output/
|
|
193
|
+
→ Run `designlang grade https://vercel.com` for a shareable report card
|
|
600
194
|
```
|
|
601
195
|
|
|
602
|
-
## How
|
|
196
|
+
## How it works
|
|
603
197
|
|
|
604
|
-
1. **Crawl** —
|
|
605
|
-
2. **Extract** —
|
|
606
|
-
3. **Process** — 17 extractor modules parse, deduplicate, cluster, and classify the raw data
|
|
607
|
-
4. **Format** —
|
|
198
|
+
1. **Crawl** — Headless Chromium via Playwright, waits for network idle and fonts
|
|
199
|
+
2. **Extract** — One `page.evaluate()` walks up to 5,000 DOM elements, collecting 25+ computed properties, inline SVGs, font sources, and image metadata
|
|
200
|
+
3. **Process** — 17 extractor modules parse, deduplicate, cluster, and classify the raw data
|
|
201
|
+
4. **Format** — 12+ formatter modules emit the output files
|
|
608
202
|
5. **Score** — Accessibility extractor calculates WCAG contrast ratios for all color pairs
|
|
609
203
|
6. **Capture** — Optional: screenshots, responsive viewport crawling, interaction state recording
|
|
610
204
|
|
package/bin/design-extract.js
CHANGED
|
@@ -45,6 +45,7 @@ import { generateClone } from '../src/clone.js';
|
|
|
45
45
|
import { watchSite } from '../src/watch.js';
|
|
46
46
|
import { diffDarkMode } from '../src/darkdiff.js';
|
|
47
47
|
import { applyDesign } from '../src/apply.js';
|
|
48
|
+
import { formatGrade, formatGradeMarkdown } from '../src/formatters/grade.js';
|
|
48
49
|
import { nameFromUrl } from '../src/utils.js';
|
|
49
50
|
|
|
50
51
|
function validateUrl(url) {
|
|
@@ -934,6 +935,81 @@ program
|
|
|
934
935
|
}
|
|
935
936
|
});
|
|
936
937
|
|
|
938
|
+
// ── Grade command — shareable HTML report card ─────────────
|
|
939
|
+
program
|
|
940
|
+
.command('grade <url>')
|
|
941
|
+
.description('Generate a shareable Design Report Card (HTML + JSON + Markdown)')
|
|
942
|
+
.option('-o, --out <dir>', 'output directory', './design-extract-output')
|
|
943
|
+
.option('-n, --name <name>', 'output file prefix (default: derived from URL)')
|
|
944
|
+
.option('--format <fmt>', 'output format: html, md, json, all', 'all')
|
|
945
|
+
.option('--open', 'open the HTML report in the default browser')
|
|
946
|
+
.action(async (url, opts) => {
|
|
947
|
+
if (!url.startsWith('http')) url = `https://${url}`;
|
|
948
|
+
validateUrl(url);
|
|
949
|
+
|
|
950
|
+
const spinner = ora('Auditing design system...').start();
|
|
951
|
+
try {
|
|
952
|
+
const design = await extractDesignLanguage(url);
|
|
953
|
+
const s = design.score;
|
|
954
|
+
if (!s) throw new Error('scoring failed — cannot grade');
|
|
955
|
+
|
|
956
|
+
const outDir = resolve(opts.out);
|
|
957
|
+
mkdirSync(outDir, { recursive: true });
|
|
958
|
+
const prefix = opts.name || nameFromUrl(url);
|
|
959
|
+
const written = [];
|
|
960
|
+
|
|
961
|
+
if (opts.format === 'all' || opts.format === 'html') {
|
|
962
|
+
const html = formatGrade(design, { version: PKG_VERSION });
|
|
963
|
+
const p = join(outDir, `${prefix}.grade.html`);
|
|
964
|
+
writeFileSync(p, html);
|
|
965
|
+
written.push(p);
|
|
966
|
+
}
|
|
967
|
+
if (opts.format === 'all' || opts.format === 'md') {
|
|
968
|
+
const md = formatGradeMarkdown(design);
|
|
969
|
+
const p = join(outDir, `${prefix}.grade.md`);
|
|
970
|
+
writeFileSync(p, md);
|
|
971
|
+
written.push(p);
|
|
972
|
+
}
|
|
973
|
+
if (opts.format === 'all' || opts.format === 'json') {
|
|
974
|
+
const p = join(outDir, `${prefix}.grade.json`);
|
|
975
|
+
writeFileSync(p, JSON.stringify({
|
|
976
|
+
url: design.meta?.url,
|
|
977
|
+
title: design.meta?.title,
|
|
978
|
+
timestamp: design.meta?.timestamp,
|
|
979
|
+
grade: s.grade,
|
|
980
|
+
overall: s.overall,
|
|
981
|
+
scores: s.scores,
|
|
982
|
+
strengths: s.strengths,
|
|
983
|
+
issues: s.issues,
|
|
984
|
+
}, null, 2));
|
|
985
|
+
written.push(p);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
spinner.stop();
|
|
989
|
+
const gradeColor = s.grade === 'A' ? chalk.green : s.grade === 'B' ? chalk.cyan : s.grade === 'C' ? chalk.yellow : chalk.red;
|
|
990
|
+
console.log('');
|
|
991
|
+
console.log(` ${gradeColor.bold(`Grade ${s.grade}`)} ${chalk.gray('·')} ${chalk.bold(`${s.overall}/100`)} ${chalk.gray('·')} ${chalk.gray(url)}`);
|
|
992
|
+
console.log('');
|
|
993
|
+
for (const f of written) console.log(` ${chalk.green('✓')} ${chalk.gray(f)}`);
|
|
994
|
+
console.log('');
|
|
995
|
+
console.log(chalk.gray(` Share: open the .grade.html in a browser, post the URL.`));
|
|
996
|
+
console.log('');
|
|
997
|
+
|
|
998
|
+
if (opts.open) {
|
|
999
|
+
const htmlPath = written.find(p => p.endsWith('.html'));
|
|
1000
|
+
if (htmlPath) {
|
|
1001
|
+
const { spawn } = await import('child_process');
|
|
1002
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
1003
|
+
spawn(cmd, [htmlPath], { detached: true, stdio: 'ignore' }).unref();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
spinner.fail('Grade failed');
|
|
1008
|
+
console.error(chalk.red(`\n ${err.message}\n`));
|
|
1009
|
+
process.exit(1);
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
|
|
937
1013
|
// ── Apply command ──────────────────────────────────────────
|
|
938
1014
|
program
|
|
939
1015
|
.command('apply <url>')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "designlang",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.1.0",
|
|
4
4
|
"description": "Extract the complete design language from any website and ship it — clone to a working Next.js starter, guard tokens with a CI drift bot, or browse everything in a local studio. Outputs W3C DTCG tokens, motion tokens, typed anatomy stubs, Tailwind config, and ready-to-paste v0 / Lovable / Cursor / Claude-Artifacts prompts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// designlang grade — standalone, shareable HTML "Design Report Card".
|
|
2
|
+
// Renders the existing scoring output as an editorial-style audit page that
|
|
3
|
+
// embeds the audited site's own design language (palette, type, spacing) as
|
|
4
|
+
// visual evidence. Self-contained: no external assets except Google Fonts.
|
|
5
|
+
|
|
6
|
+
const FONT_DISPLAY = 'Instrument Serif';
|
|
7
|
+
const FONT_BODY = 'Inter';
|
|
8
|
+
const FONT_MONO = 'JetBrains Mono';
|
|
9
|
+
|
|
10
|
+
const DIMENSIONS = [
|
|
11
|
+
['colorDiscipline', 'Color Discipline', 'Palette breadth, brand clarity, restraint.'],
|
|
12
|
+
['typographyConsistency', 'Typography', 'Family count, weight discipline, scale length.'],
|
|
13
|
+
['spacingSystem', 'Spacing System', 'Base unit fit, value count, rhythm.'],
|
|
14
|
+
['shadowConsistency', 'Elevation', 'Shadow count and tier discipline.'],
|
|
15
|
+
['radiusConsistency', 'Border Radii', 'Radius scale tightness.'],
|
|
16
|
+
['accessibility', 'Accessibility', 'WCAG contrast pass rate.'],
|
|
17
|
+
['tokenization', 'Tokenization', 'CSS variable depth.'],
|
|
18
|
+
['cssHealth', 'CSS Health', 'Unused rules, !important, duplicates.'],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function esc(s) {
|
|
22
|
+
return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function gradeAccent(grade) {
|
|
26
|
+
return ({ A: '#0a8a52', B: '#1f6feb', C: '#b08400', D: '#d2691e', F: '#c43d3d' })[grade] || '#1f1f1f';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function describeScore(n) {
|
|
30
|
+
if (n >= 90) return 'Exemplary';
|
|
31
|
+
if (n >= 80) return 'Strong';
|
|
32
|
+
if (n >= 70) return 'Adequate';
|
|
33
|
+
if (n >= 60) return 'Below standard';
|
|
34
|
+
return 'Needs work';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function arcGauge(value, accent) {
|
|
38
|
+
const v = Math.max(0, Math.min(100, value));
|
|
39
|
+
const r = 36;
|
|
40
|
+
const c = 2 * Math.PI * r;
|
|
41
|
+
const offset = c * (1 - v / 100);
|
|
42
|
+
return `
|
|
43
|
+
<svg viewBox="0 0 88 88" class="gauge" aria-hidden="true">
|
|
44
|
+
<circle cx="44" cy="44" r="${r}" class="gauge-track" fill="none" stroke-width="4"/>
|
|
45
|
+
<circle cx="44" cy="44" r="${r}" class="gauge-fill" fill="none" stroke-width="4"
|
|
46
|
+
stroke="${accent}" stroke-linecap="round"
|
|
47
|
+
stroke-dasharray="${c.toFixed(2)}"
|
|
48
|
+
stroke-dashoffset="${offset.toFixed(2)}"
|
|
49
|
+
transform="rotate(-90 44 44)"/>
|
|
50
|
+
</svg>`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function colorSwatches(design) {
|
|
54
|
+
const all = (design.colors?.all || []).slice(0, 18);
|
|
55
|
+
if (!all.length) return '';
|
|
56
|
+
return all.map(c => {
|
|
57
|
+
const hex = esc(c.hex || c);
|
|
58
|
+
return `<li class="swatch" style="--c:${hex}"><span class="swatch-chip"></span><code>${hex}</code></li>`;
|
|
59
|
+
}).join('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function typeSpecimen(design) {
|
|
63
|
+
const families = (design.typography?.families || []).slice(0, 2).map(familyName).filter(Boolean);
|
|
64
|
+
const sizeOf = s => typeof s === 'number' ? s : (s?.size ?? 0);
|
|
65
|
+
const allScale = (design.typography?.scale || []).map(sizeOf).filter(n => n > 0).sort((a, b) => b - a);
|
|
66
|
+
const scale = allScale.slice(0, 5);
|
|
67
|
+
if (!families.length) return '';
|
|
68
|
+
const head = families[0];
|
|
69
|
+
const weights = (design.typography?.weights || []).map(w => typeof w === 'object' ? (w.value || w.weight) : w).filter(Boolean);
|
|
70
|
+
return `
|
|
71
|
+
<div class="specimen" style="font-family: ${esc(head)}, ${FONT_DISPLAY}, serif">
|
|
72
|
+
${scale.map((s, i) => {
|
|
73
|
+
const lines = [
|
|
74
|
+
'The quiet authority of restraint.',
|
|
75
|
+
'How the page reads at rest.',
|
|
76
|
+
'Form follows feeling.',
|
|
77
|
+
'Calm hierarchy is a craft.',
|
|
78
|
+
'Notes from the audit.',
|
|
79
|
+
];
|
|
80
|
+
return `<div class="spec-line" style="font-size:${Math.min(s, 72)}px">${lines[i] || lines[0]}</div>`;
|
|
81
|
+
}).join('')}
|
|
82
|
+
</div>
|
|
83
|
+
<div class="specimen-meta">
|
|
84
|
+
<span>Families · ${families.map(esc).join(' / ')}</span>
|
|
85
|
+
<span>Scale · ${(design.typography?.scale || []).length} sizes</span>
|
|
86
|
+
<span>Weights · ${weights.join(', ') || '—'}</span>
|
|
87
|
+
</div>`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function spacingScale(design) {
|
|
91
|
+
const raw = (design.spacing?.scale || []).map(v => typeof v === 'number' ? v : (v?.value ?? v?.size ?? 0)).filter(n => n > 0);
|
|
92
|
+
const scale = raw.slice().sort((a, b) => a - b).slice(0, 10);
|
|
93
|
+
if (!scale.length) return '';
|
|
94
|
+
return scale.map(v => `<div class="rhythm-bar" style="width:${Math.min(v * 1.6, 220)}px"><span>${v}</span></div>`).join('');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function familyName(f) {
|
|
98
|
+
if (!f) return '';
|
|
99
|
+
if (typeof f === 'string') return f;
|
|
100
|
+
return f.name || f.family || '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function fontHref(family) {
|
|
104
|
+
const name = familyName(family);
|
|
105
|
+
if (!name) return '';
|
|
106
|
+
const f = name.replace(/['"]/g, '').split(',')[0].trim();
|
|
107
|
+
if (!f) return '';
|
|
108
|
+
return `https://fonts.googleapis.com/css2?family=${encodeURIComponent(f).replace(/%20/g, '+')}:wght@400;500;700&display=swap`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function formatGrade(design, opts = {}) {
|
|
112
|
+
const s = design.score;
|
|
113
|
+
if (!s) throw new Error('grade: design.score missing — extract failed to score');
|
|
114
|
+
|
|
115
|
+
const accent = gradeAccent(s.grade);
|
|
116
|
+
const headFamily = (design.typography?.families || [])[0];
|
|
117
|
+
const headHref = fontHref(headFamily);
|
|
118
|
+
const url = design.meta?.url || '';
|
|
119
|
+
const title = design.meta?.title || url;
|
|
120
|
+
const host = (() => { try { return new URL(url).hostname; } catch { return url; } })();
|
|
121
|
+
const date = new Date(design.meta?.timestamp || Date.now()).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
|
122
|
+
const issues = s.issues || [];
|
|
123
|
+
const strengths = s.strengths || [];
|
|
124
|
+
const ogTitle = `${host} — Grade ${s.grade}`;
|
|
125
|
+
const ogDesc = `Design system audit by designlang. ${s.overall}/100 across 8 dimensions.`;
|
|
126
|
+
|
|
127
|
+
const dims = DIMENSIONS
|
|
128
|
+
.filter(([k]) => s.scores[k] !== undefined)
|
|
129
|
+
.map(([k, label, blurb]) => {
|
|
130
|
+
const score = Math.round(s.scores[k]);
|
|
131
|
+
const dimAccent = score >= 80 ? '#0a8a52' : score >= 60 ? '#b08400' : '#c43d3d';
|
|
132
|
+
return `
|
|
133
|
+
<article class="dim">
|
|
134
|
+
<div class="dim-gauge">${arcGauge(score, dimAccent)}<span class="dim-score">${score}</span></div>
|
|
135
|
+
<div class="dim-body">
|
|
136
|
+
<h3>${esc(label)}</h3>
|
|
137
|
+
<p class="dim-blurb">${esc(blurb)}</p>
|
|
138
|
+
<p class="dim-verdict" style="color:${dimAccent}">${describeScore(score)}</p>
|
|
139
|
+
</div>
|
|
140
|
+
</article>`;
|
|
141
|
+
}).join('');
|
|
142
|
+
|
|
143
|
+
return `<!doctype html>
|
|
144
|
+
<html lang="en">
|
|
145
|
+
<head>
|
|
146
|
+
<meta charset="utf-8">
|
|
147
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
148
|
+
<title>${esc(ogTitle)} · designlang</title>
|
|
149
|
+
<meta name="description" content="${esc(ogDesc)}">
|
|
150
|
+
<meta property="og:title" content="${esc(ogTitle)}">
|
|
151
|
+
<meta property="og:description" content="${esc(ogDesc)}">
|
|
152
|
+
<meta property="og:type" content="article">
|
|
153
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
154
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
155
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
156
|
+
<link href="https://fonts.googleapis.com/css2?family=${encodeURIComponent(FONT_DISPLAY)}&family=${encodeURIComponent(FONT_BODY)}:wght@400;500;600&family=${encodeURIComponent(FONT_MONO)}:wght@400;500&display=swap" rel="stylesheet">
|
|
157
|
+
${headHref ? `<link href="${esc(headHref)}" rel="stylesheet">` : ''}
|
|
158
|
+
<style>
|
|
159
|
+
:root {
|
|
160
|
+
--paper: #f7f5ef;
|
|
161
|
+
--ink: #141414;
|
|
162
|
+
--ink-soft: #555049;
|
|
163
|
+
--rule: #e5e1d6;
|
|
164
|
+
--accent: ${accent};
|
|
165
|
+
--display: '${FONT_DISPLAY}', 'Times New Roman', serif;
|
|
166
|
+
--body: '${FONT_BODY}', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
|
167
|
+
--mono: '${FONT_MONO}', ui-monospace, 'SF Mono', monospace;
|
|
168
|
+
}
|
|
169
|
+
[data-theme="dark"] {
|
|
170
|
+
--paper: #0e0d0b;
|
|
171
|
+
--ink: #f0ece2;
|
|
172
|
+
--ink-soft: #9b9589;
|
|
173
|
+
--rule: #2a2823;
|
|
174
|
+
}
|
|
175
|
+
* { box-sizing: border-box; }
|
|
176
|
+
html, body { margin: 0; padding: 0; }
|
|
177
|
+
body {
|
|
178
|
+
background: var(--paper);
|
|
179
|
+
color: var(--ink);
|
|
180
|
+
font-family: var(--body);
|
|
181
|
+
font-size: 16px;
|
|
182
|
+
line-height: 1.55;
|
|
183
|
+
-webkit-font-smoothing: antialiased;
|
|
184
|
+
-moz-osx-font-smoothing: grayscale;
|
|
185
|
+
transition: background .25s ease, color .25s ease;
|
|
186
|
+
}
|
|
187
|
+
.wrap { max-width: 920px; margin: 0 auto; padding: 56px 40px 96px; }
|
|
188
|
+
@media (max-width: 640px) { .wrap { padding: 32px 22px 64px; } }
|
|
189
|
+
|
|
190
|
+
/* — Top bar — */
|
|
191
|
+
.topbar { display:flex; justify-content:space-between; align-items:center; margin-bottom: 72px; font-size: 13px; }
|
|
192
|
+
.brand { font-family: var(--display); font-size: 22px; letter-spacing: .01em; }
|
|
193
|
+
.brand a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--rule); padding-bottom: 1px; }
|
|
194
|
+
.topbar nav { display:flex; gap: 18px; align-items: center; color: var(--ink-soft); }
|
|
195
|
+
.theme-btn {
|
|
196
|
+
background: transparent; border: 1px solid var(--rule); color: var(--ink-soft);
|
|
197
|
+
font-family: var(--body); font-size: 12px; padding: 6px 12px; border-radius: 999px; cursor: pointer;
|
|
198
|
+
letter-spacing: .04em; text-transform: uppercase;
|
|
199
|
+
}
|
|
200
|
+
.theme-btn:hover { color: var(--ink); border-color: var(--ink); }
|
|
201
|
+
|
|
202
|
+
/* — Hero — */
|
|
203
|
+
.hero { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 32px; align-items: end; padding-bottom: 56px; border-bottom: 1px solid var(--rule); }
|
|
204
|
+
.kicker { text-transform: uppercase; letter-spacing: .18em; font-size: 11px; color: var(--ink-soft); margin-bottom: 18px; }
|
|
205
|
+
.hero h1 { font-family: var(--display); font-weight: 400; font-size: clamp(40px, 6vw, 72px); line-height: 1.02; margin: 0 0 18px; letter-spacing: -.01em; }
|
|
206
|
+
.hero h1 em { font-style: italic; color: var(--accent); }
|
|
207
|
+
.hero h1 a { color: var(--ink); text-decoration: none; border-bottom: 2px solid var(--accent); padding-bottom: 2px; transition: color .15s; }
|
|
208
|
+
.hero h1 a:hover { color: var(--accent); }
|
|
209
|
+
.hero .subject { font-size: 15px; color: var(--ink-soft); margin: 0; }
|
|
210
|
+
.hero .subject a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--rule); }
|
|
211
|
+
.hero .meta { display:flex; gap: 18px; margin-top: 22px; font-size: 12px; color: var(--ink-soft); font-family: var(--mono); text-transform: uppercase; letter-spacing: .08em; }
|
|
212
|
+
.grade-block { text-align: center; }
|
|
213
|
+
.grade-letter {
|
|
214
|
+
font-family: var(--display); font-size: clamp(180px, 26vw, 280px); line-height: .82; color: var(--accent);
|
|
215
|
+
font-weight: 400; letter-spacing: -.04em; margin: 0;
|
|
216
|
+
animation: gradeIn .9s cubic-bezier(.2,.8,.2,1);
|
|
217
|
+
}
|
|
218
|
+
.grade-score { font-family: var(--mono); font-size: 14px; color: var(--ink-soft); letter-spacing: .04em; margin-top: 8px; }
|
|
219
|
+
@keyframes gradeIn { from { opacity: 0; transform: translateY(12px) scale(.96); } to { opacity: 1; transform: none; } }
|
|
220
|
+
@media (max-width: 640px) { .hero { grid-template-columns: 1fr; gap: 0; } .grade-block { text-align: left; margin-top: 24px; } }
|
|
221
|
+
|
|
222
|
+
/* — Section frame — */
|
|
223
|
+
section { padding: 64px 0; border-bottom: 1px solid var(--rule); }
|
|
224
|
+
section:last-of-type { border-bottom: 0; }
|
|
225
|
+
section > h2 { font-family: var(--display); font-weight: 400; font-size: 32px; margin: 0 0 8px; letter-spacing: -.005em; }
|
|
226
|
+
section > h2 + .lead { color: var(--ink-soft); margin: 0 0 36px; max-width: 60ch; }
|
|
227
|
+
|
|
228
|
+
/* — Dimensions grid — */
|
|
229
|
+
.dims { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px 48px; }
|
|
230
|
+
@media (max-width: 640px) { .dims { grid-template-columns: 1fr; gap: 28px; } }
|
|
231
|
+
.dim { display: grid; grid-template-columns: 88px 1fr; gap: 18px; align-items: start; }
|
|
232
|
+
.dim-gauge { position: relative; width: 88px; height: 88px; }
|
|
233
|
+
.gauge { width: 100%; height: 100%; }
|
|
234
|
+
.gauge-track { stroke: var(--rule); }
|
|
235
|
+
.gauge-fill { stroke-dasharray: var(--c, 226); animation: arc 1.1s cubic-bezier(.2,.8,.2,1) both; }
|
|
236
|
+
@keyframes arc { from { stroke-dashoffset: 226; } }
|
|
237
|
+
.dim-score { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--mono); font-weight: 500; font-size: 18px; }
|
|
238
|
+
.dim h3 { font-family: var(--display); font-weight: 400; font-size: 22px; margin: 4px 0 6px; }
|
|
239
|
+
.dim-blurb { color: var(--ink-soft); font-size: 14px; margin: 0 0 8px; }
|
|
240
|
+
.dim-verdict { font-size: 12px; font-family: var(--mono); text-transform: uppercase; letter-spacing: .08em; margin: 0; }
|
|
241
|
+
|
|
242
|
+
/* — Lists — */
|
|
243
|
+
.ledger { display: grid; grid-template-columns: repeat(2, 1fr); gap: 48px; }
|
|
244
|
+
@media (max-width: 640px) { .ledger { grid-template-columns: 1fr; gap: 36px; } }
|
|
245
|
+
.ledger h3 { font-family: var(--display); font-weight: 400; font-size: 22px; margin: 0 0 16px; }
|
|
246
|
+
.ledger ul { list-style: none; padding: 0; margin: 0; }
|
|
247
|
+
.ledger li { padding: 14px 0; border-top: 1px solid var(--rule); display: flex; gap: 14px; align-items: baseline; }
|
|
248
|
+
.ledger li:last-child { border-bottom: 1px solid var(--rule); }
|
|
249
|
+
.ledger .marker { font-family: var(--mono); font-size: 11px; color: var(--ink-soft); flex: 0 0 24px; padding-top: 2px; letter-spacing: .04em; }
|
|
250
|
+
.ledger li p { margin: 0; font-size: 15px; line-height: 1.5; }
|
|
251
|
+
.ledger.empty p { color: var(--ink-soft); font-style: italic; font-family: var(--display); font-size: 16px; }
|
|
252
|
+
|
|
253
|
+
/* — Evidence — */
|
|
254
|
+
.swatches { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 10px; }
|
|
255
|
+
.swatch { display: flex; align-items: center; gap: 8px; padding: 6px 10px 6px 6px; background: rgba(0,0,0,.02); border: 1px solid var(--rule); border-radius: 6px; }
|
|
256
|
+
[data-theme="dark"] .swatch { background: rgba(255,255,255,.03); }
|
|
257
|
+
.swatch-chip { width: 22px; height: 22px; border-radius: 4px; background: var(--c); box-shadow: inset 0 0 0 1px rgba(0,0,0,.06); flex: 0 0 auto; }
|
|
258
|
+
.swatch code { font-family: var(--mono); font-size: 11px; color: var(--ink-soft); }
|
|
259
|
+
|
|
260
|
+
.specimen { padding: 24px 0; }
|
|
261
|
+
.spec-line { line-height: 1.05; margin: 0 0 14px; letter-spacing: -.01em; }
|
|
262
|
+
.spec-line:nth-child(2) { color: var(--ink-soft); font-style: italic; }
|
|
263
|
+
.specimen-meta { display: flex; flex-wrap: wrap; gap: 24px; font-family: var(--mono); font-size: 11px; color: var(--ink-soft); text-transform: uppercase; letter-spacing: .06em; padding-top: 18px; border-top: 1px solid var(--rule); }
|
|
264
|
+
|
|
265
|
+
.rhythm { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; }
|
|
266
|
+
.rhythm-bar { height: 14px; background: var(--accent); opacity: .82; border-radius: 2px; display: flex; align-items: center; padding-left: 10px; transition: opacity .15s; }
|
|
267
|
+
.rhythm-bar span { font-family: var(--mono); font-size: 10px; color: var(--paper); mix-blend-mode: difference; filter: invert(1); }
|
|
268
|
+
.rhythm-bar:hover { opacity: 1; }
|
|
269
|
+
|
|
270
|
+
/* — Footer — */
|
|
271
|
+
footer { padding: 48px 0 0; font-size: 13px; color: var(--ink-soft); display: flex; justify-content: space-between; align-items: end; flex-wrap: wrap; gap: 16px; }
|
|
272
|
+
footer .sig { font-family: var(--display); font-size: 22px; color: var(--ink); }
|
|
273
|
+
footer a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--rule); }
|
|
274
|
+
footer .stamp { font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }
|
|
275
|
+
|
|
276
|
+
/* — Print — */
|
|
277
|
+
@media print {
|
|
278
|
+
body { background: white; color: black; }
|
|
279
|
+
.topbar nav, .theme-btn { display: none; }
|
|
280
|
+
section, .hero { page-break-inside: avoid; border-color: #ddd; }
|
|
281
|
+
.grade-letter { color: black; }
|
|
282
|
+
}
|
|
283
|
+
</style>
|
|
284
|
+
</head>
|
|
285
|
+
<body>
|
|
286
|
+
<div class="wrap">
|
|
287
|
+
<header class="topbar">
|
|
288
|
+
<div class="brand"><a href="https://designlang.dev">designlang</a></div>
|
|
289
|
+
<nav>
|
|
290
|
+
<span>Report Card</span>
|
|
291
|
+
<button class="theme-btn" id="themeBtn" type="button">Theme</button>
|
|
292
|
+
</nav>
|
|
293
|
+
</header>
|
|
294
|
+
|
|
295
|
+
<div class="hero">
|
|
296
|
+
<div>
|
|
297
|
+
<p class="kicker">Design Audit · ${esc(date)}</p>
|
|
298
|
+
<h1>An <em>independent reading</em> of the design system at <a href="${esc(url)}" target="_blank" rel="noopener">${esc(host)}</a>.</h1>
|
|
299
|
+
<p class="subject">${esc(title)}</p>
|
|
300
|
+
<div class="meta">
|
|
301
|
+
<span>${(design.colors?.all || []).length} colors</span>
|
|
302
|
+
<span>${(design.typography?.scale || []).length} sizes</span>
|
|
303
|
+
<span>${(design.spacing?.scale || []).length} spacings</span>
|
|
304
|
+
<span>${(design.shadows?.values || []).length} shadows</span>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
<div class="grade-block">
|
|
308
|
+
<div class="grade-letter">${esc(s.grade)}</div>
|
|
309
|
+
<div class="grade-score">${s.overall} / 100</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<section>
|
|
314
|
+
<h2>Eight dimensions, scored.</h2>
|
|
315
|
+
<p class="lead">Each dimension is graded against calibrated thresholds drawn from production design systems (Stripe, Linear, Vercel, GitHub, Apple). The number is the headline; the prose underneath is what to do next.</p>
|
|
316
|
+
<div class="dims">${dims}</div>
|
|
317
|
+
</section>
|
|
318
|
+
|
|
319
|
+
<section>
|
|
320
|
+
<h2>What's working. What to fix.</h2>
|
|
321
|
+
<div class="ledger">
|
|
322
|
+
<div class="${strengths.length ? '' : 'empty'}">
|
|
323
|
+
<h3>Strengths</h3>
|
|
324
|
+
${strengths.length
|
|
325
|
+
? `<ul>${strengths.map((str, i) => `<li><span class="marker">${String(i + 1).padStart(2, '0')}</span><p>${esc(str)}</p></li>`).join('')}</ul>`
|
|
326
|
+
: `<p>No standout strengths surfaced — the system is uniformly mid-tier.</p>`}
|
|
327
|
+
</div>
|
|
328
|
+
<div class="${issues.length ? '' : 'empty'}">
|
|
329
|
+
<h3>What to fix</h3>
|
|
330
|
+
${issues.length
|
|
331
|
+
? `<ul>${issues.map((iss, i) => `<li><span class="marker">${String(i + 1).padStart(2, '0')}</span><p>${esc(iss)}</p></li>`).join('')}</ul>`
|
|
332
|
+
: `<p>Nothing material flagged. Polish, then ship.</p>`}
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
</section>
|
|
336
|
+
|
|
337
|
+
<section>
|
|
338
|
+
<h2>Evidence.</h2>
|
|
339
|
+
<p class="lead">The audit reads from the page itself. Here is what the auditor saw — palette, type, rhythm — drawn straight from the live styles.</p>
|
|
340
|
+
|
|
341
|
+
<h3 style="font-family:var(--display);font-weight:400;font-size:18px;margin:24px 0 12px;color:var(--ink-soft)">Palette</h3>
|
|
342
|
+
<ul class="swatches">${colorSwatches(design)}</ul>
|
|
343
|
+
|
|
344
|
+
<h3 style="font-family:var(--display);font-weight:400;font-size:18px;margin:32px 0 0;color:var(--ink-soft)">Type</h3>
|
|
345
|
+
${typeSpecimen(design)}
|
|
346
|
+
|
|
347
|
+
<h3 style="font-family:var(--display);font-weight:400;font-size:18px;margin:32px 0 0;color:var(--ink-soft)">Spacing rhythm</h3>
|
|
348
|
+
<div class="rhythm">${spacingScale(design)}</div>
|
|
349
|
+
</section>
|
|
350
|
+
|
|
351
|
+
<footer>
|
|
352
|
+
<div>
|
|
353
|
+
<div class="sig">designlang</div>
|
|
354
|
+
<div>Audit any site: <code style="font-family:var(--mono)">npx designlang grade ${esc(host)}</code></div>
|
|
355
|
+
</div>
|
|
356
|
+
<div class="stamp">${esc(date)} · v${esc(opts.version || '')}</div>
|
|
357
|
+
</footer>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<script>
|
|
361
|
+
(function () {
|
|
362
|
+
var btn = document.getElementById('themeBtn');
|
|
363
|
+
var saved = null;
|
|
364
|
+
try { saved = localStorage.getItem('dl-theme'); } catch (e) {}
|
|
365
|
+
if (saved) document.documentElement.setAttribute('data-theme', saved);
|
|
366
|
+
btn && btn.addEventListener('click', function () {
|
|
367
|
+
var cur = document.documentElement.getAttribute('data-theme') === 'dark' ? '' : 'dark';
|
|
368
|
+
if (cur) document.documentElement.setAttribute('data-theme', cur);
|
|
369
|
+
else document.documentElement.removeAttribute('data-theme');
|
|
370
|
+
try { localStorage.setItem('dl-theme', cur); } catch (e) {}
|
|
371
|
+
});
|
|
372
|
+
})();
|
|
373
|
+
</script>
|
|
374
|
+
</body>
|
|
375
|
+
</html>`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function formatGradeMarkdown(design) {
|
|
379
|
+
const s = design.score;
|
|
380
|
+
if (!s) throw new Error('grade: design.score missing');
|
|
381
|
+
const url = design.meta?.url || '';
|
|
382
|
+
const date = new Date(design.meta?.timestamp || Date.now()).toISOString().split('T')[0];
|
|
383
|
+
const lines = [
|
|
384
|
+
`# Design Report Card — ${url}`,
|
|
385
|
+
``,
|
|
386
|
+
`**Grade ${s.grade}** · ${s.overall}/100 · _${date}_`,
|
|
387
|
+
``,
|
|
388
|
+
`## Dimensions`,
|
|
389
|
+
``,
|
|
390
|
+
`| Dimension | Score | Verdict |`,
|
|
391
|
+
`|---|---|---|`,
|
|
392
|
+
...DIMENSIONS.filter(([k]) => s.scores[k] !== undefined).map(([k, label]) =>
|
|
393
|
+
`| ${label} | ${Math.round(s.scores[k])}/100 | ${describeScore(s.scores[k])} |`),
|
|
394
|
+
``,
|
|
395
|
+
];
|
|
396
|
+
if (s.strengths?.length) {
|
|
397
|
+
lines.push(`## Strengths`, ``, ...s.strengths.map(x => `- ${x}`), ``);
|
|
398
|
+
}
|
|
399
|
+
if (s.issues?.length) {
|
|
400
|
+
lines.push(`## What to fix`, ``, ...s.issues.map(x => `- ${x}`), ``);
|
|
401
|
+
}
|
|
402
|
+
lines.push(`---`, `_Audited by [designlang](https://designlang.dev) · \`npx designlang grade ${url}\`_`);
|
|
403
|
+
return lines.join('\n');
|
|
404
|
+
}
|