dashtaste 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +129 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +124 -0
- package/dist/color.d.ts +4 -0
- package/dist/color.js +43 -0
- package/dist/engine.d.ts +11 -0
- package/dist/engine.js +39 -0
- package/dist/extract-in-page.d.ts +3 -0
- package/dist/extract-in-page.js +149 -0
- package/dist/extract.d.ts +3 -0
- package/dist/extract.js +39 -0
- package/dist/format.d.ts +18 -0
- package/dist/format.js +65 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/packs/cn.d.ts +2 -0
- package/dist/packs/cn.js +56 -0
- package/dist/packs/global.d.ts +2 -0
- package/dist/packs/global.js +45 -0
- package/dist/packs/index.d.ts +53 -0
- package/dist/packs/index.js +13 -0
- package/dist/rules/dt001-default-palette.d.ts +3 -0
- package/dist/rules/dt001-default-palette.js +11 -0
- package/dist/rules/dt002-rainbow-palette.d.ts +3 -0
- package/dist/rules/dt002-rainbow-palette.js +11 -0
- package/dist/rules/dt003-smooth-line.d.ts +3 -0
- package/dist/rules/dt003-smooth-line.js +9 -0
- package/dist/rules/dt004-symbols-everywhere.d.ts +3 -0
- package/dist/rules/dt004-symbols-everywhere.js +21 -0
- package/dist/rules/dt005-pie-slices.d.ts +3 -0
- package/dist/rules/dt005-pie-slices.js +13 -0
- package/dist/rules/dt006-pie-overuse.d.ts +3 -0
- package/dist/rules/dt006-pie-overuse.js +14 -0
- package/dist/rules/dt007-three-d.d.ts +3 -0
- package/dist/rules/dt007-three-d.js +16 -0
- package/dist/rules/dt008-dual-axis.d.ts +3 -0
- package/dist/rules/dt008-dual-axis.js +10 -0
- package/dist/rules/dt009-missing-unit.d.ts +3 -0
- package/dist/rules/dt009-missing-unit.js +8 -0
- package/dist/rules/dt010-bar-baseline.d.ts +3 -0
- package/dist/rules/dt010-bar-baseline.js +11 -0
- package/dist/rules/dt011-raw-large-numbers.d.ts +3 -0
- package/dist/rules/dt011-raw-large-numbers.js +21 -0
- package/dist/rules/dt012-unit-system.d.ts +3 -0
- package/dist/rules/dt012-unit-system.js +12 -0
- package/dist/rules/dt013-delta-convention.d.ts +3 -0
- package/dist/rules/dt013-delta-convention.js +21 -0
- package/dist/rules/dt014-delta-colour-only.d.ts +3 -0
- package/dist/rules/dt014-delta-colour-only.js +16 -0
- package/dist/rules/dt015-low-contrast-text.d.ts +3 -0
- package/dist/rules/dt015-low-contrast-text.js +28 -0
- package/dist/rules/dt016-thin-line-contrast.d.ts +3 -0
- package/dist/rules/dt016-thin-line-contrast.js +12 -0
- package/dist/rules/dt017-gradient-decoration.d.ts +3 -0
- package/dist/rules/dt017-gradient-decoration.js +15 -0
- package/dist/rules/dt018-glow.d.ts +3 -0
- package/dist/rules/dt018-glow.js +27 -0
- package/dist/rules/dt019-kpi-overload.d.ts +3 -0
- package/dist/rules/dt019-kpi-overload.js +7 -0
- package/dist/rules/dt020-kpi-rainbow.d.ts +3 -0
- package/dist/rules/dt020-kpi-rainbow.js +27 -0
- package/dist/rules/dt021-emoji-icons.d.ts +3 -0
- package/dist/rules/dt021-emoji-icons.js +10 -0
- package/dist/rules/dt022-cjk-min-size.d.ts +3 -0
- package/dist/rules/dt022-cjk-min-size.js +9 -0
- package/dist/rules/dt023-legend-single-series.d.ts +3 -0
- package/dist/rules/dt023-legend-single-series.js +8 -0
- package/dist/rules/dt024-bar-width.d.ts +1 -0
- package/dist/rules/dt024-bar-width.js +14 -0
- package/dist/rules/dt025-legend-not-direct-label.d.ts +1 -0
- package/dist/rules/dt025-legend-not-direct-label.js +11 -0
- package/dist/rules/dt026-line-weight.d.ts +1 -0
- package/dist/rules/dt026-line-weight.js +8 -0
- package/dist/rules/dt027-tabular-numerals.d.ts +1 -0
- package/dist/rules/dt027-tabular-numerals.js +16 -0
- package/dist/rules/dt028-hero-metric.d.ts +1 -0
- package/dist/rules/dt028-hero-metric.js +14 -0
- package/dist/rules/helpers.d.ts +32 -0
- package/dist/rules/helpers.js +161 -0
- package/dist/rules/index.d.ts +2 -0
- package/dist/rules/index.js +58 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.js +1 -0
- package/docs/rules.md +735 -0
- package/docs/usage.md +126 -0
- package/package.json +74 -4
- package/skills/dashtaste-cn/SKILL.md +51 -0
- package/skills/dashtaste-cn/references/avoid.md +135 -0
- package/skills/dashtaste-cn/references/charts.md +122 -0
- package/skills/dashtaste-cn/references/layout.md +78 -0
- package/skills/dashtaste-cn/references/numbers.md +93 -0
- package/skills/dashtaste-cn/references/tokens.md +72 -0
- package/skills/dashtaste-global/SKILL.md +43 -0
- package/skills/dashtaste-global/references/avoid.md +135 -0
- package/skills/dashtaste-global/references/charts.md +122 -0
- package/skills/dashtaste-global/references/layout.md +78 -0
- package/skills/dashtaste-global/references/numbers.md +83 -0
- package/skills/dashtaste-global/references/tokens.md +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Felix Fang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,130 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dashtaste
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Two style-pack skills for coding agents, plus a `dashtaste check` CLI for chart configs and rendered dashboards.
|
|
4
|
+
|
|
5
|
+
| Before | After |
|
|
6
|
+
| ----------------------------------- | --------------------------------- |
|
|
7
|
+
|  |  |
|
|
8
|
+
|
|
9
|
+
## Install the skill
|
|
10
|
+
|
|
11
|
+
Choose a pack for the dashboard's audience:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx skills add FelixFangX/dashtaste --skill dashtaste-global
|
|
15
|
+
npx skills add FelixFangX/dashtaste --skill dashtaste-cn
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For manual installation, copy the folder from [skills/](skills/) into your agent's skills directory. Ask the agent to follow the selected skill when building or reviewing a dashboard.
|
|
19
|
+
|
|
20
|
+
## Check
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npx dashtaste check dashboard.json --pack cn
|
|
24
|
+
npx dashtaste check http://localhost:5173 --pack global
|
|
25
|
+
npx dashtaste check dashboard.json --pack cn --format json
|
|
26
|
+
npx dashtaste check dashboard.json --pack cn --disable DT003,DT005
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires Node 20.19 or newer. Checking a URL or HTML file also needs a local Chromium (set `CHROME_PATH`) and the optional `playwright-core` dependency; JSON configs need neither.
|
|
30
|
+
|
|
31
|
+
To run from a checkout instead, use pnpm:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pnpm install
|
|
35
|
+
pnpm build
|
|
36
|
+
node dist/cli.js check gallery/pages/store/before.html --pack cn --only DT003,DT005,DT010
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Actual output from that three-rule gallery check:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
gallery/pages/store/before.html
|
|
43
|
+
warn DT003 charts[0].series[1] smooth line → use straight segments: smooth: false
|
|
44
|
+
warn DT005 charts[1].series[0] pie slices → use horizontal bars or combine small parts into Other
|
|
45
|
+
error DT010 charts[0].yAxis[0] bar baseline → set min: 0 and scale: false
|
|
46
|
+
1 error, 2 warnings · score 84/100
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Exit codes: `0` passes, `1` means errors or warnings above the limit, and `2` means invalid input or unavailable extraction. Warnings have no limit by default; use `--max-warnings 0` to require none. Use `--mode dark` for dark pages. JSON output includes findings, paths, fixes, and scores. Every rule can be disabled with `--disable` and comma-separated IDs.
|
|
50
|
+
|
|
51
|
+
Scores start at 100, subtract 10 per error, 3 per warning, and 1 per informational finding, with a floor of zero. See [usage](docs/usage.md) for all flags and input shapes.
|
|
52
|
+
|
|
53
|
+
## Packs
|
|
54
|
+
|
|
55
|
+
| Choice | global | cn |
|
|
56
|
+
| ------------- | -------------------------------- | --------------------------------------------------------- |
|
|
57
|
+
| Deltas | Green up, red down; arrows | Red up, green down; arrows |
|
|
58
|
+
| Units | K / M / B | wan / yi (10⁴ / 10⁸) |
|
|
59
|
+
| KPI layout | One strip with dividers | Flat cards; period-over-period and year-over-year rows |
|
|
60
|
+
| Legend | Omit for one series | Omit for one series; direct category labels work for pies |
|
|
61
|
+
| Pie | At most five slices; prefer bars | At most five slices; rose charts allow eight |
|
|
62
|
+
| Dark / screen | Paper and ink dark tokens | Navy panels; 1920×1080 screen composition |
|
|
63
|
+
|
|
64
|
+
Both packs specify spacing, typography, chart defaults, and number formatting. Layout still needs human review.
|
|
65
|
+
|
|
66
|
+
## Rules
|
|
67
|
+
|
|
68
|
+
[Reasons, triggers, and fixes](docs/rules.md) for every rule:
|
|
69
|
+
|
|
70
|
+
| ID | Name |
|
|
71
|
+
| ----- | -------------------- |
|
|
72
|
+
| DT001 | default-palette |
|
|
73
|
+
| DT002 | rainbow-palette |
|
|
74
|
+
| DT003 | smooth-line |
|
|
75
|
+
| DT004 | symbols-everywhere |
|
|
76
|
+
| DT005 | pie-slices |
|
|
77
|
+
| DT006 | pie-overuse |
|
|
78
|
+
| DT007 | three-d |
|
|
79
|
+
| DT008 | dual-axis |
|
|
80
|
+
| DT009 | missing-unit |
|
|
81
|
+
| DT010 | bar-baseline |
|
|
82
|
+
| DT011 | raw-large-numbers |
|
|
83
|
+
| DT012 | unit-system |
|
|
84
|
+
| DT013 | delta-convention |
|
|
85
|
+
| DT014 | delta-colour-only |
|
|
86
|
+
| DT015 | low-contrast-text |
|
|
87
|
+
| DT016 | thin-line-contrast |
|
|
88
|
+
| DT017 | gradient-decoration |
|
|
89
|
+
| DT018 | glow |
|
|
90
|
+
| DT019 | kpi-overload |
|
|
91
|
+
| DT020 | kpi-rainbow |
|
|
92
|
+
| DT021 | emoji-icons |
|
|
93
|
+
| DT022 | cjk-min-size |
|
|
94
|
+
| DT023 | legend-single-series |
|
|
95
|
+
|
|
96
|
+
## Gallery
|
|
97
|
+
|
|
98
|
+
All data is fictional and seeded in [gallery/data.mjs](gallery/data.mjs). Each pair shares the same data. Before pages use common unguided defaults; after pages apply their pack. These are style checks, not a usability benchmark.
|
|
99
|
+
|
|
100
|
+
| Dashboard | Pack, mode | Before | After |
|
|
101
|
+
| --------- | ------------------ | --------------------------------------------------- | ---------------------------------- |
|
|
102
|
+
| saas | global, light | [32 errors, 50 warnings](gallery/saas-before.png) | [clean](gallery/saas-after.png) |
|
|
103
|
+
| finance | global, dark | [7 errors, 40 warnings](gallery/finance-before.png) | [clean](gallery/finance-after.png) |
|
|
104
|
+
| store | cn, light | [41 errors, 61 warnings](gallery/store-before.png) | [clean](gallery/store-after.png) |
|
|
105
|
+
| live | cn, dark 1920×1080 | [32 errors, 76 warnings](gallery/live-before.png) | [clean](gallery/live-after.png) |
|
|
106
|
+
|
|
107
|
+
The live page renders at 1920×1080; the others at 1440×900.
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
pnpm gallery
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This builds the CLI, copies local ECharts into the ignored `gallery/vendor/` folder, renders eight PNGs, and writes [results.json](gallery/results.json). No CDN or network fonts are used. Set `CHROME_PATH` to your local Chromium executable; the default is `/usr/bin/google-chrome`. The browser test extracts each live page again instead of trusting saved scores.
|
|
114
|
+
|
|
115
|
+
Comparison captions: “Default” / “Dashtaste” (global and cn; the cn caption for “Default” is written in Chinese). Wide images preserve full board height and use measured column boundaries.
|
|
116
|
+
|
|
117
|
+
- saas: [wide](gallery/compare/saas-wide.png) · [tall](gallery/compare/saas-tall.png)
|
|
118
|
+
- finance: [wide](gallery/compare/finance-wide.png) · [tall](gallery/compare/finance-tall.png)
|
|
119
|
+
- store: [wide](gallery/compare/store-wide.png) · [tall](gallery/compare/store-tall.png)
|
|
120
|
+
- live: [wide](gallery/compare/live-wide.png) · [tall](gallery/compare/live-tall.png)
|
|
121
|
+
|
|
122
|
+
## Status
|
|
123
|
+
|
|
124
|
+
Early (0.x). Expect rule changes between minor versions.
|
|
125
|
+
|
|
126
|
+
ECharts only for now. Vega-Lite and Recharts are planned. HTML mode needs a local Chromium. Rules are opinions with reasons; every rule can be disabled.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
[MIT](LICENSE).
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { check, summarize, score } from './engine.js';
|
|
4
|
+
import { rules } from './rules/index.js';
|
|
5
|
+
import { getTokens } from './packs/index.js';
|
|
6
|
+
import { extractPage } from './extract.js';
|
|
7
|
+
const version = '0.0.1';
|
|
8
|
+
const help = {
|
|
9
|
+
main: 'Usage: dashtaste <check|rules|tokens> --help',
|
|
10
|
+
check: 'Usage: dashtaste check <file|url...> [--pack global|cn] [--mode light|dark] [--format text|json] [--max-warnings N] [--disable DT001,...] [--only DT001,...] [--background #hex]\nCheck options, option arrays, or page facts. Exit: 0 pass, 1 findings exceed limit, 2 invalid input. Set NO_COLOR to disable ANSI colour; CHROME_PATH selects the browser.',
|
|
11
|
+
rules: 'Usage: dashtaste rules [--pack global|cn] [--format text|json]\nList rule metadata in rule order.',
|
|
12
|
+
tokens: 'Usage: dashtaste tokens --pack global|cn [--mode light|dark] [--format json|css]\nPrint exact pack tokens.',
|
|
13
|
+
};
|
|
14
|
+
function parseArgs(command, args) {
|
|
15
|
+
const flags = {}, inputs = [];
|
|
16
|
+
const allowed = {
|
|
17
|
+
check: ['pack', 'mode', 'format', 'max-warnings', 'disable', 'only', 'background'],
|
|
18
|
+
rules: ['pack', 'format'],
|
|
19
|
+
tokens: ['pack', 'mode', 'format'],
|
|
20
|
+
};
|
|
21
|
+
if (!command || !allowed[command])
|
|
22
|
+
throw new Error('usage: dashtaste check <file|url...> | rules | tokens --pack global|cn');
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
if (args[i].startsWith('--')) {
|
|
25
|
+
const key = args[i].slice(2);
|
|
26
|
+
if (!allowed[command].includes(key) || !args[i + 1] || args[i + 1].startsWith('--'))
|
|
27
|
+
throw new Error(`invalid option: ${args[i]}`);
|
|
28
|
+
flags[key] = args[++i];
|
|
29
|
+
}
|
|
30
|
+
else
|
|
31
|
+
inputs.push(args[i]);
|
|
32
|
+
}
|
|
33
|
+
return { flags, inputs };
|
|
34
|
+
}
|
|
35
|
+
async function main() {
|
|
36
|
+
const [command, ...args] = process.argv.slice(2);
|
|
37
|
+
if (command === '--help' || (command && args.includes('--help'))) {
|
|
38
|
+
console.log(help[command] ?? help.main);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const { flags, inputs } = parseArgs(command, args);
|
|
42
|
+
const pack = (flags.pack ?? 'global'), mode = (flags.mode ?? 'light'), format = flags.format ?? (command === 'tokens' ? 'json' : 'text');
|
|
43
|
+
if (!['global', 'cn'].includes(pack))
|
|
44
|
+
throw new Error('pack must be global or cn');
|
|
45
|
+
if (!['light', 'dark'].includes(mode))
|
|
46
|
+
throw new Error('mode must be light or dark');
|
|
47
|
+
if (!(command === 'tokens' ? ['json', 'css'] : ['json', 'text']).includes(format))
|
|
48
|
+
throw new Error('invalid output format');
|
|
49
|
+
if (command !== 'check' && inputs.length)
|
|
50
|
+
throw new Error('unexpected input');
|
|
51
|
+
if (command === 'tokens') {
|
|
52
|
+
if (!flags.pack)
|
|
53
|
+
throw new Error('tokens requires --pack');
|
|
54
|
+
const t = getTokens(pack, mode);
|
|
55
|
+
console.log(format === 'json'
|
|
56
|
+
? JSON.stringify(t, null, 2)
|
|
57
|
+
: `:root {\n${Object.entries(t)
|
|
58
|
+
.flatMap(([k, v]) => Array.isArray(v)
|
|
59
|
+
? v.map((x, i) => ` --dt-${k}-${i + 1}: ${x};`)
|
|
60
|
+
: [` --dt-${k}: ${v};`])
|
|
61
|
+
.join('\n')}\n}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (command === 'rules') {
|
|
65
|
+
const selected = rules.filter((r) => !flags.pack || r.packs.includes(pack));
|
|
66
|
+
console.log(format === 'json'
|
|
67
|
+
? JSON.stringify(selected.map(({ check, ...r }) => r), null, 2)
|
|
68
|
+
: selected.map((r) => `${r.id} ${r.severity} ${r.name} ${r.description}`).join('\n'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!inputs.length)
|
|
72
|
+
throw new Error('check requires at least one input');
|
|
73
|
+
const max = flags['max-warnings'] === undefined ? Infinity : Number(flags['max-warnings']);
|
|
74
|
+
if (max < 0 || (!Number.isInteger(max) && max !== Infinity) || flags['max-warnings'] === '')
|
|
75
|
+
throw new Error('max-warnings must be a non-negative integer');
|
|
76
|
+
const ids = (key) => {
|
|
77
|
+
if (!flags[key])
|
|
78
|
+
return undefined;
|
|
79
|
+
const values = flags[key].split(',');
|
|
80
|
+
if (values.some((id) => !rules.some((r) => r.id === id)))
|
|
81
|
+
throw new Error(`unknown rule in --${key}`);
|
|
82
|
+
return values;
|
|
83
|
+
};
|
|
84
|
+
const disable = ids('disable'), only = ids('only');
|
|
85
|
+
if (flags.background && !/^#(?:[\da-f]{3}|[\da-f]{6})$/i.test(flags.background))
|
|
86
|
+
throw new Error('background must be a hex colour');
|
|
87
|
+
const results = [];
|
|
88
|
+
for (const input of inputs) {
|
|
89
|
+
let data;
|
|
90
|
+
if (/^https?:\/\//i.test(input) || /\.html?$/i.test(input))
|
|
91
|
+
data = await extractPage(input);
|
|
92
|
+
else {
|
|
93
|
+
try {
|
|
94
|
+
data = JSON.parse(await readFile(input, 'utf8'));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
throw new Error(`cannot read JSON input ${input}: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const findings = check(data, { pack, mode, disable, only, background: flags.background });
|
|
101
|
+
results.push({ input, findings, score: score(findings) });
|
|
102
|
+
}
|
|
103
|
+
const summary = summarize(results.flatMap((r) => r.findings));
|
|
104
|
+
if (format === 'json')
|
|
105
|
+
console.log(JSON.stringify({ version, pack, inputs: results, summary }, null, 2));
|
|
106
|
+
else {
|
|
107
|
+
const tty = process.stdout.isTTY && process.env.NO_COLOR === undefined;
|
|
108
|
+
for (const result of results) {
|
|
109
|
+
console.log(result.input);
|
|
110
|
+
for (const f of result.findings) {
|
|
111
|
+
const label = tty
|
|
112
|
+
? `\x1b[${f.severity === 'error' ? 31 : f.severity === 'warn' ? 33 : 36}m${f.severity}\x1b[0m`
|
|
113
|
+
: f.severity;
|
|
114
|
+
console.log(` ${label} ${f.ruleId} ${f.path} ${f.message} → ${f.fix.replace('<pack>', pack)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
console.log(`${summary.errors} error${summary.errors === 1 ? '' : 's'}, ${summary.warnings} warning${summary.warnings === 1 ? '' : 's'} · score ${summary.score}/100`);
|
|
118
|
+
}
|
|
119
|
+
process.exitCode = summary.errors > 0 || summary.warnings > max ? 1 : 0;
|
|
120
|
+
}
|
|
121
|
+
main().catch((error) => {
|
|
122
|
+
console.error(`dashtaste: ${error.message}`);
|
|
123
|
+
process.exitCode = 2;
|
|
124
|
+
});
|
package/dist/color.d.ts
ADDED
package/dist/color.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const named = {
|
|
2
|
+
black: '#000000',
|
|
3
|
+
white: '#FFFFFF',
|
|
4
|
+
red: '#FF0000',
|
|
5
|
+
green: '#008000',
|
|
6
|
+
blue: '#0000FF',
|
|
7
|
+
gray: '#808080',
|
|
8
|
+
grey: '#808080',
|
|
9
|
+
yellow: '#FFFF00',
|
|
10
|
+
orange: '#FFA500',
|
|
11
|
+
purple: '#800080',
|
|
12
|
+
cyan: '#00FFFF',
|
|
13
|
+
magenta: '#FF00FF',
|
|
14
|
+
};
|
|
15
|
+
export function rgb(color) {
|
|
16
|
+
const normalized = color.trim().toLowerCase();
|
|
17
|
+
const h = (named[normalized] ?? normalized).replace(/^#/, '');
|
|
18
|
+
if (/^[\da-f]{3}$/i.test(h))
|
|
19
|
+
return [...h].map((c) => parseInt(c + c, 16) / 255);
|
|
20
|
+
if (/^[\da-f]{6}$/i.test(h))
|
|
21
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
|
|
22
|
+
const m = normalized.match(/^rgba?\(\s*([\d.]+)[, ]+([\d.]+)[, ]+([\d.]+)/i);
|
|
23
|
+
return m ? m.slice(1, 4).map((v) => Number(v) / 255) : undefined;
|
|
24
|
+
}
|
|
25
|
+
export const linear = (v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
|
|
26
|
+
export function contrast(a, b) {
|
|
27
|
+
const lum = (c) => {
|
|
28
|
+
const r = rgb(c);
|
|
29
|
+
return r ? r.map(linear).reduce((s, v, i) => s + v * [0.2126, 0.7152, 0.0722][i], 0) : NaN;
|
|
30
|
+
};
|
|
31
|
+
const x = lum(a), y = lum(b);
|
|
32
|
+
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
|
|
33
|
+
}
|
|
34
|
+
export function hueBucket(c) {
|
|
35
|
+
const r = rgb(c);
|
|
36
|
+
if (!r)
|
|
37
|
+
return;
|
|
38
|
+
const max = Math.max(...r), min = Math.min(...r), d = max - min;
|
|
39
|
+
if (max === 0 || d / max < 0.45)
|
|
40
|
+
return;
|
|
41
|
+
const h = max === r[0] ? (r[1] - r[2]) / d : max === r[1] ? 2 + (r[2] - r[0]) / d : 4 + (r[0] - r[1]) / d;
|
|
42
|
+
return Math.floor(((h * 60 + 360) % 360) / 60);
|
|
43
|
+
}
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CheckOptions, Finding, PageFacts } from './types.js';
|
|
2
|
+
export declare function normalizeInput(input: unknown): PageFacts;
|
|
3
|
+
export declare function check(input: unknown, options?: CheckOptions): Finding[];
|
|
4
|
+
/** Score = 100 - 10 * errors - 3 * warnings - info, floored at zero. */
|
|
5
|
+
export declare function score(findings: Finding[]): number;
|
|
6
|
+
export declare function summarize(findings: Finding[]): {
|
|
7
|
+
errors: number;
|
|
8
|
+
warnings: number;
|
|
9
|
+
info: number;
|
|
10
|
+
score: number;
|
|
11
|
+
};
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { rules } from './rules/index.js';
|
|
2
|
+
export function normalizeInput(input) {
|
|
3
|
+
const isObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
4
|
+
if (Array.isArray(input)) {
|
|
5
|
+
if (!input.every(isObject))
|
|
6
|
+
throw new Error('expected option objects');
|
|
7
|
+
return { charts: input.map((option) => ({ option })) };
|
|
8
|
+
}
|
|
9
|
+
if (!isObject(input))
|
|
10
|
+
throw new Error('expected an option object, option array, or charts envelope');
|
|
11
|
+
if ('charts' in input) {
|
|
12
|
+
if (!Array.isArray(input.charts) ||
|
|
13
|
+
!input.charts.every((c) => isObject(c) && isObject(c.option)))
|
|
14
|
+
throw new Error('charts must contain option objects');
|
|
15
|
+
return input;
|
|
16
|
+
}
|
|
17
|
+
return { charts: [{ option: input }] };
|
|
18
|
+
}
|
|
19
|
+
export function check(input, options = {}) {
|
|
20
|
+
const facts = normalizeInput(input);
|
|
21
|
+
const settings = { ...options, pack: options.pack ?? 'global', mode: options.mode ?? 'light' };
|
|
22
|
+
return rules
|
|
23
|
+
.filter((r) => r.packs.includes(settings.pack) &&
|
|
24
|
+
!options.disable?.includes(r.id) &&
|
|
25
|
+
(!options.only || options.only.includes(r.id)))
|
|
26
|
+
.flatMap((r) => r.check(facts, settings));
|
|
27
|
+
}
|
|
28
|
+
/** Score = 100 - 10 * errors - 3 * warnings - info, floored at zero. */
|
|
29
|
+
export function score(findings) {
|
|
30
|
+
return Math.max(0, 100 - findings.reduce((sum, f) => sum + { error: 10, warn: 3, info: 1 }[f.severity], 0));
|
|
31
|
+
}
|
|
32
|
+
export function summarize(findings) {
|
|
33
|
+
return {
|
|
34
|
+
errors: findings.filter((f) => f.severity === 'error').length,
|
|
35
|
+
warnings: findings.filter((f) => f.severity === 'warn').length,
|
|
36
|
+
info: findings.filter((f) => f.severity === 'info').length,
|
|
37
|
+
score: score(findings),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** Runs in the page without closures over the host process. */
|
|
2
|
+
export function extractInPage() {
|
|
3
|
+
const facts = {
|
|
4
|
+
charts: [],
|
|
5
|
+
texts: [],
|
|
6
|
+
gradients: [],
|
|
7
|
+
shadows: [],
|
|
8
|
+
kpis: [],
|
|
9
|
+
deltas: [],
|
|
10
|
+
emojis: [],
|
|
11
|
+
};
|
|
12
|
+
const selector = (el) => {
|
|
13
|
+
if (el.id)
|
|
14
|
+
return `#${CSS.escape(el.id)}`;
|
|
15
|
+
const parts = [];
|
|
16
|
+
let node = el;
|
|
17
|
+
while (node && node !== document.documentElement) {
|
|
18
|
+
const parent = node.parentElement;
|
|
19
|
+
parts.unshift(`${node.tagName.toLowerCase()}:nth-child(${parent ? [...parent.children].indexOf(node) + 1 : 1})`);
|
|
20
|
+
node = parent;
|
|
21
|
+
}
|
|
22
|
+
return parts.join(' > ');
|
|
23
|
+
};
|
|
24
|
+
// Composite transparent ancestor backgrounds over the browser canvas.
|
|
25
|
+
const compositeBackground = (el) => {
|
|
26
|
+
const layers = [];
|
|
27
|
+
let node = el;
|
|
28
|
+
while (node) {
|
|
29
|
+
const c = getComputedStyle(node)
|
|
30
|
+
.backgroundColor.match(/[\d.]+/g)
|
|
31
|
+
?.map(Number);
|
|
32
|
+
if (c) {
|
|
33
|
+
layers.push([c[0], c[1], c[2], c[3] ?? 1]);
|
|
34
|
+
if ((c[3] ?? 1) >= 1)
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
node = node.parentElement;
|
|
38
|
+
}
|
|
39
|
+
let out = [255, 255, 255];
|
|
40
|
+
for (const l of layers.reverse())
|
|
41
|
+
out = out.map((v, i) => l[i] * l[3] + v * (1 - l[3]));
|
|
42
|
+
return `rgb(${out.map(Math.round).join(', ')})`;
|
|
43
|
+
};
|
|
44
|
+
const ownText = (el) => [...el.childNodes]
|
|
45
|
+
.filter((n) => n.nodeType === Node.TEXT_NODE)
|
|
46
|
+
.map((n) => n.textContent ?? '')
|
|
47
|
+
.join(' ')
|
|
48
|
+
.trim();
|
|
49
|
+
for (const el of document.querySelectorAll('*')) {
|
|
50
|
+
const style = getComputedStyle(el);
|
|
51
|
+
if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE'].includes(el.tagName) ||
|
|
52
|
+
style.display === 'none' ||
|
|
53
|
+
style.visibility === 'hidden' ||
|
|
54
|
+
Number(style.opacity) === 0 ||
|
|
55
|
+
!el.getClientRects().length)
|
|
56
|
+
continue;
|
|
57
|
+
const path = selector(el), text = ownText(el), fontSize = parseFloat(style.fontSize);
|
|
58
|
+
// Text facts carry computed foreground, composited background, and size.
|
|
59
|
+
if (text)
|
|
60
|
+
facts.texts.push({
|
|
61
|
+
selector: path,
|
|
62
|
+
text,
|
|
63
|
+
color: style.color,
|
|
64
|
+
background: compositeBackground(el),
|
|
65
|
+
fontSize,
|
|
66
|
+
fontWeight: style.fontWeight === 'bold' ? 700 : parseInt(style.fontWeight) || 400,
|
|
67
|
+
fontVariantNumeric: style.fontVariantNumeric,
|
|
68
|
+
fontFeatureSettings: style.fontFeatureSettings,
|
|
69
|
+
});
|
|
70
|
+
// Decoration facts include CSS gradients and shadows.
|
|
71
|
+
if (/gradient\(/.test(style.backgroundImage))
|
|
72
|
+
facts.gradients.push({ selector: path, background: style.backgroundImage });
|
|
73
|
+
if (style.boxShadow !== 'none' || style.textShadow !== 'none')
|
|
74
|
+
facts.shadows.push({
|
|
75
|
+
selector: path,
|
|
76
|
+
boxShadow: style.boxShadow,
|
|
77
|
+
textShadow: style.textShadow,
|
|
78
|
+
});
|
|
79
|
+
// KPI and delta facts support explicit data attributes and visible-value heuristics.
|
|
80
|
+
if (el.hasAttribute('data-kpi') ||
|
|
81
|
+
(fontSize >= 22 &&
|
|
82
|
+
!el.closest('[data-ranking]') &&
|
|
83
|
+
/^[\s+−\-\d,.%KMB万亿¥$€]+$/.test(text) &&
|
|
84
|
+
/\d/.test(text)))
|
|
85
|
+
facts.kpis.push({
|
|
86
|
+
selector: path,
|
|
87
|
+
text: text || el.textContent?.trim() || '',
|
|
88
|
+
color: style.color,
|
|
89
|
+
background: compositeBackground(el),
|
|
90
|
+
fontSize,
|
|
91
|
+
fontVariantNumeric: style.fontVariantNumeric,
|
|
92
|
+
fontFeatureSettings: style.fontFeatureSettings,
|
|
93
|
+
});
|
|
94
|
+
if (el.hasAttribute('data-delta') || /[▲▼↑↓]|[+\-]\s*\d+(?:\.\d+)?%/.test(text)) {
|
|
95
|
+
const direction = el.getAttribute('data-direction');
|
|
96
|
+
const polarity = el.closest('[data-polarity]')?.getAttribute('data-polarity');
|
|
97
|
+
facts.deltas.push({
|
|
98
|
+
selector: path,
|
|
99
|
+
text: text || el.textContent?.trim() || '',
|
|
100
|
+
color: style.color,
|
|
101
|
+
...(polarity === 'higher-is-better' || polarity === 'lower-is-better' ? { polarity } : {}),
|
|
102
|
+
...(['up', 'down', 'flat'].includes(direction ?? '')
|
|
103
|
+
? { direction: direction }
|
|
104
|
+
: {}),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Emoji facts are limited to headings, labels, KPIs, and rankings.
|
|
108
|
+
if (/\p{Extended_Pictographic}/u.test(text) &&
|
|
109
|
+
el.closest('h1,h2,h3,h4,h5,h6,label,[data-kpi-label],[data-ranking],[data-kpi]'))
|
|
110
|
+
facts.emojis.push({ selector: path, text });
|
|
111
|
+
}
|
|
112
|
+
// Chart facts preserve the live ECharts option for each mounted chart.
|
|
113
|
+
const echarts = window.echarts;
|
|
114
|
+
if (echarts)
|
|
115
|
+
for (const el of document.querySelectorAll('[_echarts_instance_]')) {
|
|
116
|
+
const instance = echarts.getInstanceByDom(el);
|
|
117
|
+
if (instance) {
|
|
118
|
+
// Browser transports discard functions. Preserve formatter presence without executing
|
|
119
|
+
// user callbacks or sending executable code across the extraction boundary.
|
|
120
|
+
const option = JSON.parse(JSON.stringify(instance.getOption(), (_key, value) => typeof value === 'function' ? '[function]' : value));
|
|
121
|
+
for (const type of ['xAxis', 'yAxis']) {
|
|
122
|
+
const axes = option[type];
|
|
123
|
+
if (Array.isArray(axes))
|
|
124
|
+
axes.forEach((axis, index) => {
|
|
125
|
+
axis.renderedTickLabels =
|
|
126
|
+
instance
|
|
127
|
+
.getModel?.()
|
|
128
|
+
.getComponent(type, index)
|
|
129
|
+
?.axis?.getViewLabels()
|
|
130
|
+
.map((label) => label.formattedLabel) ?? [];
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// ECharts 6 returns singleton components as arrays, including tooltip.
|
|
134
|
+
if (Array.isArray(option.tooltip))
|
|
135
|
+
option.tooltip = option.tooltip[0];
|
|
136
|
+
facts.charts.push({ id: el.id || selector(el), option });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Page facts provide the background and optional delta convention.
|
|
140
|
+
facts.page = {
|
|
141
|
+
background: compositeBackground(document.body),
|
|
142
|
+
...(document.documentElement.dataset.deltaColors === 'red-up' ||
|
|
143
|
+
document.documentElement.dataset.deltaColors === 'green-up' ||
|
|
144
|
+
document.documentElement.dataset.deltaColors === 'by-polarity'
|
|
145
|
+
? { deltaColors: document.documentElement.dataset.deltaColors }
|
|
146
|
+
: {}),
|
|
147
|
+
};
|
|
148
|
+
return facts;
|
|
149
|
+
}
|
package/dist/extract.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { extractInPage } from './extract-in-page.js';
|
|
5
|
+
export { extractInPage } from './extract-in-page.js';
|
|
6
|
+
export async function extractPage(input) {
|
|
7
|
+
let chromium;
|
|
8
|
+
try {
|
|
9
|
+
({ chromium } = await import('playwright-core'));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new Error('HTML mode requires playwright-core and a browser; set CHROME_PATH to Chromium');
|
|
13
|
+
}
|
|
14
|
+
const executablePath = process.env.CHROME_PATH ||
|
|
15
|
+
(existsSync('/usr/bin/google-chrome') ? '/usr/bin/google-chrome' : undefined);
|
|
16
|
+
let browser;
|
|
17
|
+
try {
|
|
18
|
+
browser = await chromium.launch({
|
|
19
|
+
headless: true,
|
|
20
|
+
executablePath,
|
|
21
|
+
args: /^https?:\/\//i.test(input) ? [] : ['--allow-file-access-from-files'],
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error('browser unavailable; set CHROME_PATH to a Chromium executable');
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const page = await browser.newPage();
|
|
29
|
+
await page.goto(/^https?:\/\//i.test(input) ? input : pathToFileURL(resolve(input)).href, {
|
|
30
|
+
waitUntil: 'networkidle',
|
|
31
|
+
timeout: 30000,
|
|
32
|
+
});
|
|
33
|
+
await page.waitForTimeout(500);
|
|
34
|
+
return await page.evaluate(extractInPage);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await browser.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Polarity, type PackName, type DeltaColors, type Mode } from './packs/index.js';
|
|
2
|
+
export declare function formatCompact(value: number, pack: PackName, options?: {
|
|
3
|
+
minimumFractionDigits?: number;
|
|
4
|
+
compactFrom?: number;
|
|
5
|
+
}): string;
|
|
6
|
+
export declare function formatPercent(value: number, digits?: number): string;
|
|
7
|
+
export declare function formatDelta(current: number, previous: number, pack: PackName, options?: {
|
|
8
|
+
label?: string;
|
|
9
|
+
polarity?: Polarity;
|
|
10
|
+
deltaColors?: DeltaColors;
|
|
11
|
+
mode?: Mode;
|
|
12
|
+
}): {
|
|
13
|
+
readonly text: string;
|
|
14
|
+
readonly direction: "flat" | "up" | "down" | "invalid";
|
|
15
|
+
readonly color: string;
|
|
16
|
+
};
|
|
17
|
+
/** Uses local calendar fields; no locale-dependent date ordering. */
|
|
18
|
+
export declare function formatDate(value: Date | string | number, pack?: PackName): string;
|