@afokapu/atdd-bun 0.10.96 → 0.10.98
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/conventions/_profiles/coder.yaml +3 -3
- package/conventions/_profiles/design.yaml +3 -3
- package/conventions/coder.bun/coder.bun.design-primitives.convention.yaml +10 -3
- package/conventions/coder.bun/coder.bun.design-token-color.convention.yaml +8 -4
- package/conventions/coder.bun/coder.bun.design-token-hardcoded.convention.yaml +5 -2
- package/detectors/atdd_traceability_closure/detect.mjs +11 -6
- package/detectors/bun_design_system_detector/checks/_design.mjs +5 -2
- package/detectors/bun_design_system_detector/checks/design_primitives.mjs +15 -6
- package/detectors/bun_design_system_detector/checks/design_token_color.mjs +10 -6
- package/detectors/bun_design_system_detector/checks/design_token_hardcoded.mjs +12 -8
- package/integrity.json +13 -13
- package/lib/scan.mjs +55 -21
- package/package.json +1 -1
- package/relationships.yaml +3 -3
|
@@ -399,7 +399,7 @@ relationships:
|
|
|
399
399
|
control: "internal"
|
|
400
400
|
foundation: "finish_to_start"
|
|
401
401
|
origin: "@afokapu/atdd-bun"
|
|
402
|
-
reason: "Bun realization of the core obligation coder.design.primitives, checked over .tsx, .jsx, .html and .
|
|
402
|
+
reason: "Bun realization of the core obligation coder.design.primitives, checked over .tsx, .jsx, .html, .css and HTML authored in .ts/.mts literals"
|
|
403
403
|
source_ref: "coder.bun.design-primitives"
|
|
404
404
|
strength: "important"
|
|
405
405
|
target_ref: "coder.design.primitives"
|
|
@@ -429,7 +429,7 @@ relationships:
|
|
|
429
429
|
control: "internal"
|
|
430
430
|
foundation: "finish_to_start"
|
|
431
431
|
origin: "@afokapu/atdd-bun"
|
|
432
|
-
reason: "Bun realization of the core obligation coder.design.token-color, checked over .tsx, .jsx, .html and .
|
|
432
|
+
reason: "Bun realization of the core obligation coder.design.token-color, checked over .tsx, .jsx, .html, .css and HTML authored in .ts/.mts literals"
|
|
433
433
|
source_ref: "coder.bun.design-token-color"
|
|
434
434
|
strength: "important"
|
|
435
435
|
target_ref: "coder.design.token-color"
|
|
@@ -439,7 +439,7 @@ relationships:
|
|
|
439
439
|
control: "internal"
|
|
440
440
|
foundation: "finish_to_start"
|
|
441
441
|
origin: "@afokapu/atdd-bun"
|
|
442
|
-
reason: "Bun realization of the core obligation coder.design.token-hardcoded, checked over .tsx, .jsx, .html and .
|
|
442
|
+
reason: "Bun realization of the core obligation coder.design.token-hardcoded, checked over .tsx, .jsx, .html, .css and HTML authored in .ts/.mts literals"
|
|
443
443
|
source_ref: "coder.bun.design-token-hardcoded"
|
|
444
444
|
strength: "important"
|
|
445
445
|
target_ref: "coder.design.token-hardcoded"
|
|
@@ -92,7 +92,7 @@ relationships:
|
|
|
92
92
|
control: "internal"
|
|
93
93
|
foundation: "finish_to_start"
|
|
94
94
|
origin: "@afokapu/atdd-bun"
|
|
95
|
-
reason: "Bun realization of the core obligation coder.design.primitives, checked over .tsx, .jsx, .html and .
|
|
95
|
+
reason: "Bun realization of the core obligation coder.design.primitives, checked over .tsx, .jsx, .html, .css and HTML authored in .ts/.mts literals"
|
|
96
96
|
source_ref: "coder.bun.design-primitives"
|
|
97
97
|
strength: "important"
|
|
98
98
|
target_ref: "coder.design.primitives"
|
|
@@ -122,7 +122,7 @@ relationships:
|
|
|
122
122
|
control: "internal"
|
|
123
123
|
foundation: "finish_to_start"
|
|
124
124
|
origin: "@afokapu/atdd-bun"
|
|
125
|
-
reason: "Bun realization of the core obligation coder.design.token-color, checked over .tsx, .jsx, .html and .
|
|
125
|
+
reason: "Bun realization of the core obligation coder.design.token-color, checked over .tsx, .jsx, .html, .css and HTML authored in .ts/.mts literals"
|
|
126
126
|
source_ref: "coder.bun.design-token-color"
|
|
127
127
|
strength: "important"
|
|
128
128
|
target_ref: "coder.design.token-color"
|
|
@@ -132,7 +132,7 @@ relationships:
|
|
|
132
132
|
control: "internal"
|
|
133
133
|
foundation: "finish_to_start"
|
|
134
134
|
origin: "@afokapu/atdd-bun"
|
|
135
|
-
reason: "Bun realization of the core obligation coder.design.token-hardcoded, checked over .tsx, .jsx, .html and .
|
|
135
|
+
reason: "Bun realization of the core obligation coder.design.token-hardcoded, checked over .tsx, .jsx, .html, .css and HTML authored in .ts/.mts literals"
|
|
136
136
|
source_ref: "coder.bun.design-token-hardcoded"
|
|
137
137
|
strength: "important"
|
|
138
138
|
target_ref: "coder.design.token-hardcoded"
|
|
@@ -5,16 +5,20 @@ status: active
|
|
|
5
5
|
name: App components compose design-system primitives for interactive controls
|
|
6
6
|
statement: An app component (`.tsx`/`.jsx` outside the design root) MUST render interactive controls through a design-system
|
|
7
7
|
primitive rather than a raw `<button>`, `<input>`, `<select>`, or `<textarea>`, so controls stay themed, accessible,
|
|
8
|
-
and consistent.
|
|
8
|
+
and consistent. The same obligation covers controls authored as HTML in the bodies of string, template, and
|
|
9
|
+
tagged-template literals in `.ts`/`.mts` files outside the design root — the shape an htmx/Bun server-rendered
|
|
10
|
+
app writes. Bun realization of `coder.design.primitives`, mirroring `coder.vite.design-primitives`.
|
|
9
11
|
terms:
|
|
10
12
|
- term_id: design_root
|
|
11
13
|
text: 'a directory named `design`, `design_system`, or `design-system`. The first directory beneath it names the
|
|
12
14
|
layer: `tokens` or `foundations`, `primitives`, `components`, `templates`. A repository with no design root
|
|
13
15
|
is not judged (self-scoping): there is no system to connect to yet.'
|
|
14
16
|
- term_id: raw_control
|
|
15
|
-
text: a lowercase `<button`, `<input`, `<select`, or `<textarea` element written in app code
|
|
17
|
+
text: 'a lowercase `<button`, `<input`, `<select`, or `<textarea` element written in app code — in JSX, or in the
|
|
18
|
+
body of a string/template/tagged-template literal in a `.ts`/`.mts` file, followed by whitespace, `>`, or `/`.'
|
|
16
19
|
content:
|
|
17
|
-
summary: Flags raw interactive platform elements in component source outside the
|
|
20
|
+
summary: Flags raw interactive platform elements in component source and in `.ts`/`.mts` literal HTML outside the
|
|
21
|
+
design root.
|
|
18
22
|
normative_text: Controls are where focus, disabled, error, and loading states live. Each raw control re-implements
|
|
19
23
|
them, and each re-implementation diverges.
|
|
20
24
|
fix_hint: |
|
|
@@ -38,6 +42,9 @@ content:
|
|
|
38
42
|
- Test files (`*.test.*`, `*.spec.*`) and vendored/build directories are never scanned.
|
|
39
43
|
- The design system's own primitives render the raw elements and are not scanned.
|
|
40
44
|
- Plain `.html` templates cannot import components and are out of scope.
|
|
45
|
+
- 'Only literal text is judged in `.ts`/`.mts` files: code position (comparisons, angle-bracket type
|
|
46
|
+
assertions), comments, and regex literals are never treated as markup, and a control spanning a `${…}`
|
|
47
|
+
interpolation is read on its literal chunks alone.'
|
|
41
48
|
metadata:
|
|
42
49
|
aliases:
|
|
43
50
|
- BUN-DESIGN-PRIMITIVES-001
|
|
@@ -5,8 +5,10 @@ status: active
|
|
|
5
5
|
name: UI files use color tokens, not raw color literals
|
|
6
6
|
statement: Outside the tokens layer, a UI file (`.tsx`, `.jsx`, `.html`, `.htm`, `.css`) MUST NOT write a raw color
|
|
7
7
|
literal — `#rgb`/`#rrggbb`/`#rrggbbaa`, `rgb()`/`rgba()`, `hsl()`/`hsla()` — in a CSS declaration, a `style` attribute,
|
|
8
|
-
a `<style>` block, a JSX `style={{…}}` object, or a `fill`/`stroke`/`bgcolor` attribute.
|
|
9
|
-
|
|
8
|
+
a `<style>` block, a JSX `style={{…}}` object, or a `fill`/`stroke`/`bgcolor` attribute. The same declarations are
|
|
9
|
+
judged in inline `style="…"` attributes and `<style>` text authored in the HTML a `.ts`/`.mts` file renders from
|
|
10
|
+
string/template/tagged-template literals. Colors come from tokens (`var(--…)`). Bun realization of
|
|
11
|
+
`coder.design.token-color`, mirroring `coder.vite.design-token-color`.
|
|
10
12
|
terms:
|
|
11
13
|
- term_id: design_root
|
|
12
14
|
text: 'a directory named `design`, `design_system`, or `design-system`. The first directory beneath it names the
|
|
@@ -16,8 +18,8 @@ terms:
|
|
|
16
18
|
text: 'a hex, rgb(a), or hsl(a) color written in a declaration value. Selectors are not values: `#fade:hover {`
|
|
17
19
|
is never read as a color.'
|
|
18
20
|
content:
|
|
19
|
-
summary: Reads declarations from stylesheets, style attributes, style blocks,
|
|
20
|
-
raw color literals in their values.
|
|
21
|
+
summary: Reads declarations from stylesheets, style attributes, style blocks, JSX style objects, and `.ts`/`.mts`
|
|
22
|
+
literal HTML, and flags raw color literals in their values.
|
|
21
23
|
normative_text: A raw color is a theme decision made in one place. It does not follow dark mode, it drifts from
|
|
22
24
|
the palette, and nobody can find it when the palette changes.
|
|
23
25
|
fix_hint: |
|
|
@@ -42,6 +44,8 @@ content:
|
|
|
42
44
|
- 'Custom property definitions (`--danger: #b91c1c`) define tokens wherever they live and are not flagged.'
|
|
43
45
|
- The tokens/foundations layer is where raw colors are defined and is not scanned.
|
|
44
46
|
- Named colors (`red`, `white`) and `currentColor`/`transparent` are not flagged.
|
|
47
|
+
- 'Only literal text is judged in `.ts`/`.mts` files: code, comments, and regex literals are never read as
|
|
48
|
+
declarations, and a value spanning a `${…}` interpolation is judged on its literal chunks alone.'
|
|
45
49
|
metadata:
|
|
46
50
|
aliases:
|
|
47
51
|
- BUN-DESIGN-TOKEN-COLOR-001
|
|
@@ -6,8 +6,9 @@ name: UI files take spacing, radii, and durations from foundations
|
|
|
6
6
|
statement: Outside the tokens layer, a UI file MUST NOT hardcode a spacing, inset, or border-radius length greater
|
|
7
7
|
than 1px (`margin`, `padding`, `gap`, `inset`, `top`/`right`/`bottom`/`left`, `border-radius`), nor a non-zero
|
|
8
8
|
duration in `transition`/`animation`. A bare number in a JSX style object for those properties counts as a length.
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
The same declarations are judged in inline `style="…"` attributes and `<style>` text authored in the HTML a
|
|
10
|
+
`.ts`/`.mts` file renders from string/template/tagged-template literals. These values come from foundations
|
|
11
|
+
tokens. Bun realization of `coder.design.token-hardcoded` and of the raw-pixel part of `coder.design.foundations`.
|
|
11
12
|
terms:
|
|
12
13
|
- term_id: design_root
|
|
13
14
|
text: 'a directory named `design`, `design_system`, or `design-system`. The first directory beneath it names the
|
|
@@ -43,6 +44,8 @@ content:
|
|
|
43
44
|
- 'Custom property definitions (`--space-3: 12px`) define tokens wherever they live and are not flagged.'
|
|
44
45
|
- The tokens/foundations layer defines these values and is not scanned.
|
|
45
46
|
- Widths, heights, and font sizes are out of scope for this rule.
|
|
47
|
+
- 'Only literal text is judged in `.ts`/`.mts` files: code, comments, and regex literals are never read as
|
|
48
|
+
declarations, and a value spanning a `${…}` interpolation is judged on its literal chunks alone.'
|
|
46
49
|
metadata:
|
|
47
50
|
aliases:
|
|
48
51
|
- BUN-DESIGN-TOKEN-HARDCODED-001
|
|
@@ -12,7 +12,7 @@ if (!report || !Array.isArray(roots) || !roots.length) process.exit(2);
|
|
|
12
12
|
const sourceExtensions = new Set([".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"]);
|
|
13
13
|
const testName = /(?:^test_.*|.*(?:\.test|\.spec|\.e2e|_test))\.(?:[cm]?[jt]sx?)$/;
|
|
14
14
|
const violations = [];
|
|
15
|
-
const plans = { acc: new Set(), wmbt: new Set(), train: new Set() };
|
|
15
|
+
const plans = { acc: new Set(), wmbt: new Set(), train: new Set(), journey: new Set() };
|
|
16
16
|
const tests = new Map();
|
|
17
17
|
const declaredAt = new Map();
|
|
18
18
|
const sources = [];
|
|
@@ -36,7 +36,8 @@ function add(rule_id, file, line, evidence, source_line = "") {
|
|
|
36
36
|
function lineOf(content, token) { return lineAt(content, content.indexOf(token)); }
|
|
37
37
|
function lineAt(content, index) { return content.slice(0, index).split("\n").length; }
|
|
38
38
|
// The identity each binding header must carry.
|
|
39
|
-
const PREFIX = { Acceptance: "acc:", WMBT: "wmbt:", Train: "train:" };
|
|
39
|
+
const PREFIX = { Acceptance: "acc:", WMBT: "wmbt:", Train: "train:", Journey: "journey:" };
|
|
40
|
+
const JOURNEY_ID = /^journey:[a-z][a-z0-9-]*$/;
|
|
40
41
|
|
|
41
42
|
for (const root of roots) {
|
|
42
43
|
// `plan_root: .` (or `./`) must still match root-relative paths, which never start with `./`.
|
|
@@ -44,12 +45,16 @@ for (const root of roots) {
|
|
|
44
45
|
walk(root, (path) => {
|
|
45
46
|
const name = path.split("/").pop() || "";
|
|
46
47
|
const content = text(path);
|
|
47
|
-
|
|
48
|
+
const planPath = relative(root, path).replaceAll("\\", "/");
|
|
49
|
+
if (/\.ya?ml$/i.test(name) && planPath.startsWith(planPrefix)) {
|
|
48
50
|
for (const [kind, re] of Object.entries({
|
|
49
51
|
acc: /\bacc:[A-Za-z0-9_.:-]+/g,
|
|
50
52
|
wmbt: /\bwmbt:[A-Za-z0-9_.:-]+/g,
|
|
51
53
|
train: /\btrain:[A-Za-z0-9_.:-]+/g,
|
|
52
54
|
})) for (const match of content.matchAll(re)) plans[kind].add(match[0]);
|
|
55
|
+
if (planPath.startsWith(`${planPrefix}_journeys/`)) {
|
|
56
|
+
for (const match of content.matchAll(/["']?\bjourney_id["']?[ \t]*:[ \t]*["']?(journey:[A-Za-z0-9_.:-]+)/g)) plans.journey.add(match[1]);
|
|
57
|
+
}
|
|
53
58
|
// Where each acceptance is DECLARED (its `urn:` line), so a missing test is reported on the artifact to fix.
|
|
54
59
|
for (const m of content.matchAll(/["']?\burn["']?[ \t]*:[ \t]*["']?(acc:[A-Za-z0-9_.:-]+)/g)) if (!declaredAt.has(m[1])) declaredAt.set(m[1], { path, line: lineAt(content, m.index) });
|
|
55
60
|
return;
|
|
@@ -60,14 +65,14 @@ for (const root of roots) {
|
|
|
60
65
|
const urn = head.match(/^\s*\/\/\s*URN:\s*(test:[^\s]+)/m);
|
|
61
66
|
if (!urn) return;
|
|
62
67
|
// EVERY binding header is judged, not only the first: a valid Acceptance: must not carry an unresolved Train:.
|
|
63
|
-
const headers = [...head.matchAll(/^[ \t]*\/\/[ \t]*(Acceptance|WMBT|Train):[ \t]*(\S*)/gm)].map((m) => ({ kind: m[1], id: m[2], raw: m[0].trim(), line: lineAt(head, m.index) }));
|
|
64
|
-
const bindings = headers.filter((h) => h.id.startsWith(PREFIX[h.kind]));
|
|
68
|
+
const headers = [...head.matchAll(/^[ \t]*\/\/[ \t]*(Acceptance|WMBT|Train|Journey):[ \t]*(\S*)/gm)].map((m) => ({ kind: m[1], id: m[2], raw: m[0].trim(), line: lineAt(head, m.index) }));
|
|
69
|
+
const bindings = headers.filter((h) => h.kind === "Journey" ? JOURNEY_ID.test(h.id) : h.id.startsWith(PREFIX[h.kind]));
|
|
65
70
|
// A test identity may be reused by equivalent smoke harnesses. Preserve every
|
|
66
71
|
// independently declared binding: replacing the earlier record made a valid
|
|
67
72
|
// acceptance disappear solely because a later file had the same test URN.
|
|
68
73
|
const prior = tests.get(urn[1]);
|
|
69
74
|
tests.set(urn[1], { path, bindings: [...(prior?.bindings ?? []), ...bindings] });
|
|
70
|
-
if (!headers.length) add("traceability.test.binding-resolves", path, lineOf(head, urn[0]), "test has a URN but no Acceptance:, WMBT:, or
|
|
75
|
+
if (!headers.length) add("traceability.test.binding-resolves", path, lineOf(head, urn[0]), "test has a URN but no Acceptance:, WMBT:, Train:, or Journey: binding", urn[0]);
|
|
71
76
|
for (const h of headers) if (!bindings.includes(h)) add("traceability.test.binding-resolves", path, h.line, `${h.kind}: ${h.id || "<empty>"} is not a ${PREFIX[h.kind]} identity`, h.raw);
|
|
72
77
|
return;
|
|
73
78
|
}
|
|
@@ -71,14 +71,17 @@ export function designTarget(file, specifier) {
|
|
|
71
71
|
return d ? { layer: d.layer, layerName: d.layerName, root: d.root, path: t.path } : null;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export function masked(file) {
|
|
74
|
+
export function masked(file, opts) {
|
|
75
75
|
const text = readText(file);
|
|
76
76
|
if (text === null) return null;
|
|
77
|
-
return { text, code: extname(file) === ".css" ? text.replace(/\/\*[\s\S]*?\*\//g, (c) => c.replace(/[^\n]/g, " ")) : maskLiteralsAndComments(text) };
|
|
77
|
+
return { text, code: extname(file) === ".css" ? text.replace(/\/\*[\s\S]*?\*\//g, (c) => c.replace(/[^\n]/g, " ")) : maskLiteralsAndComments(text, opts) };
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export const isUi = (file) => UI_EXT.has(extname(file));
|
|
81
81
|
export const isComponentSource = (file) => extname(file) === ".tsx" || extname(file) === ".jsx";
|
|
82
|
+
// `.ts`/`.mts` app files render HTML from string/template/tagged-template literals: the
|
|
83
|
+
// literals-only mask keeps those bodies visible for the design rules that judge rendered markup.
|
|
84
|
+
export const isTsSource = (file) => { const e = extname(file); return e === ".ts" || e === ".mts"; };
|
|
82
85
|
export const hasDesignRoot = (files) => files.some((file) => designOf(file));
|
|
83
86
|
|
|
84
87
|
const kebab = (prop) => prop.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// Detector: coder.bun.design-primitives
|
|
3
3
|
// App components (outside the design root) render interactive controls through the
|
|
4
|
-
// design-system primitive, not raw <button>/<input>/<select>/<textarea
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
// design-system primitive, not raw <button>/<input>/<select>/<textarea> — including the
|
|
5
|
+
// HTML an app renders from string/template/tagged-template literals in .ts/.mts files,
|
|
6
|
+
// the shape an htmx/Bun server-rendered app actually writes.
|
|
7
|
+
import { readRoots, readExcludes, emit, locate } from "../../../lib/scan.mjs";
|
|
8
|
+
import { scope, designOf, isComponentSource, isTsSource, masked } from "./_design.mjs";
|
|
7
9
|
|
|
8
10
|
const RULE_ID = "coder.bun.design-primitives";
|
|
9
11
|
const violations = [];
|
|
10
12
|
for (const file of scope(readRoots(), readExcludes())) {
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
if (designOf(file)) continue;
|
|
14
|
+
// Component sources are judged on code with literals masked; .ts/.mts sources on literal
|
|
15
|
+
// bodies with code masked (the keepLiterals mode), so a match is always authored markup.
|
|
16
|
+
const component = isComponentSource(file), literalHtml = !component && isTsSource(file);
|
|
17
|
+
if (!component && !literalHtml) continue;
|
|
18
|
+
const m = masked(file, literalHtml ? { keepLiterals: true } : undefined);
|
|
19
|
+
if (!m) continue;
|
|
20
|
+
const pattern = component ? /<(button|input|select|textarea)\b/g : /<(button|input|select|textarea)(?=[\s/>])/g;
|
|
21
|
+
for (const c of m.code.matchAll(pattern))
|
|
22
|
+
violations.push({ rule_id: RULE_ID, file, ...locate(m.text, c.index), evidence: `raw <${c[1]}> in app code; compose the design-system primitive instead` });
|
|
14
23
|
}
|
|
15
24
|
process.stderr.write(`bun-detector[design-primitives]: ${violations.length} violation(s)\n`);
|
|
16
25
|
emit(violations);
|
|
@@ -3,19 +3,23 @@
|
|
|
3
3
|
// Outside the tokens layer, UI files (.tsx/.jsx/.html/.css) take colors from tokens
|
|
4
4
|
// (`var(--…)`), never raw hex, rgb(), or hsl() literals. Defining a custom
|
|
5
5
|
// property (`--danger: #b91c1c`) is defining a token, not using a raw value.
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// The same declarations are judged in the HTML a .ts/.mts file renders from
|
|
7
|
+
// string/template/tagged-template literals, read through the literals-only mask.
|
|
8
|
+
import { readRoots, readExcludes, emit, locate } from "../../../lib/scan.mjs";
|
|
9
|
+
import { scope, designOf, isUi, isTsSource, masked, declarations } from "./_design.mjs";
|
|
8
10
|
|
|
9
11
|
const RULE_ID = "coder.bun.design-token-color";
|
|
10
12
|
const COLOR_RE = /#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?)\s*\(/;
|
|
11
13
|
const violations = [];
|
|
12
14
|
for (const file of scope(readRoots(), readExcludes())) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const literalHtml = !isUi(file) && isTsSource(file);
|
|
16
|
+
if ((!isUi(file) && !literalHtml) || designOf(file)?.layer === 0) continue;
|
|
17
|
+
const src = masked(file, literalHtml ? { keepLiterals: true } : undefined);
|
|
18
|
+
if (!src) continue;
|
|
19
|
+
for (const d of declarations(file, literalHtml ? src.code : src.text)) {
|
|
16
20
|
if (d.prop.startsWith("--")) continue;
|
|
17
21
|
const hit = d.value.match(COLOR_RE);
|
|
18
|
-
if (hit) violations.push({ rule_id: RULE_ID, file, ...locate(text, d.index), evidence: `${d.prop} uses the raw color ${hit[0].replace(/\s*\($/, "()")}; reference a color token (var(--…)) instead` });
|
|
22
|
+
if (hit) violations.push({ rule_id: RULE_ID, file, ...locate(src.text, d.index), evidence: `${d.prop} uses the raw color ${hit[0].replace(/\s*\($/, "()")}; reference a color token (var(--…)) instead` });
|
|
19
23
|
}
|
|
20
24
|
}
|
|
21
25
|
process.stderr.write(`bun-detector[design-token-color]: ${violations.length} violation(s)\n`);
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// Outside the tokens layer, spacing, radii, and durations come from foundations.
|
|
4
4
|
// 0 and 1px (hairline borders) are not design decisions and are allowed. Custom
|
|
5
5
|
// property definitions (`--space-3: 12px`) define tokens and are never judged.
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// The same declarations are judged in the HTML a .ts/.mts file renders from
|
|
7
|
+
// string/template/tagged-template literals, read through the literals-only mask.
|
|
8
|
+
import { readRoots, readExcludes, emit, locate } from "../../../lib/scan.mjs";
|
|
9
|
+
import { scope, designOf, isUi, isTsSource, masked, declarations } from "./_design.mjs";
|
|
8
10
|
|
|
9
11
|
const RULE_ID = "coder.bun.design-token-hardcoded";
|
|
10
12
|
const LENGTH_PROP = /^(margin|padding|gap|row-gap|column-gap|inset|top|right|bottom|left|border-radius)(-|$)/;
|
|
@@ -12,16 +14,18 @@ const DURATION_PROP = /^(transition|animation)(-|$)/;
|
|
|
12
14
|
const violations = [];
|
|
13
15
|
const report = (file, text, d, value) => violations.push({ rule_id: RULE_ID, file, ...locate(text, d.index), evidence: `${d.prop}: ${value} is hardcoded; use a foundations token (var(--…))` });
|
|
14
16
|
for (const file of scope(readRoots(), readExcludes())) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
const literalHtml = !isUi(file) && isTsSource(file);
|
|
18
|
+
if ((!isUi(file) && !literalHtml) || designOf(file)?.layer === 0) continue;
|
|
19
|
+
const src = masked(file, literalHtml ? { keepLiterals: true } : undefined);
|
|
20
|
+
if (!src) continue;
|
|
21
|
+
for (const d of declarations(file, literalHtml ? src.code : src.text)) {
|
|
18
22
|
if (LENGTH_PROP.test(d.prop)) {
|
|
19
|
-
if (d.numeric) { if (Math.abs(Number(d.value)) > 1) report(file, text, d, d.value); continue; }
|
|
23
|
+
if (d.numeric) { if (Math.abs(Number(d.value)) > 1) report(file, src.text, d, d.value); continue; }
|
|
20
24
|
const px = [...d.value.matchAll(/(-?\d*\.?\d+)px\b/g)].find((m) => Math.abs(Number(m[1])) > 1);
|
|
21
|
-
if (px) report(file, text, d, px[0]);
|
|
25
|
+
if (px) report(file, src.text, d, px[0]);
|
|
22
26
|
} else if (DURATION_PROP.test(d.prop)) {
|
|
23
27
|
const time = [...d.value.matchAll(/(\d*\.?\d+)(ms|s)\b/g)].find((m) => Number(m[1]) > 0);
|
|
24
|
-
if (time) report(file, text, d, time[0]);
|
|
28
|
+
if (time) report(file, src.text, d, time[0]);
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
31
|
}
|
package/integrity.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.98",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
6
6
|
"README.md": "8f0e45595fc10bb6f033b81b67246e57bb3f5bbaf62b35ba950beb5f9f82a938",
|
|
7
7
|
"bunfig.toml": "b9fc65eca9014c5179380259d70e76385a6a80788fa9a2df5fb3eaa5554fd2fe",
|
|
8
8
|
"conventions/_profiles/architecture.yaml": "f1808768b720a4e476047e4a0912002ca673946562cea9aeef5d0c214828d93a",
|
|
9
|
-
"conventions/_profiles/coder.yaml": "
|
|
10
|
-
"conventions/_profiles/design.yaml": "
|
|
9
|
+
"conventions/_profiles/coder.yaml": "53693363c6623b5b12ece1c746c8cf4f052615310fc5683160d8de8be7d8e0c9",
|
|
10
|
+
"conventions/_profiles/design.yaml": "e76c87d2fbf1c1f1ecc9a24247be93b6915062939636678a9983ec23fe5f7af6",
|
|
11
11
|
"conventions/_profiles/docs.yaml": "05485c3b32f1757e77c42e331eec1db651cb52ef57fc4e8607bb2f73b056cbb3",
|
|
12
12
|
"conventions/_profiles/flow.yaml": "ca23ee662bf2fff590fee7a5b06816d720969531510bb35fb4ae3289c2871802",
|
|
13
13
|
"conventions/_profiles/htmx.yaml": "38fc8b3e1a53429e16977ed63e83e751b203deffe9b338ea171050fb64641559",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"conventions/coder.bun/coder.bun.design-dependency-flow.convention.yaml": "8c99a0293f6a3e527c8aa99b2d5634695627c6daf8e37398fa25667a0e47dbff",
|
|
52
52
|
"conventions/coder.bun/coder.bun.design-hierarchy-import.convention.yaml": "59ca34fb9c6ec72b6ad7a099a5a8bceaa79a646226cf05eb0bb5c99854804dd2",
|
|
53
53
|
"conventions/coder.bun/coder.bun.design-orphan-export.convention.yaml": "ca36d04d8cc961e04974f65fd774c2dbfab92fe64846010cdb27fc220fb0d935",
|
|
54
|
-
"conventions/coder.bun/coder.bun.design-primitives.convention.yaml": "
|
|
55
|
-
"conventions/coder.bun/coder.bun.design-token-color.convention.yaml": "
|
|
56
|
-
"conventions/coder.bun/coder.bun.design-token-hardcoded.convention.yaml": "
|
|
54
|
+
"conventions/coder.bun/coder.bun.design-primitives.convention.yaml": "b32a5bbbcc4f515fee2cb5f545e142f30ff610d87eca0d25d224dad100644282",
|
|
55
|
+
"conventions/coder.bun/coder.bun.design-token-color.convention.yaml": "d0deecd6b7cf6b770af4ce6954a8c9e3470e1040dd2143dd648746ad43d875fb",
|
|
56
|
+
"conventions/coder.bun/coder.bun.design-token-hardcoded.convention.yaml": "c46afb4a4c8babcce0a4ffcf361c1e0181343b8c76b73311f094380fd4285a22",
|
|
57
57
|
"conventions/coder.bun/coder.bun.design-tokens-pure.convention.yaml": "8f7e768ac3041dc3d3285a03fdcabd0d376695bcfab0233166e107fb2f1419c8",
|
|
58
58
|
"conventions/coder.bun/coder.bun.design-wagons-import.convention.yaml": "76595759544ce81bf9b0e0ee1d59f7a2f5cc446105697b23a5c545a8939bcb28",
|
|
59
59
|
"conventions/coder.bun/coder.bun.dto-mapper.convention.yaml": "c524b5f37db6ae4f7558fc3ec672aa1c5b764738a0f25a20062d736fed6bbcb3",
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
"detectors/atdd_topology/fixtures/dirty/src/wagons/orders/features/place-order/domain/order.ts": "6fdaad52671ef25b47d93f9b4e97e12c62eb78f5b4cde727435c3ac135a1c0e5",
|
|
192
192
|
"detectors/atdd_topology/fixtures/dirty/tests/wagons/orders/features/place-order/unit/order.test.ts": "f117e6c8b034700e1f00889bea48cb1528f0003d699cfd3e279cd9164b264a38",
|
|
193
193
|
"detectors/atdd_traceability_closure/atdd.implementation.yaml": "82e56cc85539476e42fc8d58e718bc998f1eff6c325a0cd240c0284625834d68",
|
|
194
|
-
"detectors/atdd_traceability_closure/detect.mjs": "
|
|
194
|
+
"detectors/atdd_traceability_closure/detect.mjs": "4fe2f6c73fc5535255f15676fb89f0a887598e3bf80c82f048c8e69432c38342",
|
|
195
195
|
"detectors/atdd_traceability_closure/fixtures/clean/plan/orders/E001.yaml": "0ddad928dde10c2ab690558259a6975125af540a1f30eca9aa0c615bf0ad69fb",
|
|
196
196
|
"detectors/atdd_traceability_closure/fixtures/clean/src/order.ts": "efe6db7e3f723c7b6096abdf58dec9c88b27ec8978d434a10135b3e77bfce321",
|
|
197
197
|
"detectors/atdd_traceability_closure/fixtures/clean/test/order.test.ts": "bee451198d472749616f85c1f1e97c0a06ad28255b9a6a2cc8701bd2e8d56562",
|
|
@@ -231,14 +231,14 @@
|
|
|
231
231
|
"detectors/bun_clean_architecture_detector/fixtures/dirty/features/billing/integration/client.ts": "51ed50a6489f782fa8d5de2c1c3863bd7554a5918d2f6d26c0f65a86228e482c",
|
|
232
232
|
"detectors/bun_clean_architecture_detector/fixtures/dirty/features/billing/presentation/api.ts": "109fdfcff0043c1cfea5e8faef029a2398f82cb852c6ab96bfaa113b05c47ece",
|
|
233
233
|
"detectors/bun_design_system_detector/atdd.implementation.yaml": "3370a481643a736a5309c9ca77f08b89c60a559bbfb898d4c8b64fbe59fc31c6",
|
|
234
|
-
"detectors/bun_design_system_detector/checks/_design.mjs": "
|
|
234
|
+
"detectors/bun_design_system_detector/checks/_design.mjs": "a05659748126f8bddb567ec2cda4afc8f5647e42ca488f5874436af861396842",
|
|
235
235
|
"detectors/bun_design_system_detector/checks/_map.json": "564138c6a771f052023ec93f3c47832fd5e7511c5b13f6a40a09c25ccc3c4fb5",
|
|
236
236
|
"detectors/bun_design_system_detector/checks/design_connected.mjs": "ff7976f22efe5957a3c8b6c29b8c950b5877336ab3870a232b1e42d49d97060c",
|
|
237
237
|
"detectors/bun_design_system_detector/checks/design_dependency_flow.mjs": "159121d5a158b50dcf0c585153adb94c2207e9b07ae05b1eb78cd5ffc9cf7027",
|
|
238
238
|
"detectors/bun_design_system_detector/checks/design_orphan_export.mjs": "655da3f5592f6e8e6719c01532acf2424da6a3fcf255891d7dd52483cc6b17ce",
|
|
239
|
-
"detectors/bun_design_system_detector/checks/design_primitives.mjs": "
|
|
240
|
-
"detectors/bun_design_system_detector/checks/design_token_color.mjs": "
|
|
241
|
-
"detectors/bun_design_system_detector/checks/design_token_hardcoded.mjs": "
|
|
239
|
+
"detectors/bun_design_system_detector/checks/design_primitives.mjs": "bc534e1b6b01c9157f91f9f32bbef1be059fba0f3a6e604b82464ff4c7cc7142",
|
|
240
|
+
"detectors/bun_design_system_detector/checks/design_token_color.mjs": "6624e87b6f496219ed6649e296e1782d74b5837a3437e6539bbefb82b8f295cd",
|
|
241
|
+
"detectors/bun_design_system_detector/checks/design_token_hardcoded.mjs": "1698b5c2ab10d669a3d8bd44190b92c6ebd73367b9f92c2dda73d2a79d85ffd6",
|
|
242
242
|
"detectors/bun_design_system_detector/checks/design_tokens_pure.mjs": "9fd668df42c7a6a7d7949bb5975ba3c6925abcb908d626a573f56800f96e32d9",
|
|
243
243
|
"detectors/bun_design_system_detector/checks/design_wagons_import.mjs": "5cbd9776bcc3defdd4f28cbc228b3164ef1a694c64c86b151a1cd67f9687dd21",
|
|
244
244
|
"detectors/bun_design_system_detector/detect.mjs": "f8eb9cda3e35b8a808614a9655fd80574e506d1a438fcc15e68c0b7fdd833dcc",
|
|
@@ -1035,7 +1035,7 @@
|
|
|
1035
1035
|
"lib/imports.mjs": "3db4979269488fa26f6e7aefded6ece9c646be6587a10e249821a5a208109962",
|
|
1036
1036
|
"lib/interlocking.mjs": "d0803d080ac7d7477c51be70112df1e85213d339fd3c6b71f75f26145fa8accd",
|
|
1037
1037
|
"lib/journey-chain.mjs": "bdafc9cc3da4ade644aebacc6e1015e29285b764465588d764b7c581bade9025",
|
|
1038
|
-
"lib/scan.mjs": "
|
|
1038
|
+
"lib/scan.mjs": "b6973e411ecd14c548a055c871e01e948f002b772042136be8401f38d998e687",
|
|
1039
1039
|
"planner-nodes/ENFORCEMENT_SCOPE.yaml": "d8ec7039c219857f3d77af5d4d84612b533b69c1a5fcef935fbf46470adafed4",
|
|
1040
1040
|
"planner-nodes/nodes/planner.acceptance.abstract-fields-required.convention.yaml": "bd565693f0ae861f0561ad2f7a91a8c55f9a209c3acd59c5750cfcffd96a85d8",
|
|
1041
1041
|
"planner-nodes/nodes/planner.acceptance.authoring-guidelines.convention.yaml": "16348dceed5868041b832bde766e9c8d4a950d4032f1f3066b357644081a3b17",
|
|
@@ -1255,7 +1255,7 @@
|
|
|
1255
1255
|
"planner-schemas/train.schema.json": "d6fbac9067c1bffeb84eb79a71228441055af6e6e7ba141dd4e0bcb079345ec0",
|
|
1256
1256
|
"planner-schemas/wagon.schema.json": "12353e46d2216d2deef754615b629a696bd8a7bb35326379e7ef2b221a5ece3a",
|
|
1257
1257
|
"planner-schemas/wmbt.schema.json": "7a3a5693bfcf01340667c2be4a3c48e74f34b1af7e8dc13372c0de06b7f4fe12",
|
|
1258
|
-
"relationships.yaml": "
|
|
1258
|
+
"relationships.yaml": "fccaf745b383fe527b7db9022233037bdf5b5cc3d495bb40da3cca4746f5e241",
|
|
1259
1259
|
"src/agent.ts": "6d6c6e75908af8fbab13432f0001b0374bb25cf3dee46ba218dfd67e0090cb56",
|
|
1260
1260
|
"src/ci.ts": "b5c57dd87fc780b2b272156b68830d14e695a1a28300dec46a06f71864e0c59e",
|
|
1261
1261
|
"src/cli.ts": "d93c3aebb78e752fd96347a5b177c0ed0697010c6e2533e116de93e772cd65a8",
|
package/lib/scan.mjs
CHANGED
|
@@ -132,6 +132,29 @@ function regexMayStart(text, i) {
|
|
|
132
132
|
return false;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// The index just past the `${` substitution opening at `i` (text[i] === "$", text[i+1] === "{").
|
|
136
|
+
// A `${ … }` substitution is code: it may hold strings, braces and templates of its own
|
|
137
|
+
// (escaped backticks included), so the substitution ends only at the matching `}` outside them.
|
|
138
|
+
function substitutionEnd(text, i) {
|
|
139
|
+
const n = text.length;
|
|
140
|
+
let depth = 1;
|
|
141
|
+
let j = i + 2;
|
|
142
|
+
while (j < n && depth > 0) {
|
|
143
|
+
const d = text[j];
|
|
144
|
+
if (d === "`") { j = templateEnd(text, j); continue; }
|
|
145
|
+
if (d === '"' || d === "'") {
|
|
146
|
+
let k = j + 1;
|
|
147
|
+
while (k < n && text[k] !== d && text[k] !== "\n") k += text[k] === "\\" ? 2 : 1;
|
|
148
|
+
j = k + 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (d === "{") depth++;
|
|
152
|
+
else if (d === "}") depth--;
|
|
153
|
+
j++;
|
|
154
|
+
}
|
|
155
|
+
return j;
|
|
156
|
+
}
|
|
157
|
+
|
|
135
158
|
// The index just past the template literal opening at `start`. A `${ … }` substitution is code: it may hold strings,
|
|
136
159
|
// braces and templates of its own (escaped backticks included), so the template ends only at a backtick outside them.
|
|
137
160
|
function templateEnd(text, start) {
|
|
@@ -141,30 +164,39 @@ function templateEnd(text, start) {
|
|
|
141
164
|
const c = text[j];
|
|
142
165
|
if (c === "\\") { j += 2; continue; }
|
|
143
166
|
if (c === "`") return j + 1;
|
|
144
|
-
if (c === "$" && text[j + 1] === "{") {
|
|
145
|
-
let depth = 1;
|
|
146
|
-
j += 2;
|
|
147
|
-
while (j < n && depth > 0) {
|
|
148
|
-
const d = text[j];
|
|
149
|
-
if (d === "`") { j = templateEnd(text, j); continue; }
|
|
150
|
-
if (d === '"' || d === "'") {
|
|
151
|
-
let k = j + 1;
|
|
152
|
-
while (k < n && text[k] !== d && text[k] !== "\n") k += text[k] === "\\" ? 2 : 1;
|
|
153
|
-
j = k + 1;
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (d === "{") depth++;
|
|
157
|
-
else if (d === "}") depth--;
|
|
158
|
-
j++;
|
|
159
|
-
}
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
167
|
+
if (c === "$" && text[j + 1] === "{") { j = substitutionEnd(text, j); continue; }
|
|
162
168
|
j++;
|
|
163
169
|
}
|
|
164
170
|
return n;
|
|
165
171
|
}
|
|
166
172
|
|
|
167
|
-
|
|
173
|
+
// Inside the template literal spanning [from, to): keep the literal text (already in `out`),
|
|
174
|
+
// blank the `${` / `}` delimiters, and mask the substitution code by the same rule — a string
|
|
175
|
+
// or template of its own inside the substitution is still a literal body, and stays kept.
|
|
176
|
+
function maskTemplateLiterals(text, out, from, to, blank) {
|
|
177
|
+
let j = from;
|
|
178
|
+
while (j < to) {
|
|
179
|
+
if (text[j] === "\\") { j += 2; continue; }
|
|
180
|
+
if (text[j] !== "$" || text[j + 1] !== "{") { j++; continue; }
|
|
181
|
+
const k = Math.min(substitutionEnd(text, j), to); // just past the closing `}`
|
|
182
|
+
const body = j + 2, end = k - 1;
|
|
183
|
+
blank(j, body);
|
|
184
|
+
if (end > body) out.splice(body, end - body, ...maskLiteralsAndComments(text.slice(body, end), KEEP_LITERALS).split(""));
|
|
185
|
+
blank(end, k);
|
|
186
|
+
j = k;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Mask string literals, template literals and comments with spaces of equal
|
|
191
|
+
// length, preserving every line and column offset. A check that greps raw source
|
|
192
|
+
// otherwise fires on the rule's own name inside a comment or a doc string — the
|
|
193
|
+
// classic detector false positive.
|
|
194
|
+
// With `keepLiterals` the payload inverts: literal BODIES stay while code, comments and
|
|
195
|
+
// regex literals are blanked — the mode for checks whose subject is the HTML a `.ts` file
|
|
196
|
+
// renders from string/template/tagged-template literals, so a match can only ever come
|
|
197
|
+
// from markup an author wrote in a literal.
|
|
198
|
+
const KEEP_LITERALS = { keepLiterals: true };
|
|
199
|
+
export function maskLiteralsAndComments(text, { keepLiterals = false } = {}) {
|
|
168
200
|
const out = text.split("");
|
|
169
201
|
let i = 0;
|
|
170
202
|
const n = text.length;
|
|
@@ -199,7 +231,8 @@ export function maskLiteralsAndComments(text) {
|
|
|
199
231
|
i = j + 1;
|
|
200
232
|
} else if (ch === "`") {
|
|
201
233
|
const j = templateEnd(text, i);
|
|
202
|
-
|
|
234
|
+
if (keepLiterals) maskTemplateLiterals(text, out, i + 1, j - 1 >= i + 1 ? j - 1 : i + 1, blank);
|
|
235
|
+
else blank(i + 1, j - 1 >= i + 1 ? j - 1 : i + 1);
|
|
203
236
|
i = j;
|
|
204
237
|
} else if (ch === '"' || ch === "'") {
|
|
205
238
|
let j = i + 1;
|
|
@@ -208,9 +241,10 @@ export function maskLiteralsAndComments(text) {
|
|
|
208
241
|
if (text[j] === ch) { j++; break; }
|
|
209
242
|
j++;
|
|
210
243
|
}
|
|
211
|
-
blank(i + 1, j - 1 >= i + 1 ? j - 1 : i + 1);
|
|
244
|
+
if (!keepLiterals) blank(i + 1, j - 1 >= i + 1 ? j - 1 : i + 1);
|
|
212
245
|
i = j;
|
|
213
246
|
} else {
|
|
247
|
+
if (keepLiterals) blank(i, i + 1);
|
|
214
248
|
i++;
|
|
215
249
|
}
|
|
216
250
|
}
|
package/package.json
CHANGED
package/relationships.yaml
CHANGED
|
@@ -4413,7 +4413,7 @@ edges:
|
|
|
4413
4413
|
constraint: mandatory
|
|
4414
4414
|
control: internal
|
|
4415
4415
|
foundation: finish_to_start
|
|
4416
|
-
reason: "Bun realization of the core obligation coder.design.token-color, checked over .tsx, .jsx, .html and .
|
|
4416
|
+
reason: "Bun realization of the core obligation coder.design.token-color, checked over .tsx, .jsx, .html, .css and HTML authored in .ts/.mts literals"
|
|
4417
4417
|
source_ref: coder.bun.design-token-color
|
|
4418
4418
|
strength: important
|
|
4419
4419
|
target_ref: coder.design.token-color
|
|
@@ -4423,7 +4423,7 @@ edges:
|
|
|
4423
4423
|
constraint: mandatory
|
|
4424
4424
|
control: internal
|
|
4425
4425
|
foundation: finish_to_start
|
|
4426
|
-
reason: "Bun realization of the core obligation coder.design.token-hardcoded, checked over .tsx, .jsx, .html and .
|
|
4426
|
+
reason: "Bun realization of the core obligation coder.design.token-hardcoded, checked over .tsx, .jsx, .html, .css and HTML authored in .ts/.mts literals"
|
|
4427
4427
|
source_ref: coder.bun.design-token-hardcoded
|
|
4428
4428
|
strength: important
|
|
4429
4429
|
target_ref: coder.design.token-hardcoded
|
|
@@ -4433,7 +4433,7 @@ edges:
|
|
|
4433
4433
|
constraint: mandatory
|
|
4434
4434
|
control: internal
|
|
4435
4435
|
foundation: finish_to_start
|
|
4436
|
-
reason: "Bun realization of the core obligation coder.design.primitives, checked over .tsx, .jsx, .html and .
|
|
4436
|
+
reason: "Bun realization of the core obligation coder.design.primitives, checked over .tsx, .jsx, .html, .css and HTML authored in .ts/.mts literals"
|
|
4437
4437
|
source_ref: coder.bun.design-primitives
|
|
4438
4438
|
strength: important
|
|
4439
4439
|
target_ref: coder.design.primitives
|