@mandujs/core 0.44.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/client/router.ts +12 -2
- package/src/client/spa-nav-helper.ts +1 -1
- package/src/config/validate.ts +36 -0
- package/src/design/__tests__/parser.test.ts +195 -0
- package/src/design/index.ts +49 -0
- package/src/design/parser.ts +555 -0
- package/src/design/scaffold.ts +147 -0
- package/src/design/types.ts +210 -0
- package/src/guard/__tests__/design-inline-class.test.ts +219 -0
- package/src/guard/check.ts +15 -0
- package/src/guard/design-inline-class.ts +353 -0
- package/src/guard/rules.ts +9 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DESIGN.md type surface.
|
|
3
|
+
*
|
|
4
|
+
* The 9-section schema is borrowed verbatim from Google Stitch's
|
|
5
|
+
* DESIGN.md convention (also used by VoltAgent's awesome-design-md
|
|
6
|
+
* catalog of 69 brand spec files). Mandu adopts it as a first-class
|
|
7
|
+
* format rather than inventing its own — see Issue #245 v2 plan.
|
|
8
|
+
*
|
|
9
|
+
* Parsing is **permissive**: a DESIGN.md may include any subset of
|
|
10
|
+
* sections, in any order, with arbitrary free-form content between
|
|
11
|
+
* structured tokens. The parser populates the sections it understands
|
|
12
|
+
* and stores the rest as `rawBody` so round-trips don't lose user
|
|
13
|
+
* prose. Tools (Guard, MCP, token bridge) consume only the structured
|
|
14
|
+
* fields they care about.
|
|
15
|
+
*
|
|
16
|
+
* @module core/design/types
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const DESIGN_SECTION_IDS = [
|
|
20
|
+
"theme",
|
|
21
|
+
"color-palette",
|
|
22
|
+
"typography",
|
|
23
|
+
"components",
|
|
24
|
+
"layout",
|
|
25
|
+
"shadows",
|
|
26
|
+
"dos-donts",
|
|
27
|
+
"responsive",
|
|
28
|
+
"agent-prompts",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export type DesignSectionId = (typeof DESIGN_SECTION_IDS)[number];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Color palette entry. `hex` may be missing when a row only carries a
|
|
35
|
+
* semantic role (e.g. "primary — brand color, see Stripe docs"); the
|
|
36
|
+
* Guard rule and the token bridge skip rows without a parseable value.
|
|
37
|
+
*/
|
|
38
|
+
export interface ColorToken {
|
|
39
|
+
name: string;
|
|
40
|
+
/** `#rrggbb`, `#rrggbbaa`, `rgb(...)`, `rgba(...)`, `oklch(...)`, etc. */
|
|
41
|
+
value?: string;
|
|
42
|
+
/** Functional role — "primary", "surface", "text-muted", … (free-form). */
|
|
43
|
+
role?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TypographyToken {
|
|
47
|
+
name: string;
|
|
48
|
+
fontFamily?: string;
|
|
49
|
+
weight?: string;
|
|
50
|
+
size?: string;
|
|
51
|
+
lineHeight?: string;
|
|
52
|
+
/** Free-form usage hint ("h1 hero", "body small", …). */
|
|
53
|
+
usage?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ComponentToken {
|
|
57
|
+
name: string;
|
|
58
|
+
/** Variants surfaced as a `name → list` map. Empty when not declared. */
|
|
59
|
+
variants: Record<string, string[]>;
|
|
60
|
+
/** Free-form description / props notes from markdown body. */
|
|
61
|
+
notes?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SpacingToken {
|
|
65
|
+
/** "xs", "sm", "md", "lg", "xl" — caller-defined. */
|
|
66
|
+
name: string;
|
|
67
|
+
value?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ShadowToken {
|
|
71
|
+
/** "card", "popover", "modal" — caller-defined elevation name. */
|
|
72
|
+
name: string;
|
|
73
|
+
value?: string;
|
|
74
|
+
/** Free-form usage hint. */
|
|
75
|
+
usage?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface DoDontRule {
|
|
79
|
+
/** "do" or "dont" — Guard rule consumes "dont" as `forbidInlineClasses` candidates. */
|
|
80
|
+
kind: "do" | "dont";
|
|
81
|
+
/** Plain-text rule body. */
|
|
82
|
+
text: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ResponsiveBreakpoint {
|
|
86
|
+
name: string;
|
|
87
|
+
value?: string;
|
|
88
|
+
/** Free-form notes (touch target, scaling strategy, …). */
|
|
89
|
+
notes?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface AgentPrompt {
|
|
93
|
+
/** Heading or first sentence used as a label. */
|
|
94
|
+
title: string;
|
|
95
|
+
/** Body of the prompt — passed through verbatim. */
|
|
96
|
+
body: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Per-section payload. Every section carries:
|
|
101
|
+
* - `present`: whether the heading was found at all
|
|
102
|
+
* - `rawBody`: original markdown body for round-trip / fallback
|
|
103
|
+
* - structured fields where applicable (`tokens`, `rules`, …)
|
|
104
|
+
*
|
|
105
|
+
* Tools that only care about presence (e.g. `mandu design validate`)
|
|
106
|
+
* read `present`; structured consumers (Guard, token bridge) read the
|
|
107
|
+
* typed fields and treat empties as "not declared".
|
|
108
|
+
*/
|
|
109
|
+
export interface DesignSection {
|
|
110
|
+
id: DesignSectionId;
|
|
111
|
+
present: boolean;
|
|
112
|
+
/** Heading line as written by the user (e.g. "## Color Palette"). */
|
|
113
|
+
headingText?: string;
|
|
114
|
+
/** Raw markdown body between this heading and the next H2. */
|
|
115
|
+
rawBody: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ThemeSection extends DesignSection {
|
|
119
|
+
id: "theme";
|
|
120
|
+
/** First non-empty paragraph — quick "vibe" answer. */
|
|
121
|
+
summary?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ColorPaletteSection extends DesignSection {
|
|
125
|
+
id: "color-palette";
|
|
126
|
+
tokens: ColorToken[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface TypographySection extends DesignSection {
|
|
130
|
+
id: "typography";
|
|
131
|
+
tokens: TypographyToken[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface ComponentsSection extends DesignSection {
|
|
135
|
+
id: "components";
|
|
136
|
+
tokens: ComponentToken[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface LayoutSection extends DesignSection {
|
|
140
|
+
id: "layout";
|
|
141
|
+
tokens: SpacingToken[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ShadowsSection extends DesignSection {
|
|
145
|
+
id: "shadows";
|
|
146
|
+
tokens: ShadowToken[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface DosDontsSection extends DesignSection {
|
|
150
|
+
id: "dos-donts";
|
|
151
|
+
rules: DoDontRule[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface ResponsiveSection extends DesignSection {
|
|
155
|
+
id: "responsive";
|
|
156
|
+
breakpoints: ResponsiveBreakpoint[];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface AgentPromptsSection extends DesignSection {
|
|
160
|
+
id: "agent-prompts";
|
|
161
|
+
prompts: AgentPrompt[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type AnyDesignSection =
|
|
165
|
+
| ThemeSection
|
|
166
|
+
| ColorPaletteSection
|
|
167
|
+
| TypographySection
|
|
168
|
+
| ComponentsSection
|
|
169
|
+
| LayoutSection
|
|
170
|
+
| ShadowsSection
|
|
171
|
+
| DosDontsSection
|
|
172
|
+
| ResponsiveSection
|
|
173
|
+
| AgentPromptsSection;
|
|
174
|
+
|
|
175
|
+
export interface DesignSpec {
|
|
176
|
+
/** Original source string (for round-trip). */
|
|
177
|
+
source: string;
|
|
178
|
+
/**
|
|
179
|
+
* Sections in canonical order. `present: false` when the user omitted
|
|
180
|
+
* the section — Mandu still reserves the slot so consumers can index
|
|
181
|
+
* by id without `find()`.
|
|
182
|
+
*/
|
|
183
|
+
sections: {
|
|
184
|
+
theme: ThemeSection;
|
|
185
|
+
"color-palette": ColorPaletteSection;
|
|
186
|
+
typography: TypographySection;
|
|
187
|
+
components: ComponentsSection;
|
|
188
|
+
layout: LayoutSection;
|
|
189
|
+
shadows: ShadowsSection;
|
|
190
|
+
"dos-donts": DosDontsSection;
|
|
191
|
+
responsive: ResponsiveSection;
|
|
192
|
+
"agent-prompts": AgentPromptsSection;
|
|
193
|
+
};
|
|
194
|
+
/** Extra H2 sections the user wrote that don't map to the 9-section spec. */
|
|
195
|
+
extraSections: Array<{ heading: string; body: string }>;
|
|
196
|
+
/** Optional H1 title at the top of the file. */
|
|
197
|
+
title?: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface ValidationIssue {
|
|
201
|
+
/** "missing" — section absent. "empty" — section present but no structured tokens. "malformed" — parse error inside a section. */
|
|
202
|
+
kind: "missing" | "empty" | "malformed";
|
|
203
|
+
section: DesignSectionId;
|
|
204
|
+
message: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface ValidationResult {
|
|
208
|
+
ok: boolean;
|
|
209
|
+
issues: ValidationIssue[];
|
|
210
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard `DESIGN_INLINE_CLASS` rule tests (Issue #245 M2).
|
|
3
|
+
*
|
|
4
|
+
* Cover the three forbid-list sources (explicit / DESIGN.md auto /
|
|
5
|
+
* combined), the exclude paths (canonical component dirs), and the
|
|
6
|
+
* regex matcher's variant-prefix handling.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from "bun:test";
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
import { checkDesignInlineClasses } from "../design-inline-class";
|
|
14
|
+
|
|
15
|
+
async function makeRoot(prefix: string): Promise<string> {
|
|
16
|
+
return fs.mkdtemp(path.join(os.tmpdir(), `mandu-design-guard-${prefix}-`));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function writeFile(root: string, rel: string, content: string): Promise<void> {
|
|
20
|
+
const full = path.join(root, rel);
|
|
21
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
22
|
+
await fs.writeFile(full, content);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("checkDesignInlineClasses", () => {
|
|
26
|
+
it("returns no violations when guard.design is undefined", async () => {
|
|
27
|
+
const root = await makeRoot("undef");
|
|
28
|
+
await writeFile(
|
|
29
|
+
root,
|
|
30
|
+
"src/client/page.tsx",
|
|
31
|
+
`export default () => <div className="btn-hard">x</div>;\n`,
|
|
32
|
+
);
|
|
33
|
+
const out = await checkDesignInlineClasses(root, undefined);
|
|
34
|
+
expect(out).toHaveLength(0);
|
|
35
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("flags an inline forbidden class with line number", async () => {
|
|
39
|
+
const root = await makeRoot("flag");
|
|
40
|
+
await writeFile(
|
|
41
|
+
root,
|
|
42
|
+
"src/client/page.tsx",
|
|
43
|
+
`import * as React from 'react';
|
|
44
|
+
|
|
45
|
+
export default () => (
|
|
46
|
+
<div className="px-4 py-2 btn-hard rounded-md">x</div>
|
|
47
|
+
);
|
|
48
|
+
`,
|
|
49
|
+
);
|
|
50
|
+
const out = await checkDesignInlineClasses(root, {
|
|
51
|
+
forbidInlineClasses: ["btn-hard"],
|
|
52
|
+
});
|
|
53
|
+
expect(out).toHaveLength(1);
|
|
54
|
+
expect(out[0]?.ruleId).toBe("DESIGN_INLINE_CLASS");
|
|
55
|
+
expect(out[0]?.file.replace(/\\/g, "/")).toBe("src/client/page.tsx");
|
|
56
|
+
expect(out[0]?.line).toBe(4);
|
|
57
|
+
expect(out[0]?.message).toContain("btn-hard");
|
|
58
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("surfaces the replacement component in the message + suggestion", async () => {
|
|
62
|
+
const root = await makeRoot("hint");
|
|
63
|
+
await writeFile(
|
|
64
|
+
root,
|
|
65
|
+
"src/client/page.tsx",
|
|
66
|
+
`<div className="btn-hard">x</div>`,
|
|
67
|
+
);
|
|
68
|
+
const out = await checkDesignInlineClasses(root, {
|
|
69
|
+
forbidInlineClasses: ["btn-hard"],
|
|
70
|
+
requireComponent: { "btn-hard": "@/client/shared/ui#MButton" },
|
|
71
|
+
});
|
|
72
|
+
expect(out[0]?.message).toContain("@/client/shared/ui#MButton");
|
|
73
|
+
expect(out[0]?.suggestion).toContain("@/client/shared/ui#MButton");
|
|
74
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("excludes the canonical component dirs by default", async () => {
|
|
78
|
+
const root = await makeRoot("exclude-default");
|
|
79
|
+
// The component definition itself uses btn-hard — that's where it
|
|
80
|
+
// legitimately lives. Default exclude must skip this file.
|
|
81
|
+
await writeFile(
|
|
82
|
+
root,
|
|
83
|
+
"src/client/shared/ui/m-button.tsx",
|
|
84
|
+
`<button className="btn-hard">x</button>`,
|
|
85
|
+
);
|
|
86
|
+
// A page that imports it must still be flagged.
|
|
87
|
+
await writeFile(
|
|
88
|
+
root,
|
|
89
|
+
"src/client/pages/home.tsx",
|
|
90
|
+
`<div className="btn-hard">x</div>`,
|
|
91
|
+
);
|
|
92
|
+
const out = await checkDesignInlineClasses(root, {
|
|
93
|
+
forbidInlineClasses: ["btn-hard"],
|
|
94
|
+
});
|
|
95
|
+
const files = out.map((v) => v.file.replace(/\\/g, "/"));
|
|
96
|
+
expect(files).toContain("src/client/pages/home.tsx");
|
|
97
|
+
expect(files).not.toContain("src/client/shared/ui/m-button.tsx");
|
|
98
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("strips Tailwind variant prefixes (hover:btn-hard matches btn-hard)", async () => {
|
|
102
|
+
const root = await makeRoot("variant");
|
|
103
|
+
await writeFile(
|
|
104
|
+
root,
|
|
105
|
+
"src/client/page.tsx",
|
|
106
|
+
`<div className="hover:btn-hard focus:btn-hard">x</div>`,
|
|
107
|
+
);
|
|
108
|
+
const out = await checkDesignInlineClasses(root, {
|
|
109
|
+
forbidInlineClasses: ["btn-hard"],
|
|
110
|
+
});
|
|
111
|
+
expect(out.length).toBeGreaterThanOrEqual(1);
|
|
112
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("auto-extracts forbid tokens from DESIGN.md §7 Don't section", async () => {
|
|
116
|
+
const root = await makeRoot("auto-design");
|
|
117
|
+
await writeFile(
|
|
118
|
+
root,
|
|
119
|
+
"DESIGN.md",
|
|
120
|
+
`# X
|
|
121
|
+
|
|
122
|
+
## Do's & Don'ts
|
|
123
|
+
|
|
124
|
+
### Do
|
|
125
|
+
- Use the \`MButton\` component.
|
|
126
|
+
|
|
127
|
+
### Don't
|
|
128
|
+
- Inline \`btn-hard\` directly in pages.
|
|
129
|
+
- Don't use \`shadow-hard\` outside ui/.
|
|
130
|
+
`,
|
|
131
|
+
);
|
|
132
|
+
await writeFile(
|
|
133
|
+
root,
|
|
134
|
+
"src/client/page.tsx",
|
|
135
|
+
`<div className="btn-hard shadow-hard">x</div>`,
|
|
136
|
+
);
|
|
137
|
+
const out = await checkDesignInlineClasses(root, {
|
|
138
|
+
autoFromDesignMd: true,
|
|
139
|
+
});
|
|
140
|
+
const tokens = out.map((v) => v.message.match(/"([\w-]+)"/)?.[1]);
|
|
141
|
+
expect(tokens).toContain("btn-hard");
|
|
142
|
+
expect(tokens).toContain("shadow-hard");
|
|
143
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("merges explicit forbid + DESIGN.md auto", async () => {
|
|
147
|
+
const root = await makeRoot("merge");
|
|
148
|
+
await writeFile(
|
|
149
|
+
root,
|
|
150
|
+
"DESIGN.md",
|
|
151
|
+
`## Do's & Don'ts
|
|
152
|
+
|
|
153
|
+
### Don't
|
|
154
|
+
- Avoid \`shadow-hard\`.
|
|
155
|
+
`,
|
|
156
|
+
);
|
|
157
|
+
await writeFile(
|
|
158
|
+
root,
|
|
159
|
+
"src/client/page.tsx",
|
|
160
|
+
`<div className="btn-hard shadow-hard">x</div>`,
|
|
161
|
+
);
|
|
162
|
+
const out = await checkDesignInlineClasses(root, {
|
|
163
|
+
forbidInlineClasses: ["btn-hard"],
|
|
164
|
+
autoFromDesignMd: true,
|
|
165
|
+
});
|
|
166
|
+
const tokens = out.map((v) => v.message.match(/"([\w-]+)"/)?.[1]);
|
|
167
|
+
expect(tokens).toContain("btn-hard");
|
|
168
|
+
expect(tokens).toContain("shadow-hard");
|
|
169
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("scans both src/ and app/ when present", async () => {
|
|
173
|
+
const root = await makeRoot("dual-roots");
|
|
174
|
+
await writeFile(root, "src/x.tsx", `<div className="btn-hard">x</div>`);
|
|
175
|
+
await writeFile(root, "app/y.tsx", `<div className="btn-hard">y</div>`);
|
|
176
|
+
const out = await checkDesignInlineClasses(root, {
|
|
177
|
+
forbidInlineClasses: ["btn-hard"],
|
|
178
|
+
});
|
|
179
|
+
const files = out.map((v) => v.file.replace(/\\/g, "/")).sort();
|
|
180
|
+
expect(files).toEqual(["app/y.tsx", "src/x.tsx"]);
|
|
181
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("respects custom exclude patterns", async () => {
|
|
185
|
+
const root = await makeRoot("custom-exclude");
|
|
186
|
+
await writeFile(root, "src/legacy/old-page.tsx", `<div className="btn-hard">x</div>`);
|
|
187
|
+
const out = await checkDesignInlineClasses(root, {
|
|
188
|
+
forbidInlineClasses: ["btn-hard"],
|
|
189
|
+
exclude: ["src/legacy/**"],
|
|
190
|
+
});
|
|
191
|
+
expect(out).toHaveLength(0);
|
|
192
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("honours severity setting on emitted violations", async () => {
|
|
196
|
+
const root = await makeRoot("severity");
|
|
197
|
+
await writeFile(root, "src/x.tsx", `<div className="btn-hard">x</div>`);
|
|
198
|
+
const errors = await checkDesignInlineClasses(root, {
|
|
199
|
+
forbidInlineClasses: ["btn-hard"],
|
|
200
|
+
severity: "error",
|
|
201
|
+
});
|
|
202
|
+
expect(errors[0]?.severity).toBe("error");
|
|
203
|
+
const warnings = await checkDesignInlineClasses(root, {
|
|
204
|
+
forbidInlineClasses: ["btn-hard"],
|
|
205
|
+
severity: "warning",
|
|
206
|
+
});
|
|
207
|
+
expect(warnings[0]?.severity).toBe("warning");
|
|
208
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("returns nothing when forbid list resolves to empty", async () => {
|
|
212
|
+
const root = await makeRoot("empty-forbid");
|
|
213
|
+
await writeFile(root, "src/x.tsx", `<div className="btn-hard">x</div>`);
|
|
214
|
+
// No explicit list, autoFromDesignMd false → nothing to enforce.
|
|
215
|
+
const out = await checkDesignInlineClasses(root, {});
|
|
216
|
+
expect(out).toHaveLength(0);
|
|
217
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
218
|
+
});
|
|
219
|
+
});
|
package/src/guard/check.ts
CHANGED
|
@@ -453,6 +453,21 @@ export async function runGuardCheck(
|
|
|
453
453
|
violations.push(...slotContentViolations);
|
|
454
454
|
violations.push(...contractViolations);
|
|
455
455
|
|
|
456
|
+
// ============================================
|
|
457
|
+
// Issue #245 — DESIGN.md-driven inline-class enforcement.
|
|
458
|
+
// Skipped silently when guard.design is undefined or has nothing
|
|
459
|
+
// to enforce (no explicit forbid list + autoFromDesignMd off).
|
|
460
|
+
// ============================================
|
|
461
|
+
const designConfig = (config.guard as { design?: unknown } | undefined)?.design;
|
|
462
|
+
if (designConfig && typeof designConfig === "object") {
|
|
463
|
+
const { checkDesignInlineClasses } = await import("./design-inline-class");
|
|
464
|
+
const designViolations = await checkDesignInlineClasses(
|
|
465
|
+
rootDir,
|
|
466
|
+
designConfig as Parameters<typeof checkDesignInlineClasses>[1],
|
|
467
|
+
);
|
|
468
|
+
violations.push(...designViolations);
|
|
469
|
+
}
|
|
470
|
+
|
|
456
471
|
// ============================================
|
|
457
472
|
// Phase 18.ν — Consumer-defined custom rules
|
|
458
473
|
// ============================================
|