dashtaste 0.0.0-stage → 0.0.2

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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -2
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +124 -0
  5. package/dist/color.d.ts +4 -0
  6. package/dist/color.js +43 -0
  7. package/dist/engine.d.ts +11 -0
  8. package/dist/engine.js +39 -0
  9. package/dist/extract-in-page.d.ts +3 -0
  10. package/dist/extract-in-page.js +149 -0
  11. package/dist/extract.d.ts +3 -0
  12. package/dist/extract.js +39 -0
  13. package/dist/format.d.ts +18 -0
  14. package/dist/format.js +65 -0
  15. package/dist/index.d.ts +5 -0
  16. package/dist/index.js +5 -0
  17. package/dist/packs/cn.d.ts +2 -0
  18. package/dist/packs/cn.js +56 -0
  19. package/dist/packs/global.d.ts +2 -0
  20. package/dist/packs/global.js +45 -0
  21. package/dist/packs/index.d.ts +53 -0
  22. package/dist/packs/index.js +13 -0
  23. package/dist/rules/dt001-default-palette.d.ts +3 -0
  24. package/dist/rules/dt001-default-palette.js +11 -0
  25. package/dist/rules/dt002-rainbow-palette.d.ts +3 -0
  26. package/dist/rules/dt002-rainbow-palette.js +11 -0
  27. package/dist/rules/dt003-smooth-line.d.ts +3 -0
  28. package/dist/rules/dt003-smooth-line.js +9 -0
  29. package/dist/rules/dt004-symbols-everywhere.d.ts +3 -0
  30. package/dist/rules/dt004-symbols-everywhere.js +21 -0
  31. package/dist/rules/dt005-pie-slices.d.ts +3 -0
  32. package/dist/rules/dt005-pie-slices.js +13 -0
  33. package/dist/rules/dt006-pie-overuse.d.ts +3 -0
  34. package/dist/rules/dt006-pie-overuse.js +14 -0
  35. package/dist/rules/dt007-three-d.d.ts +3 -0
  36. package/dist/rules/dt007-three-d.js +16 -0
  37. package/dist/rules/dt008-dual-axis.d.ts +3 -0
  38. package/dist/rules/dt008-dual-axis.js +10 -0
  39. package/dist/rules/dt009-missing-unit.d.ts +3 -0
  40. package/dist/rules/dt009-missing-unit.js +8 -0
  41. package/dist/rules/dt010-bar-baseline.d.ts +3 -0
  42. package/dist/rules/dt010-bar-baseline.js +11 -0
  43. package/dist/rules/dt011-raw-large-numbers.d.ts +3 -0
  44. package/dist/rules/dt011-raw-large-numbers.js +21 -0
  45. package/dist/rules/dt012-unit-system.d.ts +3 -0
  46. package/dist/rules/dt012-unit-system.js +12 -0
  47. package/dist/rules/dt013-delta-convention.d.ts +3 -0
  48. package/dist/rules/dt013-delta-convention.js +21 -0
  49. package/dist/rules/dt014-delta-colour-only.d.ts +3 -0
  50. package/dist/rules/dt014-delta-colour-only.js +16 -0
  51. package/dist/rules/dt015-low-contrast-text.d.ts +3 -0
  52. package/dist/rules/dt015-low-contrast-text.js +28 -0
  53. package/dist/rules/dt016-thin-line-contrast.d.ts +3 -0
  54. package/dist/rules/dt016-thin-line-contrast.js +12 -0
  55. package/dist/rules/dt017-gradient-decoration.d.ts +3 -0
  56. package/dist/rules/dt017-gradient-decoration.js +15 -0
  57. package/dist/rules/dt018-glow.d.ts +3 -0
  58. package/dist/rules/dt018-glow.js +27 -0
  59. package/dist/rules/dt019-kpi-overload.d.ts +3 -0
  60. package/dist/rules/dt019-kpi-overload.js +7 -0
  61. package/dist/rules/dt020-kpi-rainbow.d.ts +3 -0
  62. package/dist/rules/dt020-kpi-rainbow.js +27 -0
  63. package/dist/rules/dt021-emoji-icons.d.ts +3 -0
  64. package/dist/rules/dt021-emoji-icons.js +10 -0
  65. package/dist/rules/dt022-cjk-min-size.d.ts +3 -0
  66. package/dist/rules/dt022-cjk-min-size.js +9 -0
  67. package/dist/rules/dt023-legend-single-series.d.ts +3 -0
  68. package/dist/rules/dt023-legend-single-series.js +8 -0
  69. package/dist/rules/dt024-bar-width.d.ts +1 -0
  70. package/dist/rules/dt024-bar-width.js +14 -0
  71. package/dist/rules/dt025-legend-not-direct-label.d.ts +1 -0
  72. package/dist/rules/dt025-legend-not-direct-label.js +11 -0
  73. package/dist/rules/dt026-line-weight.d.ts +1 -0
  74. package/dist/rules/dt026-line-weight.js +8 -0
  75. package/dist/rules/dt027-tabular-numerals.d.ts +1 -0
  76. package/dist/rules/dt027-tabular-numerals.js +16 -0
  77. package/dist/rules/dt028-hero-metric.d.ts +1 -0
  78. package/dist/rules/dt028-hero-metric.js +14 -0
  79. package/dist/rules/helpers.d.ts +32 -0
  80. package/dist/rules/helpers.js +161 -0
  81. package/dist/rules/index.d.ts +2 -0
  82. package/dist/rules/index.js +58 -0
  83. package/dist/types.d.ts +122 -0
  84. package/dist/types.js +1 -0
  85. package/docs/rules.md +735 -0
  86. package/docs/usage.md +126 -0
  87. package/package.json +74 -4
  88. package/skills/dashtaste-cn/SKILL.md +51 -0
  89. package/skills/dashtaste-cn/references/avoid.md +135 -0
  90. package/skills/dashtaste-cn/references/charts.md +122 -0
  91. package/skills/dashtaste-cn/references/layout.md +78 -0
  92. package/skills/dashtaste-cn/references/numbers.md +93 -0
  93. package/skills/dashtaste-cn/references/tokens.md +72 -0
  94. package/skills/dashtaste-global/SKILL.md +43 -0
  95. package/skills/dashtaste-global/references/avoid.md +135 -0
  96. package/skills/dashtaste-global/references/charts.md +122 -0
  97. package/skills/dashtaste-global/references/layout.md +78 -0
  98. package/skills/dashtaste-global/references/numbers.md +83 -0
  99. package/skills/dashtaste-global/references/tokens.md +65 -0
@@ -0,0 +1,27 @@
1
+ import { walk, rule, charts } from './helpers.js';
2
+ import { rgb } from '../color.js';
3
+ /** Coloured CSS shadows with blur at least 12px, or chart shadowBlur at least 10, create glow. */
4
+ export const dt018 = rule(18, 'glow', 'warn', 'Coloured CSS shadows with blur at least 12px, or chart shadowBlur at least 10, create glow.', 'remove glow and use hairline borders', function check(facts, context, emit) {
5
+ for (const entry of facts.shadows ?? []) {
6
+ const value = entry.boxShadow + ' ' + entry.textShadow;
7
+ const chunks = value.split(/,(?![^()]*\))/);
8
+ if (chunks.some((item) => {
9
+ const nums = [
10
+ ...item.replace(/rgba?\([^)]*\)|#[\da-f]+/gi, '').matchAll(/(-?[\d.]+)px/g),
11
+ ].map((m) => +m[1]);
12
+ const lineColor = item.match(/rgba?\([^)]*\)|#[\da-f]+/i)?.[0];
13
+ const channels = lineColor ? rgb(lineColor) : undefined;
14
+ return nums[2] >= 12 && channels && Math.max(...channels) - Math.min(...channels) > 0.05;
15
+ }))
16
+ emit(entry.selector);
17
+ }
18
+ charts(facts, (option, path) => {
19
+ let found = false;
20
+ walk(option, (key, value) => {
21
+ if (key === 'shadowBlur' && typeof value === 'number' && value >= 10)
22
+ found = true;
23
+ });
24
+ if (found)
25
+ emit(path);
26
+ });
27
+ });
@@ -0,0 +1,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** More than 6 global or 8 cn KPI tiles weakens priority. */
3
+ export declare const dt019: Rule;
@@ -0,0 +1,7 @@
1
+ import { packs } from '../packs/index.js';
2
+ import { rule } from './helpers.js';
3
+ /** More than 6 global or 8 cn KPI tiles weakens priority. */
4
+ export const dt019 = rule(19, 'kpi-overload', 'warn', 'More than 6 global or 8 cn KPI tiles weakens priority.', 'keep only primary KPIs in the first view', function check(facts, context, emit) {
5
+ if ((facts.kpis?.length ?? 0) > packs[context.pack].limits.maxKpis)
6
+ emit('page');
7
+ });
@@ -0,0 +1,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Three or more distinct accent colours on KPI values or tiles distract. Neutral greys and pack text/background tokens are excluded. */
3
+ export declare const dt020: Rule;
@@ -0,0 +1,27 @@
1
+ import { rule, sameColor } from './helpers.js';
2
+ import { rgb } from '../color.js';
3
+ import { getTokens } from '../packs/index.js';
4
+ /** Three or more distinct accent colours on KPI values or tiles distract. Neutral greys and pack text/background tokens are excluded. */
5
+ export const dt020 = rule(20, 'kpi-rainbow', 'warn', 'Three or more distinct accent colours on KPI values or tiles distract. Neutral greys and pack text/background tokens are excluded.', 'use ink for values and reserve colour for deltas', function check(facts, context, emit) {
6
+ const tokens = getTokens(context.pack, context.mode);
7
+ const neutral = [
8
+ tokens.ink,
9
+ tokens.muted,
10
+ tokens.paper,
11
+ tokens.card,
12
+ tokens.page,
13
+ tokens.bg,
14
+ tokens.panel,
15
+ ].filter((entry) => typeof entry === 'string');
16
+ const accent = (entry) => {
17
+ const channels = rgb(entry);
18
+ return (channels &&
19
+ Math.max(...channels) - Math.min(...channels) > 0.08 &&
20
+ !neutral.some((n) => sameColor(n, entry)));
21
+ };
22
+ const colors = (facts.kpis ?? [])
23
+ .flatMap((kpi) => [kpi.color, kpi.background ?? ''])
24
+ .filter(accent);
25
+ if (new Set(colors.map((entry) => rgb(entry)?.join(','))).size >= 3)
26
+ emit('page');
27
+ });
@@ -0,0 +1,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** Emoji in headings, KPI labels, and rankings add inconsistent iconography. */
3
+ export declare const dt021: Rule;
@@ -0,0 +1,10 @@
1
+ import { rule, charts } from './helpers.js';
2
+ /** Emoji in headings, KPI labels, and rankings add inconsistent iconography. */
3
+ export const dt021 = rule(21, 'emoji-icons', 'warn', 'Emoji in headings, KPI labels, and rankings add inconsistent iconography.', 'use plain text and plain numerals for rankings', function check(facts, context, emit) {
4
+ for (const entry of facts.emojis ?? [])
5
+ emit(entry.selector);
6
+ charts(facts, (option, path) => {
7
+ if (/\p{Extended_Pictographic}/u.test(JSON.stringify(option.title ?? '')))
8
+ emit(`${path}.title`);
9
+ });
10
+ });
@@ -0,0 +1,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** CJK text must be at least 12px. */
3
+ export declare const dt022: Rule;
@@ -0,0 +1,9 @@
1
+ import { packs } from '../packs/index.js';
2
+ import { rule } from './helpers.js';
3
+ /** CJK text must be at least 12px. */
4
+ export const dt022 = rule(22, 'cjk-min-size', 'warn', 'CJK text must be at least 12px.', 'set CJK text to at least 12px', function check(facts, context, emit) {
5
+ for (const text of facts.texts ?? [])
6
+ if (/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(text.text) &&
7
+ text.fontSize < packs[context.pack].limits.cjkMinFontSize)
8
+ emit(text.selector);
9
+ }, true);
@@ -0,0 +1,3 @@
1
+ import type { Rule } from '../types.js';
2
+ /** An explicitly visible legend on one series repeats context. */
3
+ export declare const dt023: Rule;
@@ -0,0 +1,8 @@
1
+ import { asArray, series, rule, charts } from './helpers.js';
2
+ /** An explicitly visible legend on one series repeats context. */
3
+ export const dt023 = rule(23, 'legend-single-series', 'info', 'An explicitly visible legend on one series repeats context.', 'hide the legend or label the series directly', function check(facts, context, emit) {
4
+ charts(facts, (option, path) => {
5
+ if (series(option).length === 1 && asArray(option.legend).some((l) => l.show !== false))
6
+ emit(`${path}.legend`);
7
+ });
8
+ });
@@ -0,0 +1 @@
1
+ export declare const dt024: import("../types.js").Rule;
@@ -0,0 +1,14 @@
1
+ import { rule, charts, series } from './helpers.js';
2
+ export const dt024 = rule(24, 'bar-width', 'warn', 'Bars need an explicit width cap of 24px, or 32px on screens.', 'set barMaxWidth to 24 or less; use 32 or less on screens', (facts, context, emit) => {
3
+ charts(facts, (option, path) => series(option).forEach((s, i) => {
4
+ if (s.type !== 'bar')
5
+ return;
6
+ const cap = context.pack === 'cn' && context.mode === 'dark' ? 32 : 24;
7
+ const width = s.barMaxWidth ?? s.barWidth;
8
+ if (typeof width !== 'number' ||
9
+ width > cap ||
10
+ width <= 0 ||
11
+ (typeof s.barWidth === 'number' && s.barWidth > cap))
12
+ emit(`${path}.series[${i}]`);
13
+ }));
14
+ });
@@ -0,0 +1 @@
1
+ export declare const dt025: import("../types.js").Rule;
@@ -0,0 +1,11 @@
1
+ import { rule, charts, series, asArray } from './helpers.js';
2
+ export const dt025 = rule(25, 'legend-not-direct-label', 'warn', 'Two to four line series need direct labels instead of a legend.', 'hide the legend and label series at their last point', (facts, context, emit) => {
3
+ charts(facts, (option, path) => {
4
+ const lines = series(option).filter((s) => s.type === 'line');
5
+ if (lines.length < 2 || lines.length > 4)
6
+ return;
7
+ if (asArray(option.legend).some((l) => l.show !== false) &&
8
+ !lines.every((s) => s.endLabel?.show || s.label?.show))
9
+ emit(path);
10
+ });
11
+ });
@@ -0,0 +1 @@
1
+ export declare const dt026: import("../types.js").Rule;
@@ -0,0 +1,8 @@
1
+ import { rule, charts, series } from './helpers.js';
2
+ export const dt026 = rule(26, 'line-weight', 'warn', 'Line widths outside 1–3px obscure the data.', 'use 2px main lines and 1.5px comparison lines', (facts, context, emit) => {
3
+ charts(facts, (option, path) => series(option).forEach((s, i) => {
4
+ const width = s.lineStyle?.width;
5
+ if (s.type === 'line' && typeof width === 'number' && (width < 1 || width > 3))
6
+ emit(`${path}.series[${i}]`);
7
+ }));
8
+ });
@@ -0,0 +1 @@
1
+ export declare const dt027: import("../types.js").Rule;
@@ -0,0 +1,16 @@
1
+ import { rule } from './helpers.js';
2
+ export const dt027 = rule(27, 'tabular-numerals', 'warn', 'KPI numbers need tabular numerals.', 'set font-variant-numeric to tabular-nums', (facts, context, emit) => {
3
+ const numbers = [
4
+ ...(facts.kpis ?? []),
5
+ ...(facts.texts ?? []).filter((t) => /^[\s+−\-\d,.%KMB万亿¥$€]+$/.test(t.text) && /\d/.test(t.text)),
6
+ ];
7
+ const seen = new Set();
8
+ for (const n of numbers) {
9
+ const tabular = /tabular-nums/.test(n.fontVariantNumeric ?? '') ||
10
+ /["']tnum["'](?:\s+(?:1|on))?(?=\s*(?:,|$))/.test(n.fontFeatureSettings ?? '');
11
+ if (!tabular && !seen.has(n.selector)) {
12
+ emit(n.selector);
13
+ seen.add(n.selector);
14
+ }
15
+ }
16
+ });
@@ -0,0 +1 @@
1
+ export declare const dt028: import("../types.js").Rule;
@@ -0,0 +1,14 @@
1
+ import { rule } from './helpers.js';
2
+ export const dt028 = rule(28, 'hero-metric', 'info', 'Pages with at least three KPIs need a clear hero metric.', 'make the largest KPI at least 1.6 times the median of the others', (facts, context, emit) => {
3
+ const sizes = (facts.kpis ?? [])
4
+ .map((k) => k.fontSize ?? 0)
5
+ .filter((n) => n > 0)
6
+ .sort((a, b) => a - b);
7
+ if (sizes.length < 3)
8
+ return;
9
+ const largest = sizes.pop();
10
+ const mid = Math.floor(sizes.length / 2);
11
+ const median = sizes.length % 2 ? sizes[mid] : (sizes[mid - 1] + sizes[mid]) / 2;
12
+ if (largest < median * 1.6)
13
+ emit('page.kpis');
14
+ });
@@ -0,0 +1,32 @@
1
+ import type { Rule, Option, PageFacts, CheckOptions, Severity } from '../types.js';
2
+ export declare function asArray<T>(value: T | T[] | null | undefined): T[];
3
+ export declare function series(option: Option): Option[];
4
+ interface ValueAxis extends Option {
5
+ _dimension: 'x' | 'y';
6
+ _index: number;
7
+ _path: string;
8
+ }
9
+ /** An axis without a declared type or category data defaults to a value axis. */
10
+ export declare function valueAxes(option: Option): ValueAxis[];
11
+ /** Data may contain numbers, coordinate tuples, or objects with a value field. */
12
+ export declare function seriesValues(item: Option): number[];
13
+ export declare function seriesColor(item: Option, index: number, option: Option): unknown;
14
+ export declare function allColors(option: Option): string[];
15
+ export declare function walk(value: unknown, visit: (key: string, value: unknown) => void): void;
16
+ export declare function isPie(item: Option): boolean;
17
+ export declare function pieRadii(item: Option): number[];
18
+ export declare function hasUnit(axis: Option): boolean;
19
+ export declare const defaultColors: Set<string>;
20
+ export type Context = Required<Pick<CheckOptions, 'pack' | 'mode'>> & CheckOptions;
21
+ export type Emit = (path: string, message?: string, severity?: Severity) => void;
22
+ /** Keep finding metadata and default messages consistent across all rule checks. */
23
+ export declare function rule(number: number, name: string, severity: Severity, description: string, fix: string, run: (facts: PageFacts, context: Context, emit: Emit) => void, cnOnly?: boolean): Rule;
24
+ export declare function charts(facts: PageFacts, visit: (option: Option, path: string) => void): void;
25
+ export declare function background(facts: PageFacts, context: Context, option?: Option): string;
26
+ /** Allow small rounding differences between hex and computed CSS colours. */
27
+ export declare function sameColor(first: string, second: string): boolean;
28
+ export declare function deltaDirection(text: string): 'up' | 'down' | 'flat';
29
+ export declare function conventionColor(color: string, expected: string): boolean;
30
+ /** Narrow JSON objects before inspecting extension fields. */
31
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
32
+ export {};
@@ -0,0 +1,161 @@
1
+ import { rgb } from '../color.js';
2
+ import { getTokens } from '../packs/index.js';
3
+ export function asArray(value) {
4
+ if (value == null)
5
+ return [];
6
+ return Array.isArray(value) ? value : [value];
7
+ }
8
+ export function series(option) {
9
+ return asArray(option.series);
10
+ }
11
+ /** An axis without a declared type or category data defaults to a value axis. */
12
+ export function valueAxes(option) {
13
+ const axes = [];
14
+ for (const dimension of ['x', 'y']) {
15
+ const options = dimension === 'x' ? option.xAxis : option.yAxis;
16
+ asArray(options).forEach((axis, index) => {
17
+ if (axis.type === 'value' || (!axis.type && !axis.data)) {
18
+ axes.push({
19
+ ...axis,
20
+ _dimension: dimension,
21
+ _index: index,
22
+ _path: `${dimension}Axis[${index}]`,
23
+ });
24
+ }
25
+ });
26
+ }
27
+ return axes;
28
+ }
29
+ /** Data may contain numbers, coordinate tuples, or objects with a value field. */
30
+ export function seriesValues(item) {
31
+ return asArray(item.data).flatMap((datum) => {
32
+ const value = isRecord(datum) ? datum.value : datum;
33
+ return asArray(value).filter((entry) => typeof entry === 'number' && Number.isFinite(entry));
34
+ });
35
+ }
36
+ export function seriesColor(item, index, option) {
37
+ const palette = asArray(option.color);
38
+ return (item.lineStyle?.color ?? item.itemStyle?.color ?? palette[index % Math.max(1, palette.length)]);
39
+ }
40
+ export function allColors(option) {
41
+ const colors = [...asArray(option.color)];
42
+ series(option).forEach((item, index) => {
43
+ colors.push(seriesColor(item, index, option));
44
+ for (const datum of asArray(item.data)) {
45
+ if (isRecord(datum) && isRecord(datum.itemStyle))
46
+ colors.push(datum.itemStyle.color);
47
+ }
48
+ });
49
+ return colors.filter((color) => typeof color === 'string');
50
+ }
51
+ export function walk(value, visit) {
52
+ if (!value || typeof value !== 'object')
53
+ return;
54
+ for (const [key, child] of Object.entries(value)) {
55
+ visit(key, child);
56
+ walk(child, visit);
57
+ }
58
+ }
59
+ export function isPie(item) {
60
+ return item.type === 'pie';
61
+ }
62
+ export function pieRadii(item) {
63
+ return asArray(item.radius).map((radius) => parseFloat(String(radius)));
64
+ }
65
+ export function hasUnit(axis) {
66
+ return !!(axis.name ||
67
+ axis.unit ||
68
+ axis.unitMetadata ||
69
+ axis.meta?.unit ||
70
+ axis.axisLabel?.formatter ||
71
+ (Array.isArray(axis.renderedTickLabels) &&
72
+ axis.renderedTickLabels.some((text) => /[$¥€£%万亿]|\d\s*[KMB]\b|\d\s*(?:元|人|件|次|ms|s|kg|days|months)\b/i.test(String(text)))));
73
+ }
74
+ export const defaultColors = new Set([
75
+ // ECharts 6 default theme, alongside the ECharts 5 palette below.
76
+ '#5070dd',
77
+ '#b6d634',
78
+ '#505372',
79
+ '#ff994d',
80
+ '#0ca8df',
81
+ '#ffd10a',
82
+ '#fb628b',
83
+ '#785db0',
84
+ '#3fbe95',
85
+ '#5470c6',
86
+ '#91cc75',
87
+ '#fac858',
88
+ '#ee6666',
89
+ '#73c0de',
90
+ '#3ba272',
91
+ '#fc8452',
92
+ '#9a60b4',
93
+ '#ea7ccc',
94
+ ]);
95
+ /** Keep finding metadata and default messages consistent across all rule checks. */
96
+ export function rule(number, name, severity, description, fix, run, cnOnly = false) {
97
+ const id = `DT${String(number).padStart(3, '0')}`;
98
+ return {
99
+ id,
100
+ name,
101
+ severity,
102
+ packs: cnOnly ? ['cn'] : ['global', 'cn'],
103
+ description,
104
+ fix,
105
+ check(facts, context) {
106
+ const findings = [];
107
+ const emit = (path, message = name.replaceAll('-', ' '), level = severity) => {
108
+ findings.push({ ruleId: id, severity: level, message, path, fix });
109
+ };
110
+ run(facts, context, emit);
111
+ return findings;
112
+ },
113
+ };
114
+ }
115
+ export function charts(facts, visit) {
116
+ facts.charts.forEach((chart, index) => visit(chart.option, `charts[${index}]`));
117
+ }
118
+ export function background(facts, context, option) {
119
+ const tokens = getTokens(context.pack, context.mode);
120
+ return (context.background ??
121
+ option?.backgroundColor ??
122
+ facts.page?.background ??
123
+ String(tokens.paper ?? tokens.card ?? tokens.panel));
124
+ }
125
+ /** Allow small rounding differences between hex and computed CSS colours. */
126
+ export function sameColor(first, second) {
127
+ const firstChannels = rgb(first);
128
+ const secondChannels = rgb(second);
129
+ return (!!firstChannels &&
130
+ !!secondChannels &&
131
+ firstChannels.every((channel, index) => Math.abs(channel - secondChannels[index]) < 0.015));
132
+ }
133
+ export function deltaDirection(text) {
134
+ if (/[▼↓]|-\s*\d/.test(text))
135
+ return 'down';
136
+ if (/[▲↑]|\+\s*\d/.test(text))
137
+ return 'up';
138
+ return 'flat';
139
+ }
140
+ function dominantChannel(channels) {
141
+ const [red, green, blue] = channels;
142
+ if (red > green * 1.15 && red > blue * 1.15)
143
+ return 'red';
144
+ if (green > red * 1.15 && green > blue * 1.05)
145
+ return 'green';
146
+ return 'other';
147
+ }
148
+ export function conventionColor(color, expected) {
149
+ if (sameColor(color, expected))
150
+ return true;
151
+ const actualChannels = rgb(color);
152
+ const expectedChannels = rgb(expected);
153
+ if (!actualChannels || !expectedChannels)
154
+ return false;
155
+ const actualChannel = dominantChannel(actualChannels);
156
+ return actualChannel !== 'other' && actualChannel === dominantChannel(expectedChannels);
157
+ }
158
+ /** Narrow JSON objects before inspecting extension fields. */
159
+ export function isRecord(value) {
160
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
161
+ }
@@ -0,0 +1,2 @@
1
+ import type { Rule } from '../types.js';
2
+ export declare const rules: Rule[];
@@ -0,0 +1,58 @@
1
+ import { dt028 } from './dt028-hero-metric.js';
2
+ import { dt027 } from './dt027-tabular-numerals.js';
3
+ import { dt026 } from './dt026-line-weight.js';
4
+ import { dt025 } from './dt025-legend-not-direct-label.js';
5
+ import { dt024 } from './dt024-bar-width.js';
6
+ import { dt001 } from './dt001-default-palette.js';
7
+ import { dt002 } from './dt002-rainbow-palette.js';
8
+ import { dt003 } from './dt003-smooth-line.js';
9
+ import { dt004 } from './dt004-symbols-everywhere.js';
10
+ import { dt005 } from './dt005-pie-slices.js';
11
+ import { dt006 } from './dt006-pie-overuse.js';
12
+ import { dt007 } from './dt007-three-d.js';
13
+ import { dt008 } from './dt008-dual-axis.js';
14
+ import { dt009 } from './dt009-missing-unit.js';
15
+ import { dt010 } from './dt010-bar-baseline.js';
16
+ import { dt011 } from './dt011-raw-large-numbers.js';
17
+ import { dt012 } from './dt012-unit-system.js';
18
+ import { dt013 } from './dt013-delta-convention.js';
19
+ import { dt014 } from './dt014-delta-colour-only.js';
20
+ import { dt015 } from './dt015-low-contrast-text.js';
21
+ import { dt016 } from './dt016-thin-line-contrast.js';
22
+ import { dt017 } from './dt017-gradient-decoration.js';
23
+ import { dt018 } from './dt018-glow.js';
24
+ import { dt019 } from './dt019-kpi-overload.js';
25
+ import { dt020 } from './dt020-kpi-rainbow.js';
26
+ import { dt021 } from './dt021-emoji-icons.js';
27
+ import { dt022 } from './dt022-cjk-min-size.js';
28
+ import { dt023 } from './dt023-legend-single-series.js';
29
+ export const rules = [
30
+ dt001,
31
+ dt002,
32
+ dt003,
33
+ dt004,
34
+ dt005,
35
+ dt006,
36
+ dt007,
37
+ dt008,
38
+ dt009,
39
+ dt010,
40
+ dt011,
41
+ dt012,
42
+ dt013,
43
+ dt014,
44
+ dt015,
45
+ dt016,
46
+ dt017,
47
+ dt018,
48
+ dt019,
49
+ dt020,
50
+ dt021,
51
+ dt022,
52
+ dt023,
53
+ dt024,
54
+ dt025,
55
+ dt026,
56
+ dt027,
57
+ dt028,
58
+ ];
@@ -0,0 +1,122 @@
1
+ import type { PackName, Mode, DeltaColors, Polarity } from './packs/index.js';
2
+ /** JSON-compatible ECharts options; unknown extension fields are retained. */
3
+ export interface ChartStyle {
4
+ type?: string;
5
+ color?: string | Option;
6
+ formatter?: string | ((...args: unknown[]) => unknown);
7
+ width?: number;
8
+ fontSize?: number;
9
+ fontWeight?: number | 'bold' | 'normal';
10
+ show?: boolean;
11
+ }
12
+ /** Fields inspected by the rules; other ECharts fields remain available as unknown. */
13
+ export interface Option {
14
+ [key: string]: unknown;
15
+ series?: Option | Option[];
16
+ xAxis?: Option | Option[];
17
+ yAxis?: Option | Option[];
18
+ legend?: Option | Option[];
19
+ color?: string | string[];
20
+ backgroundColor?: string;
21
+ type?: string;
22
+ data?: unknown[];
23
+ lineStyle?: ChartStyle;
24
+ itemStyle?: ChartStyle;
25
+ textStyle?: ChartStyle;
26
+ axisLabel?: ChartStyle;
27
+ label?: ChartStyle;
28
+ tooltip?: {
29
+ show?: boolean;
30
+ formatter?: unknown;
31
+ valueFormatter?: unknown;
32
+ };
33
+ name?: string;
34
+ unit?: unknown;
35
+ unitMetadata?: unknown;
36
+ meta?: {
37
+ unit?: unknown;
38
+ };
39
+ radius?: string | number | (string | number)[];
40
+ smooth?: boolean | number;
41
+ showSymbol?: boolean;
42
+ symbol?: string;
43
+ roseType?: string | boolean;
44
+ min?: number | string;
45
+ scale?: boolean;
46
+ }
47
+ export interface ChartFacts {
48
+ id?: string;
49
+ option: Option;
50
+ }
51
+ export interface TextFact {
52
+ selector: string;
53
+ text: string;
54
+ color: string;
55
+ background: string;
56
+ fontSize: number;
57
+ fontWeight: number;
58
+ fontVariantNumeric?: string;
59
+ fontFeatureSettings?: string;
60
+ }
61
+ export interface DeltaFact {
62
+ selector: string;
63
+ text: string;
64
+ color: string;
65
+ direction?: 'up' | 'down' | 'flat';
66
+ polarity?: Polarity;
67
+ }
68
+ export interface PageFacts {
69
+ charts: ChartFacts[];
70
+ texts?: TextFact[];
71
+ gradients?: {
72
+ selector: string;
73
+ background: string;
74
+ }[];
75
+ shadows?: {
76
+ selector: string;
77
+ boxShadow: string;
78
+ textShadow: string;
79
+ }[];
80
+ kpis?: {
81
+ selector: string;
82
+ text: string;
83
+ color: string;
84
+ background?: string;
85
+ fontSize?: number;
86
+ fontVariantNumeric?: string;
87
+ fontFeatureSettings?: string;
88
+ }[];
89
+ deltas?: DeltaFact[];
90
+ emojis?: {
91
+ selector: string;
92
+ text: string;
93
+ }[];
94
+ page?: {
95
+ background?: string;
96
+ deltaColors?: DeltaColors;
97
+ };
98
+ }
99
+ export interface CheckOptions {
100
+ pack?: PackName;
101
+ mode?: Mode;
102
+ background?: string;
103
+ disable?: string[];
104
+ only?: string[];
105
+ }
106
+ export type Severity = 'error' | 'warn' | 'info';
107
+ export interface Finding {
108
+ ruleId: string;
109
+ severity: Severity;
110
+ message: string;
111
+ path: string;
112
+ fix: string;
113
+ }
114
+ export interface Rule {
115
+ id: string;
116
+ name: string;
117
+ severity: Severity;
118
+ packs: PackName[];
119
+ description: string;
120
+ fix: string;
121
+ check: (facts: PageFacts, options: Required<Pick<CheckOptions, 'pack' | 'mode'>> & CheckOptions) => Finding[];
122
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};