@maz-ui/mcp 5.1.1 → 5.2.0-beta.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/README.md +9 -2
- package/dist/chunks/files.mjs +70 -0
- package/dist/chunks/index.mjs +28535 -0
- package/dist/chunks/prompt.mjs +280 -0
- package/dist/chunks/theme.mjs +14789 -0
- package/dist/cli.mjs +9 -11
- package/dist/index.d.mts +90 -5
- package/dist/index.d.ts +90 -5
- package/dist/index.mjs +13 -11
- package/dist/shared/mcp.Ccxm-Aoc.mjs +3312 -0
- package/dist/shared/mcp.D3_Fk0U3.mjs +34563 -0
- package/docs/agents/agents-md.md +32 -17
- package/docs/agents/claude-rule.md +32 -17
- package/docs/components/maz-alert.md +1 -1
- package/docs/components/maz-bottom-sheet.md +1 -1
- package/docs/components/maz-btn.md +3 -3
- package/docs/components/maz-container.md +2 -2
- package/docs/components/maz-dropdown.md +1 -1
- package/docs/components/maz-dropzone.md +1 -1
- package/docs/components/maz-icon.md +2 -2
- package/docs/components/maz-input.md +2 -2
- package/docs/components/maz-link.md +2 -2
- package/docs/components/maz-select-country.md +1 -1
- package/docs/components/maz-sidebar-menu-button.md +1 -1
- package/docs/components/maz-sidebar-menu-sub.md +1 -1
- package/docs/components/maz-sidebar.md +2 -2
- package/docs/ecosystem/icons/index.md +7 -4
- package/docs/ecosystem/mcp.md +47 -2
- package/docs/ecosystem/node/setup-agents.md +76 -3
- package/docs/guide/ai-agents.md +51 -17
- package/docs/manifest.json +1 -1
- package/docs/rules.md +32 -17
- package/package.json +28 -12
- package/dist/shared/mcp.cBFLVChV.mjs +0 -3002
|
@@ -1,3002 +0,0 @@
|
|
|
1
|
-
import { Buffer } from 'node:buffer';
|
|
2
|
-
import { join, dirname, resolve, isAbsolute, relative } from 'node:path';
|
|
3
|
-
import { parseArgs } from 'node:util';
|
|
4
|
-
import { findProjectPackage, readPackageVersion, AGENT_TARGETS, detectAgentTargets, setupAgents } from '@maz-ui/node';
|
|
5
|
-
import { getErrorMessage } from '@maz-ui/utils/helpers/getErrorMessage';
|
|
6
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { z } from 'zod';
|
|
9
|
-
import MiniSearch from 'minisearch';
|
|
10
|
-
import { NodeTypes, parse } from '@vue/compiler-dom';
|
|
11
|
-
import { glob, globSync } from 'tinyglobby';
|
|
12
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
|
-
import { execFileSync } from 'node:child_process';
|
|
15
|
-
import { parse as parse$1 } from '@babel/parser';
|
|
16
|
-
|
|
17
|
-
function readOwnVersion(from = dirname(fileURLToPath(import.meta.url))) {
|
|
18
|
-
let directory = from;
|
|
19
|
-
for (; ; ) {
|
|
20
|
-
const file = join(directory, "package.json");
|
|
21
|
-
if (existsSync(file)) {
|
|
22
|
-
const { name, version } = JSON.parse(readFileSync(file, "utf8"));
|
|
23
|
-
if (name === "@maz-ui/mcp" && version)
|
|
24
|
-
return version;
|
|
25
|
-
}
|
|
26
|
-
const parent = dirname(directory);
|
|
27
|
-
if (parent === directory)
|
|
28
|
-
return "0.0.0";
|
|
29
|
-
directory = parent;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const SUPPORTED_SCHEMA_VERSION = 1;
|
|
34
|
-
const docCategories = ["guide", "component", "composable", "directive", "plugin", "ecosystem", "util", "node", "reference"];
|
|
35
|
-
const namedTokenSchema = z.object({ name: z.string(), value: z.string() });
|
|
36
|
-
const docEntrySchema = z.object({
|
|
37
|
-
id: z.string(),
|
|
38
|
-
category: z.enum(docCategories),
|
|
39
|
-
slug: z.string(),
|
|
40
|
-
title: z.string(),
|
|
41
|
-
description: z.string(),
|
|
42
|
-
package: z.string(),
|
|
43
|
-
file: z.string(),
|
|
44
|
-
url: z.string().optional(),
|
|
45
|
-
symbols: z.array(z.string()),
|
|
46
|
-
imports: z.array(z.string()),
|
|
47
|
-
aliases: z.array(z.string()),
|
|
48
|
-
headings: z.array(z.string()),
|
|
49
|
-
related: z.array(z.string()),
|
|
50
|
-
chars: z.number()
|
|
51
|
-
});
|
|
52
|
-
const propApiSchema = z.object({
|
|
53
|
-
name: z.string(),
|
|
54
|
-
type: z.string(),
|
|
55
|
-
description: z.string().optional(),
|
|
56
|
-
required: z.boolean(),
|
|
57
|
-
default: z.string().optional(),
|
|
58
|
-
values: z.array(z.string()).optional(),
|
|
59
|
-
strictValues: z.boolean(),
|
|
60
|
-
model: z.string().optional()
|
|
61
|
-
});
|
|
62
|
-
const componentApiSchema = z.object({
|
|
63
|
-
name: z.string(),
|
|
64
|
-
docId: z.string(),
|
|
65
|
-
description: z.string().optional(),
|
|
66
|
-
import: z.string(),
|
|
67
|
-
parent: z.string().optional(),
|
|
68
|
-
props: z.array(propApiSchema),
|
|
69
|
-
events: z.array(z.object({ name: z.string(), description: z.string().optional(), payload: z.string().optional() })),
|
|
70
|
-
slots: z.array(z.object({
|
|
71
|
-
name: z.string(),
|
|
72
|
-
description: z.string().optional(),
|
|
73
|
-
bindings: z.array(z.object({ name: z.string(), type: z.string().optional(), description: z.string().optional() }))
|
|
74
|
-
})),
|
|
75
|
-
expose: z.array(z.object({ name: z.string(), description: z.string().optional() }))
|
|
76
|
-
});
|
|
77
|
-
const tailwindSchema = z.object({
|
|
78
|
-
entries: z.array(z.object({ path: z.string(), description: z.string() })),
|
|
79
|
-
colors: z.array(z.object({
|
|
80
|
-
name: z.string(),
|
|
81
|
-
variable: z.string(),
|
|
82
|
-
foreground: z.boolean(),
|
|
83
|
-
scale: z.array(z.string()),
|
|
84
|
-
usage: z.string()
|
|
85
|
-
})),
|
|
86
|
-
radius: z.array(namedTokenSchema),
|
|
87
|
-
shadows: z.array(namedTokenSchema),
|
|
88
|
-
breakpoints: z.array(namedTokenSchema),
|
|
89
|
-
zIndex: z.array(namedTokenSchema),
|
|
90
|
-
fonts: z.array(namedTokenSchema),
|
|
91
|
-
durations: z.array(namedTokenSchema),
|
|
92
|
-
easings: z.array(namedTokenSchema),
|
|
93
|
-
spacing: z.array(namedTokenSchema),
|
|
94
|
-
utilities: z.array(z.object({ name: z.string(), css: z.string() })),
|
|
95
|
-
variants: z.array(namedTokenSchema),
|
|
96
|
-
containerVariables: z.array(namedTokenSchema),
|
|
97
|
-
transitions: z.array(z.string())
|
|
98
|
-
});
|
|
99
|
-
const manifestSchema = z.object({
|
|
100
|
-
schemaVersion: z.literal(SUPPORTED_SCHEMA_VERSION),
|
|
101
|
-
name: z.literal("maz-ui"),
|
|
102
|
-
version: z.string(),
|
|
103
|
-
homepage: z.string(),
|
|
104
|
-
packages: z.array(z.object({ name: z.string(), description: z.string(), docId: z.string() })),
|
|
105
|
-
docs: z.array(docEntrySchema),
|
|
106
|
-
components: z.array(componentApiSchema),
|
|
107
|
-
icons: z.object({ package: z.string(), names: z.array(z.string()) }),
|
|
108
|
-
tailwind: tailwindSchema
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
class Knowledge {
|
|
112
|
-
/**
|
|
113
|
-
* @param manifest manifest of the docs bundle
|
|
114
|
-
* @param source where the bundle was found
|
|
115
|
-
* @param documentedVersion version of the documented maz-ui, read from a package.json at runtime when possible: the
|
|
116
|
-
* docs are generated before the release bumps the version, so `manifest.version` can be the previous one
|
|
117
|
-
*/
|
|
118
|
-
constructor(manifest, source, documentedVersion = manifest.version) {
|
|
119
|
-
this.manifest = manifest;
|
|
120
|
-
this.source = source;
|
|
121
|
-
this.documentedVersion = documentedVersion;
|
|
122
|
-
this.docsById = new Map(manifest.docs.map((doc) => [doc.id, doc]));
|
|
123
|
-
this.componentsByName = new Map(manifest.components.map((component) => [component.name, component]));
|
|
124
|
-
this.iconNames = new Set(manifest.icons.names);
|
|
125
|
-
}
|
|
126
|
-
docsById;
|
|
127
|
-
componentsByName;
|
|
128
|
-
iconNames;
|
|
129
|
-
cache = /* @__PURE__ */ new Map();
|
|
130
|
-
get version() {
|
|
131
|
-
return this.documentedVersion;
|
|
132
|
-
}
|
|
133
|
-
get docs() {
|
|
134
|
-
return this.manifest.docs;
|
|
135
|
-
}
|
|
136
|
-
readDoc(id) {
|
|
137
|
-
const cached = this.cache.get(id);
|
|
138
|
-
if (cached !== void 0)
|
|
139
|
-
return cached;
|
|
140
|
-
const doc = this.docsById.get(id);
|
|
141
|
-
if (!doc)
|
|
142
|
-
throw new Error(`Unknown doc "${id}"`);
|
|
143
|
-
const content = readFileSync(join(this.source.directory, doc.file), "utf8");
|
|
144
|
-
this.cache.set(id, content);
|
|
145
|
-
return content;
|
|
146
|
-
}
|
|
147
|
-
readFile(relativePath) {
|
|
148
|
-
const file = join(this.source.directory, relativePath);
|
|
149
|
-
return existsSync(file) ? readFileSync(file, "utf8") : void 0;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
function defaultBundledDir() {
|
|
153
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
154
|
-
const candidates = [resolve(here, "../docs"), resolve(here, "../../docs")];
|
|
155
|
-
return candidates.find((candidate) => existsSync(join(candidate, "manifest.json"))) ?? candidates[0];
|
|
156
|
-
}
|
|
157
|
-
function readManifest(directory) {
|
|
158
|
-
const file = join(directory, "manifest.json");
|
|
159
|
-
if (!existsSync(file))
|
|
160
|
-
return void 0;
|
|
161
|
-
const parsed = manifestSchema.safeParse(JSON.parse(readFileSync(file, "utf8")));
|
|
162
|
-
return parsed.success ? parsed.data : void 0;
|
|
163
|
-
}
|
|
164
|
-
function findProjectMazUi(cwd) {
|
|
165
|
-
const directory = findProjectPackage(cwd, "maz-ui");
|
|
166
|
-
return directory ? { directory, version: readPackageVersion(directory) ?? "0.0.0" } : void 0;
|
|
167
|
-
}
|
|
168
|
-
function loadKnowledge(options = {}) {
|
|
169
|
-
const explicit = options.docsDir ?? process.env.MAZ_UI_DOCS_DIR;
|
|
170
|
-
if (explicit) {
|
|
171
|
-
const manifest2 = readManifest(explicit);
|
|
172
|
-
if (!manifest2)
|
|
173
|
-
throw new Error(`No valid maz-ui docs bundle (manifest.json) in ${explicit}`);
|
|
174
|
-
return new Knowledge(manifest2, { kind: "custom", directory: explicit }, readPackageVersion(dirname(explicit)) ?? manifest2.version);
|
|
175
|
-
}
|
|
176
|
-
const project = findProjectMazUi(options.cwd ?? process.cwd());
|
|
177
|
-
if (project) {
|
|
178
|
-
const directory = join(project.directory, "docs");
|
|
179
|
-
const manifest2 = readManifest(directory);
|
|
180
|
-
if (manifest2)
|
|
181
|
-
return new Knowledge(manifest2, { kind: "project", directory, projectVersion: project.version }, project.version);
|
|
182
|
-
}
|
|
183
|
-
const bundledDir = options.bundledDir ?? defaultBundledDir();
|
|
184
|
-
const manifest = readManifest(bundledDir);
|
|
185
|
-
if (!manifest)
|
|
186
|
-
throw new Error(`The docs bundle of @maz-ui/mcp is missing or invalid (${bundledDir}). Reinstall the package.`);
|
|
187
|
-
return new Knowledge(manifest, { kind: "bundled", directory: bundledDir, projectVersion: project?.version }, options.bundledVersion ?? readOwnVersion());
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const CATEGORY_PRIORITY = ["component", "composable", "directive", "plugin", "util", "guide", "ecosystem", "reference", "node"];
|
|
191
|
-
function toKebab(value) {
|
|
192
|
-
return value.replaceAll(/([a-z\d])([A-Z])/g, "$1-$2").replaceAll(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
193
|
-
}
|
|
194
|
-
function normalizeName(value) {
|
|
195
|
-
return value.trim().replace(/^<\/?|\s*\/?>$/g, "").replace(/^https?:\/\/(?:www\.)?maz-ui\.com\//, "").replace(/^maz-ui:\/\//, "").replace(/^([a-z]+):\/\//, "$1s/").replace(/\.md$/, "").replace(/\/$/, "");
|
|
196
|
-
}
|
|
197
|
-
function byPriority(a, b) {
|
|
198
|
-
return CATEGORY_PRIORITY.indexOf(a.category) - CATEGORY_PRIORITY.indexOf(b.category);
|
|
199
|
-
}
|
|
200
|
-
function resolveDoc(knowledge, query, category) {
|
|
201
|
-
const name = normalizeName(query);
|
|
202
|
-
const lower = name.toLowerCase();
|
|
203
|
-
const kebab = toKebab(name);
|
|
204
|
-
const candidates = knowledge.docs;
|
|
205
|
-
const pick = (predicate) => candidates.filter(predicate).sort(byPriority)[0];
|
|
206
|
-
const byId = knowledge.docsById.get(name);
|
|
207
|
-
if (byId && (true))
|
|
208
|
-
return byId;
|
|
209
|
-
const pathMatch = lower.includes("/") ? pick((doc) => doc.id.toLowerCase() === lower || doc.id.toLowerCase().endsWith(`/${lower}`)) : void 0;
|
|
210
|
-
return pathMatch ?? pick((doc) => doc.symbols.includes(name)) ?? pick((doc) => doc.symbols.some((symbol) => symbol.toLowerCase() === lower)) ?? pick((doc) => doc.slug === lower || doc.slug === kebab) ?? pick((doc) => doc.slug === `maz-${kebab}` || doc.slug === kebab.replace(/^v-/, "") || doc.slug === `use-${kebab}`) ?? pick((doc) => doc.title.toLowerCase() === lower) ?? (lower.includes("/") ? resolveDoc(knowledge, lower.split("/").at(-1)) : void 0);
|
|
211
|
-
}
|
|
212
|
-
function resolveComponent(knowledge, query) {
|
|
213
|
-
const name = normalizeName(query).replace(/^components\//, "");
|
|
214
|
-
const exact = knowledge.componentsByName.get(name);
|
|
215
|
-
if (exact)
|
|
216
|
-
return exact;
|
|
217
|
-
const kebab = toKebab(name).replace(/^maz-/, "");
|
|
218
|
-
for (const component of knowledge.manifest.components) {
|
|
219
|
-
if (toKebab(component.name).replace(/^maz-/, "") === kebab)
|
|
220
|
-
return component;
|
|
221
|
-
}
|
|
222
|
-
return void 0;
|
|
223
|
-
}
|
|
224
|
-
function editDistance(a, b, max = 3) {
|
|
225
|
-
if (Math.abs(a.length - b.length) > max)
|
|
226
|
-
return max + 1;
|
|
227
|
-
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
228
|
-
for (let i = 1; i <= a.length; i++) {
|
|
229
|
-
const current = [i];
|
|
230
|
-
let rowMin = i;
|
|
231
|
-
for (let j = 1; j <= b.length; j++) {
|
|
232
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
233
|
-
current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost);
|
|
234
|
-
rowMin = Math.min(rowMin, current[j]);
|
|
235
|
-
}
|
|
236
|
-
if (rowMin > max)
|
|
237
|
-
return max + 1;
|
|
238
|
-
previous = current;
|
|
239
|
-
}
|
|
240
|
-
return previous[b.length];
|
|
241
|
-
}
|
|
242
|
-
function closest(value, candidates, { max = 3, limit = 3 } = {}) {
|
|
243
|
-
const lower = value.toLowerCase();
|
|
244
|
-
return [...candidates].map((candidate) => ({ candidate, distance: editDistance(lower, candidate.toLowerCase(), max) })).filter(({ distance }) => distance <= max).sort((a, b) => a.distance - b.distance || a.candidate.length - b.candidate.length).slice(0, limit).map(({ candidate }) => candidate);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const SYNONYMS = {
|
|
248
|
-
modal: ["dialog"],
|
|
249
|
-
popup: ["dialog", "popover"],
|
|
250
|
-
lightbox: ["dialog", "fullscreen"],
|
|
251
|
-
confirmation: ["confirm"],
|
|
252
|
-
notification: ["toast"],
|
|
253
|
-
notify: ["toast"],
|
|
254
|
-
snackbar: ["toast"],
|
|
255
|
-
flash: ["toast"],
|
|
256
|
-
loader: ["spinner", "loading"],
|
|
257
|
-
loading: ["spinner", "skeleton", "wait"],
|
|
258
|
-
shimmer: ["skeleton"],
|
|
259
|
-
placeholder: ["skeleton"],
|
|
260
|
-
combobox: ["select"],
|
|
261
|
-
autocomplete: ["select"],
|
|
262
|
-
multiselect: ["select"],
|
|
263
|
-
picker: ["select"],
|
|
264
|
-
datepicker: ["date"],
|
|
265
|
-
calendar: ["date"],
|
|
266
|
-
datetime: ["date", "time"],
|
|
267
|
-
picture: ["avatar", "image"],
|
|
268
|
-
photo: ["avatar", "image", "gallery"],
|
|
269
|
-
profile: ["avatar"],
|
|
270
|
-
img: ["image"],
|
|
271
|
-
upload: ["dropzone"],
|
|
272
|
-
file: ["dropzone"],
|
|
273
|
-
toggle: ["switch"],
|
|
274
|
-
checkbox: ["checkbox"],
|
|
275
|
-
wizard: ["stepper"],
|
|
276
|
-
step: ["stepper"],
|
|
277
|
-
grid: ["table"],
|
|
278
|
-
datagrid: ["table"],
|
|
279
|
-
datatable: ["table"],
|
|
280
|
-
menu: ["dropdown"],
|
|
281
|
-
nav: ["sidebar"],
|
|
282
|
-
navigation: ["sidebar", "tab"],
|
|
283
|
-
sidenav: ["sidebar"],
|
|
284
|
-
dark: ["theme"],
|
|
285
|
-
darkmode: ["theme"],
|
|
286
|
-
palette: ["theme", "color"],
|
|
287
|
-
token: ["tailwind", "theme"],
|
|
288
|
-
css: ["tailwind"],
|
|
289
|
-
class: ["tailwind"],
|
|
290
|
-
classname: ["tailwind"],
|
|
291
|
-
color: ["tailwind", "theme"],
|
|
292
|
-
i18n: ["translation"],
|
|
293
|
-
translate: ["translation"],
|
|
294
|
-
locale: ["translation"],
|
|
295
|
-
language: ["translation"],
|
|
296
|
-
tel: ["phone"],
|
|
297
|
-
telephone: ["phone"],
|
|
298
|
-
money: ["currency", "price"],
|
|
299
|
-
price: ["currency"],
|
|
300
|
-
otp: ["code"],
|
|
301
|
-
pin: ["code"],
|
|
302
|
-
validation: ["validator"],
|
|
303
|
-
validate: ["validator"],
|
|
304
|
-
schema: ["validator"],
|
|
305
|
-
zod: ["validator"],
|
|
306
|
-
valibot: ["validator"],
|
|
307
|
-
hint: ["tooltip"],
|
|
308
|
-
hover: ["tooltip", "popover"],
|
|
309
|
-
hovering: ["tooltip", "popover"],
|
|
310
|
-
typing: ["debounce"],
|
|
311
|
-
keystroke: ["debounce"],
|
|
312
|
-
temporary: ["toast"],
|
|
313
|
-
message: ["toast", "alert"],
|
|
314
|
-
localized: ["format"],
|
|
315
|
-
display: ["format"],
|
|
316
|
-
chip: ["badge", "tag"],
|
|
317
|
-
pill: ["badge"],
|
|
318
|
-
label: ["badge"],
|
|
319
|
-
collapse: ["accordion", "expand"],
|
|
320
|
-
collapsible: ["accordion"],
|
|
321
|
-
marquee: ["ticker"],
|
|
322
|
-
slideshow: ["carousel"],
|
|
323
|
-
gesture: ["swipe", "drag"],
|
|
324
|
-
inactivity: ["idle"],
|
|
325
|
-
afk: ["idle"],
|
|
326
|
-
responsive: ["breakpoint"],
|
|
327
|
-
viewport: ["window", "breakpoint"],
|
|
328
|
-
mobile: ["breakpoint"],
|
|
329
|
-
clipboard: ["copy"],
|
|
330
|
-
slug: ["kebab", "normalize"],
|
|
331
|
-
delay: ["debounce", "sleep"],
|
|
332
|
-
wait: ["sleep", "wait"],
|
|
333
|
-
trash: ["delete"],
|
|
334
|
-
install: ["getting", "started", "setup"],
|
|
335
|
-
setup: ["getting", "started", "install"],
|
|
336
|
-
autoimport: ["resolver", "nuxt"],
|
|
337
|
-
ai: ["agent", "mcp"],
|
|
338
|
-
llm: ["agent", "mcp"]
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const STOPWORDS = /* @__PURE__ */ new Set([
|
|
342
|
-
"a",
|
|
343
|
-
"an",
|
|
344
|
-
"and",
|
|
345
|
-
"any",
|
|
346
|
-
"are",
|
|
347
|
-
"as",
|
|
348
|
-
"at",
|
|
349
|
-
"be",
|
|
350
|
-
"by",
|
|
351
|
-
"can",
|
|
352
|
-
"create",
|
|
353
|
-
"do",
|
|
354
|
-
"does",
|
|
355
|
-
"for",
|
|
356
|
-
"from",
|
|
357
|
-
"get",
|
|
358
|
-
"how",
|
|
359
|
-
"i",
|
|
360
|
-
"in",
|
|
361
|
-
"into",
|
|
362
|
-
"is",
|
|
363
|
-
"it",
|
|
364
|
-
"its",
|
|
365
|
-
"make",
|
|
366
|
-
"me",
|
|
367
|
-
"my",
|
|
368
|
-
"need",
|
|
369
|
-
"of",
|
|
370
|
-
"on",
|
|
371
|
-
"or",
|
|
372
|
-
"some",
|
|
373
|
-
"that",
|
|
374
|
-
"the",
|
|
375
|
-
"this",
|
|
376
|
-
"to",
|
|
377
|
-
"use",
|
|
378
|
-
"using",
|
|
379
|
-
"want",
|
|
380
|
-
"way",
|
|
381
|
-
"what",
|
|
382
|
-
"when",
|
|
383
|
-
"which",
|
|
384
|
-
"with",
|
|
385
|
-
"you",
|
|
386
|
-
"your",
|
|
387
|
-
"maz",
|
|
388
|
-
"ui",
|
|
389
|
-
"vue",
|
|
390
|
-
"nuxt"
|
|
391
|
-
]);
|
|
392
|
-
function stem(term) {
|
|
393
|
-
if (term.length <= 4 || /\d/.test(term))
|
|
394
|
-
return term;
|
|
395
|
-
if (term.endsWith("ies"))
|
|
396
|
-
return `${term.slice(0, -3)}y`;
|
|
397
|
-
if (/(?:ch|sh|x|ss)es$/.test(term))
|
|
398
|
-
return term.slice(0, -2);
|
|
399
|
-
if (term.endsWith("s") && !term.endsWith("ss") && !term.endsWith("us") && !term.endsWith("is"))
|
|
400
|
-
return term.slice(0, -1);
|
|
401
|
-
if (term.endsWith("ing") && term.length > 6)
|
|
402
|
-
return term.slice(0, -3);
|
|
403
|
-
if (term.endsWith("ed") && term.length > 5)
|
|
404
|
-
return term.slice(0, -2);
|
|
405
|
-
return term;
|
|
406
|
-
}
|
|
407
|
-
function tokenize(text) {
|
|
408
|
-
const tokens = [];
|
|
409
|
-
for (const word of text.split(/[^\w$-]+|_+/)) {
|
|
410
|
-
if (!word)
|
|
411
|
-
continue;
|
|
412
|
-
const parts = word.replaceAll(/([a-z])(\d)/gi, "$1 $2").replaceAll(/([a-z\d])([A-Z])/g, "$1 $2").replaceAll(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").split(/[\s-]+/).filter(Boolean);
|
|
413
|
-
if (parts.length > 1)
|
|
414
|
-
tokens.push(word.replaceAll("-", ""));
|
|
415
|
-
tokens.push(...parts);
|
|
416
|
-
}
|
|
417
|
-
return tokens;
|
|
418
|
-
}
|
|
419
|
-
function processTerm(term) {
|
|
420
|
-
const lower = term.toLowerCase().replaceAll("$", "");
|
|
421
|
-
if (lower.length < 2 || STOPWORDS.has(lower))
|
|
422
|
-
return null;
|
|
423
|
-
return stem(lower);
|
|
424
|
-
}
|
|
425
|
-
function analyze(text) {
|
|
426
|
-
return tokenize(text).map(processTerm).filter((term) => term !== null);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const FIELD_BOOSTS = {
|
|
430
|
-
name: 8,
|
|
431
|
-
symbols: 3,
|
|
432
|
-
aliases: 5,
|
|
433
|
-
description: 2.5,
|
|
434
|
-
headings: 1.5,
|
|
435
|
-
api: 1.2,
|
|
436
|
-
body: 0.5
|
|
437
|
-
};
|
|
438
|
-
const CATEGORY_HINTS = {
|
|
439
|
-
component: "component",
|
|
440
|
-
composable: "composable",
|
|
441
|
-
hook: "composable",
|
|
442
|
-
directive: "directive",
|
|
443
|
-
plugin: "plugin",
|
|
444
|
-
helper: "util",
|
|
445
|
-
util: "util",
|
|
446
|
-
utility: "util",
|
|
447
|
-
function: "util",
|
|
448
|
-
guide: "guide"
|
|
449
|
-
};
|
|
450
|
-
const META_DOCS = /* @__PURE__ */ new Map([
|
|
451
|
-
["guide/ai-agents", /\b(?:ai|agents?|llms?|claude|cursor|copilot|codex|mcp)\b/i],
|
|
452
|
-
["ecosystem/mcp", /\b(?:ai|agents?|llms?|claude|cursor|copilot|codex|mcp)\b/i],
|
|
453
|
-
["ecosystem/eslint-config", /\b(?:eslint|lint|linter|linting)\b/i],
|
|
454
|
-
["ecosystem/stylelint-config", /\b(?:stylelint|lint|linter|linting)\b/i],
|
|
455
|
-
["guide/migration-v5", /\b(?:migrat\w*|upgrad\w*|v4|v5|breaking)\b/i]
|
|
456
|
-
]);
|
|
457
|
-
const META_DOC_FACTOR = 0.15;
|
|
458
|
-
const LOW_VALUE_TERMS = new Set(["add", "show", "display", "delete", "remove", "update", "set", "open", "close", "user", "users", "page", "item", "data", "value", "list", "text", "click", "between", "before", "after"].map((term) => processTerm(term) ?? term));
|
|
459
|
-
const LOW_VALUE_FACTOR = 0.4;
|
|
460
|
-
const LOW_VALUE_ONLY_FACTOR = 0.25;
|
|
461
|
-
const EXACT_MATCH_BOOST = 4;
|
|
462
|
-
const EXACT_ALIAS_BOOST = 4;
|
|
463
|
-
const CATEGORY_HINT_BOOST = 1.4;
|
|
464
|
-
function normalizedSynonyms() {
|
|
465
|
-
const map = /* @__PURE__ */ new Map();
|
|
466
|
-
for (const [key, values] of Object.entries(SYNONYMS)) {
|
|
467
|
-
const term = processTerm(key);
|
|
468
|
-
if (term)
|
|
469
|
-
map.set(term, values.map(processTerm).filter((value) => !!value && value !== term));
|
|
470
|
-
}
|
|
471
|
-
return map;
|
|
472
|
-
}
|
|
473
|
-
function bodyOf(markdown) {
|
|
474
|
-
return markdown.replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
475
|
-
}
|
|
476
|
-
class DocSearch {
|
|
477
|
-
constructor(knowledge) {
|
|
478
|
-
this.knowledge = knowledge;
|
|
479
|
-
this.index = new MiniSearch({
|
|
480
|
-
fields: Object.keys(FIELD_BOOSTS),
|
|
481
|
-
storeFields: [],
|
|
482
|
-
tokenize,
|
|
483
|
-
processTerm
|
|
484
|
-
});
|
|
485
|
-
this.index.addAll(knowledge.docs.map((doc) => this.toIndexed(doc)));
|
|
486
|
-
}
|
|
487
|
-
index;
|
|
488
|
-
synonyms = normalizedSynonyms();
|
|
489
|
-
toIndexed(doc) {
|
|
490
|
-
const component = doc.category === "component" ? this.knowledge.componentsByName.get(doc.title) : void 0;
|
|
491
|
-
const api = component ? [
|
|
492
|
-
...component.props.map((prop) => prop.name),
|
|
493
|
-
...component.events.map((event) => event.name),
|
|
494
|
-
...component.slots.map((slot) => slot.name)
|
|
495
|
-
].join(" ") : "";
|
|
496
|
-
return {
|
|
497
|
-
id: doc.id,
|
|
498
|
-
name: [doc.title, doc.slug].join(" "),
|
|
499
|
-
symbols: doc.symbols.join(" "),
|
|
500
|
-
aliases: doc.aliases.join(" . "),
|
|
501
|
-
description: doc.description,
|
|
502
|
-
headings: doc.headings.join(" . "),
|
|
503
|
-
api,
|
|
504
|
-
body: bodyOf(this.knowledge.readDoc(doc.id))
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
exactMatch(query) {
|
|
508
|
-
const name = normalizeName(query);
|
|
509
|
-
const lower = name.toLowerCase();
|
|
510
|
-
const kebab = toKebab(name);
|
|
511
|
-
return this.knowledge.docs.find(
|
|
512
|
-
(doc) => doc.symbols.some((symbol) => symbol.toLowerCase() === lower) || doc.title.toLowerCase() === lower || doc.slug === lower || doc.slug === kebab || doc.id === lower || doc.slug === `maz-${kebab}`
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
search(query, { category, limit = 8 } = {}) {
|
|
516
|
-
const terms = analyze(query);
|
|
517
|
-
if (terms.length === 0)
|
|
518
|
-
return [];
|
|
519
|
-
const hintedCategory = category ? void 0 : terms.map((term) => CATEGORY_HINTS[term]).find(Boolean);
|
|
520
|
-
const expansion = [...new Set(terms.flatMap((term) => this.synonyms.get(term) ?? []))].filter((term) => !terms.includes(term));
|
|
521
|
-
const exact = this.exactMatch(query);
|
|
522
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
523
|
-
const hasValuableTerm = terms.some((term) => !LOW_VALUE_TERMS.has(term));
|
|
524
|
-
const results = this.index.search({
|
|
525
|
-
combineWith: "OR",
|
|
526
|
-
queries: [
|
|
527
|
-
{ queries: [terms.join(" ")], combineWith: "OR", boostTerm: (term) => LOW_VALUE_TERMS.has(term) ? LOW_VALUE_FACTOR : 1 },
|
|
528
|
-
...expansion.length > 0 ? [{ queries: [expansion.join(" ")], combineWith: "OR", boostTerm: () => 0.45 }] : []
|
|
529
|
-
]
|
|
530
|
-
}, {
|
|
531
|
-
boost: FIELD_BOOSTS,
|
|
532
|
-
fuzzy: (term) => term.length >= 5 ? 0.2 : false,
|
|
533
|
-
prefix: (term) => term.length >= 4,
|
|
534
|
-
weights: { fuzzy: 0.35, prefix: 0.5 },
|
|
535
|
-
filter: (result) => !category || this.knowledge.docsById.get(result.id)?.category === category
|
|
536
|
-
});
|
|
537
|
-
return results.map((result) => {
|
|
538
|
-
const doc = this.knowledge.docsById.get(result.id);
|
|
539
|
-
const matched = terms.filter((term) => result.queryTerms.includes(term));
|
|
540
|
-
const coverage = matched.length / terms.length;
|
|
541
|
-
let score = result.score * (0.6 + coverage);
|
|
542
|
-
if (exact && doc.id === exact.id)
|
|
543
|
-
score *= EXACT_MATCH_BOOST;
|
|
544
|
-
else if (doc.aliases.includes(normalizedQuery))
|
|
545
|
-
score *= EXACT_ALIAS_BOOST;
|
|
546
|
-
if (hintedCategory && doc.category === hintedCategory)
|
|
547
|
-
score *= CATEGORY_HINT_BOOST;
|
|
548
|
-
if (hasValuableTerm && result.queryTerms.every((term) => LOW_VALUE_TERMS.has(term)))
|
|
549
|
-
score *= LOW_VALUE_ONLY_FACTOR;
|
|
550
|
-
const metaTopic = META_DOCS.get(doc.id);
|
|
551
|
-
if (metaTopic && !metaTopic.test(query))
|
|
552
|
-
score *= META_DOC_FACTOR;
|
|
553
|
-
return { doc, score, matched };
|
|
554
|
-
}).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
const ICON_SYNONYMS = {
|
|
558
|
-
delete: ["trash"],
|
|
559
|
-
remove: ["trash", "xmark", "minus"],
|
|
560
|
-
bin: ["trash"],
|
|
561
|
-
close: ["xmark"],
|
|
562
|
-
cross: ["xmark"],
|
|
563
|
-
cancel: ["xmark"],
|
|
564
|
-
add: ["plus"],
|
|
565
|
-
plus: ["plus"],
|
|
566
|
-
new: ["plus"],
|
|
567
|
-
create: ["plus"],
|
|
568
|
-
edit: ["pencil"],
|
|
569
|
-
write: ["pencil"],
|
|
570
|
-
rename: ["pencil"],
|
|
571
|
-
search: ["magnifying", "glass"],
|
|
572
|
-
find: ["magnifying"],
|
|
573
|
-
zoom: ["magnifying"],
|
|
574
|
-
settings: ["cog", "adjustment"],
|
|
575
|
-
setting: ["cog", "adjustment"],
|
|
576
|
-
config: ["cog"],
|
|
577
|
-
preferences: ["cog", "adjustment"],
|
|
578
|
-
filter: ["funnel", "adjustment"],
|
|
579
|
-
menu: ["bars"],
|
|
580
|
-
hamburger: ["bars"],
|
|
581
|
-
user: ["user"],
|
|
582
|
-
account: ["user"],
|
|
583
|
-
profile: ["user"],
|
|
584
|
-
people: ["user"],
|
|
585
|
-
team: ["user"],
|
|
586
|
-
logout: ["arrow", "rectangle"],
|
|
587
|
-
login: ["arrow", "rectangle"],
|
|
588
|
-
signout: ["arrow", "rectangle"],
|
|
589
|
-
download: ["arrow", "down", "tray"],
|
|
590
|
-
upload: ["arrow", "up", "tray"],
|
|
591
|
-
export: ["arrow", "up", "tray"],
|
|
592
|
-
import: ["arrow", "down", "tray"],
|
|
593
|
-
back: ["arrow", "left"],
|
|
594
|
-
previous: ["chevron", "left"],
|
|
595
|
-
next: ["chevron", "right"],
|
|
596
|
-
forward: ["arrow", "right"],
|
|
597
|
-
expand: ["chevron", "down"],
|
|
598
|
-
dropdown: ["chevron", "down"],
|
|
599
|
-
success: ["check"],
|
|
600
|
-
valid: ["check"],
|
|
601
|
-
done: ["check"],
|
|
602
|
-
ok: ["check"],
|
|
603
|
-
error: ["exclamation"],
|
|
604
|
-
warning: ["exclamation", "triangle"],
|
|
605
|
-
alert: ["exclamation", "bell"],
|
|
606
|
-
info: ["information"],
|
|
607
|
-
help: ["question"],
|
|
608
|
-
notification: ["bell"],
|
|
609
|
-
email: ["envelope"],
|
|
610
|
-
mail: ["envelope"],
|
|
611
|
-
message: ["chat", "envelope"],
|
|
612
|
-
comment: ["chat"],
|
|
613
|
-
phone: ["phone", "device"],
|
|
614
|
-
call: ["phone"],
|
|
615
|
-
calendar: ["calendar"],
|
|
616
|
-
date: ["calendar"],
|
|
617
|
-
time: ["clock"],
|
|
618
|
-
clock: ["clock"],
|
|
619
|
-
favorite: ["star", "heart"],
|
|
620
|
-
like: ["heart", "thumb"],
|
|
621
|
-
love: ["heart"],
|
|
622
|
-
home: ["home"],
|
|
623
|
-
house: ["home"],
|
|
624
|
-
lock: ["lock"],
|
|
625
|
-
password: ["lock", "key"],
|
|
626
|
-
security: ["shield", "lock"],
|
|
627
|
-
visible: ["eye"],
|
|
628
|
-
show: ["eye"],
|
|
629
|
-
hide: ["eye", "slash"],
|
|
630
|
-
hidden: ["eye", "slash"],
|
|
631
|
-
copy: ["clipboard", "document", "duplicate"],
|
|
632
|
-
paste: ["clipboard"],
|
|
633
|
-
link: ["link"],
|
|
634
|
-
share: ["share"],
|
|
635
|
-
image: ["photo"],
|
|
636
|
-
picture: ["photo"],
|
|
637
|
-
camera: ["camera"],
|
|
638
|
-
video: ["video", "film"],
|
|
639
|
-
music: ["musical"],
|
|
640
|
-
file: ["document"],
|
|
641
|
-
folder: ["folder"],
|
|
642
|
-
cart: ["shopping"],
|
|
643
|
-
shop: ["shopping"],
|
|
644
|
-
payment: ["credit", "card", "banknote"],
|
|
645
|
-
money: ["banknote", "currency"],
|
|
646
|
-
refresh: ["arrow", "path"],
|
|
647
|
-
reload: ["arrow", "path"],
|
|
648
|
-
sync: ["arrow", "path"],
|
|
649
|
-
undo: ["arrow", "uturn"],
|
|
650
|
-
dark: ["moon"],
|
|
651
|
-
light: ["sun"],
|
|
652
|
-
theme: ["moon", "sun", "swatch"],
|
|
653
|
-
language: ["language", "globe"],
|
|
654
|
-
world: ["globe"],
|
|
655
|
-
location: ["map", "pin"],
|
|
656
|
-
map: ["map"],
|
|
657
|
-
chart: ["chart"],
|
|
658
|
-
stats: ["chart"],
|
|
659
|
-
print: ["printer"],
|
|
660
|
-
more: ["ellipsis"],
|
|
661
|
-
options: ["ellipsis"],
|
|
662
|
-
drag: ["bars"],
|
|
663
|
-
external: ["arrow", "top", "right", "square"],
|
|
664
|
-
github: ["github"],
|
|
665
|
-
code: ["code"]
|
|
666
|
-
};
|
|
667
|
-
const regionNames = new Intl.DisplayNames(["en"], { type: "region" });
|
|
668
|
-
function regionName(code) {
|
|
669
|
-
try {
|
|
670
|
-
return regionNames.of(code) ?? "";
|
|
671
|
-
} catch {
|
|
672
|
-
return "";
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
const iconWordsCache = /* @__PURE__ */ new Map();
|
|
676
|
-
function iconWords(name) {
|
|
677
|
-
const cached = iconWordsCache.get(name);
|
|
678
|
-
if (cached)
|
|
679
|
-
return cached;
|
|
680
|
-
const flag = /^MazFlag(?:Square)?([A-Z]{2})(?:[A-Z]{2})?$/.exec(name);
|
|
681
|
-
const country = flag ? regionName(flag[1]) : "";
|
|
682
|
-
const words = [...analyze(name.replace(/^Maz/, "")), ...flag ? ["flag", "country", flag[1].toLowerCase()] : [], ...analyze(country)];
|
|
683
|
-
iconWordsCache.set(name, words);
|
|
684
|
-
return words;
|
|
685
|
-
}
|
|
686
|
-
function searchIcons(names, query, limit = 20) {
|
|
687
|
-
const words = analyze(query.replace(/\bicons?\b/gi, ""));
|
|
688
|
-
if (words.length === 0)
|
|
689
|
-
return [];
|
|
690
|
-
const synonyms = new Map(Object.entries(ICON_SYNONYMS).map(([key, values]) => [
|
|
691
|
-
processTerm(key) ?? key,
|
|
692
|
-
values.map((value) => processTerm(value) ?? value)
|
|
693
|
-
]));
|
|
694
|
-
return [...names].map((name) => {
|
|
695
|
-
const parts = iconWords(name);
|
|
696
|
-
const lowerName = name.toLowerCase();
|
|
697
|
-
let score = 0;
|
|
698
|
-
for (const word of words) {
|
|
699
|
-
const candidates = [word, ...synonyms.get(word) ?? []];
|
|
700
|
-
for (const [index, candidate] of candidates.entries()) {
|
|
701
|
-
const weight = index === 0 ? 1 : 0.8;
|
|
702
|
-
if (parts.includes(candidate))
|
|
703
|
-
score += 3 * weight;
|
|
704
|
-
else if (lowerName.includes(candidate))
|
|
705
|
-
score += 1.5 * weight;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
if (score > 0 && name.startsWith("MazFlag") && !words.some((word) => ["flag", "country"].includes(word)))
|
|
709
|
-
score *= 0.3;
|
|
710
|
-
return { name, score: score - parts.length * 0.05 };
|
|
711
|
-
}).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit).map((entry) => entry.name);
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function createContext(options = {}) {
|
|
715
|
-
const cwd = options.cwd ?? process.cwd();
|
|
716
|
-
const knowledge = options.knowledge ?? loadKnowledge({ ...options, cwd });
|
|
717
|
-
return {
|
|
718
|
-
knowledge,
|
|
719
|
-
search: new DocSearch(knowledge),
|
|
720
|
-
cwd
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const COLOR_UTILITIES = "bg|text|border(?:-[trblxyse])?|ring(?:-offset)?|outline|fill|stroke|divide|from|via|to|decoration|placeholder|accent|caret|shadow|inset-shadow|inset-ring|text-shadow";
|
|
725
|
-
const PALETTE = "slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose";
|
|
726
|
-
const PALETTE_CLASS = new RegExp(`^(?:[\\w-]+:)*!?(${COLOR_UTILITIES})-(${PALETTE})-(\\d{2,3})(?:/\\d+)?!?$`);
|
|
727
|
-
const BLACK_WHITE_CLASS = new RegExp(`^(?:[\\w-]+:)*!?(${COLOR_UTILITIES})-(white|black)(?:/\\d+)?!?$`);
|
|
728
|
-
const ARBITRARY_COLOR_CLASS = /^(?:[\w-]+:)*!?[\w-]+-\[(?:#[\da-f]{3,8}|(?:rgba?|hsla?|oklch|oklab|lab|lch|color)\([^\]]*\))\]/i;
|
|
729
|
-
const TAILWIND_V3_CLASS = /^(?:[\w-]+:)*!?(?:(?:bg|text|border|divide|ring|placeholder)-opacity-\d+|flex-(?:shrink|grow)(?:-\d+)?|overflow-ellipsis|decoration-(?:slice|clone))$/;
|
|
730
|
-
const HUE_TO_THEME = {
|
|
731
|
-
slate: "surface / foreground / muted / divider",
|
|
732
|
-
gray: "surface / foreground / muted / divider",
|
|
733
|
-
zinc: "surface / foreground / muted / divider",
|
|
734
|
-
neutral: "surface / foreground / muted / divider",
|
|
735
|
-
stone: "surface / foreground / muted / divider",
|
|
736
|
-
red: "destructive",
|
|
737
|
-
rose: "destructive",
|
|
738
|
-
orange: "warning",
|
|
739
|
-
amber: "warning",
|
|
740
|
-
yellow: "warning",
|
|
741
|
-
green: "success",
|
|
742
|
-
emerald: "success",
|
|
743
|
-
lime: "success",
|
|
744
|
-
teal: "success / info",
|
|
745
|
-
cyan: "info",
|
|
746
|
-
sky: "info",
|
|
747
|
-
blue: "primary / info",
|
|
748
|
-
indigo: "primary",
|
|
749
|
-
violet: "secondary / accent",
|
|
750
|
-
purple: "secondary / accent",
|
|
751
|
-
fuchsia: "accent",
|
|
752
|
-
pink: "accent"
|
|
753
|
-
};
|
|
754
|
-
function neutralSuggestion(utility) {
|
|
755
|
-
if (utility === "text")
|
|
756
|
-
return "`text-foreground` (body text) or `text-muted` (secondary text)";
|
|
757
|
-
if (utility.startsWith("border") || utility === "divide")
|
|
758
|
-
return `\`${utility}-divider\``;
|
|
759
|
-
if (utility === "bg")
|
|
760
|
-
return "`bg-surface`, `bg-surface-{50..950}` or `bg-container`";
|
|
761
|
-
return `\`${utility}-foreground\`, \`${utility}-muted\` or \`${utility}-divider\``;
|
|
762
|
-
}
|
|
763
|
-
function themeSuggestion(utility, hue, step) {
|
|
764
|
-
const neutral = ["slate", "gray", "zinc", "neutral", "stone"].includes(hue);
|
|
765
|
-
if (neutral)
|
|
766
|
-
return neutralSuggestion(utility);
|
|
767
|
-
const colors = HUE_TO_THEME[hue].split(" / ");
|
|
768
|
-
const scale = step === "500" ? "" : `-${step}`;
|
|
769
|
-
return colors.map((color) => `\`${utility}-${color}${scale}\``).join(" or ");
|
|
770
|
-
}
|
|
771
|
-
const THEME_BACKGROUND = /^(?:[\w-]+:)*!?bg-(primary|secondary|accent|info|success|warning|destructive|contrast)(?:-\d{2,3})?(?:\/\d+)?!?$/;
|
|
772
|
-
const FIXED_BACKGROUND = /^(?:[\w-]+:)*!?(?:bg-(?:overlay|black|white|transparent|linear|radial|conic|gradient|none)|bg-\[|bg-\(|from-|via-|to-)/;
|
|
773
|
-
function backgroundOf(classList) {
|
|
774
|
-
for (const token of classList.split(/\s+/)) {
|
|
775
|
-
const theme = THEME_BACKGROUND.exec(token);
|
|
776
|
-
if (theme)
|
|
777
|
-
return { kind: "theme", color: theme[1] };
|
|
778
|
-
if (FIXED_BACKGROUND.test(token))
|
|
779
|
-
return { kind: "fixed" };
|
|
780
|
-
}
|
|
781
|
-
return void 0;
|
|
782
|
-
}
|
|
783
|
-
function reportBlackWhite(token, utility, color, background, position, context) {
|
|
784
|
-
if (background?.kind === "fixed" && utility !== "bg")
|
|
785
|
-
return;
|
|
786
|
-
if (background?.color && color === "white" && utility !== "bg") {
|
|
787
|
-
context.report({
|
|
788
|
-
rule: "theme-colors",
|
|
789
|
-
severity: "warning",
|
|
790
|
-
message: `\`${token}\` on \`bg-${background.color}\`: the theme gives the readable color of each background.`,
|
|
791
|
-
suggestion: `Use \`${utility}-${background.color}-foreground\`.`
|
|
792
|
-
}, position);
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
context.report({
|
|
796
|
-
rule: "theme-colors",
|
|
797
|
-
severity: "warning",
|
|
798
|
-
message: `\`${token}\` is a fixed color that does not follow the theme nor dark mode.`,
|
|
799
|
-
suggestion: color === "white" ? `Use \`${utility}-surface\` (backgrounds) or the \`-foreground\` of the color behind (\`text-primary-foreground\`). Over media (image, video) or a fixed dark background, keep it with \`<!-- maz-ui-review-ignore theme-colors -- over the video -->\` above the element.` : `Use \`${utility}-foreground\` (text) or \`${utility}-contrast\`.`
|
|
800
|
-
}, position);
|
|
801
|
-
}
|
|
802
|
-
function checkClassList(value, offset, context, inherited) {
|
|
803
|
-
const background = backgroundOf(value) ?? inherited;
|
|
804
|
-
for (const match of value.matchAll(/\S+/g)) {
|
|
805
|
-
const token = match[0];
|
|
806
|
-
const position = offset + (match.index ?? 0);
|
|
807
|
-
if (/^(?:[\w-]+:)*maz:/.test(token) || token.startsWith("maz:")) {
|
|
808
|
-
context.report({
|
|
809
|
-
rule: "internal-prefix",
|
|
810
|
-
severity: "error",
|
|
811
|
-
message: `\`${token}\` uses the \`maz:\` prefix, which is internal to the maz-ui library and its docs: it does not exist in your project.`,
|
|
812
|
-
suggestion: `Use \`${token.replace(/maz:/g, "")}\`.`
|
|
813
|
-
}, position);
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
const palette = PALETTE_CLASS.exec(token);
|
|
817
|
-
if (palette) {
|
|
818
|
-
const [, utility, hue, step] = palette;
|
|
819
|
-
context.report({
|
|
820
|
-
rule: "theme-colors",
|
|
821
|
-
severity: "warning",
|
|
822
|
-
message: `\`${token}\` uses the default Tailwind palette: it ignores the maz-ui theme (presets, dark mode).`,
|
|
823
|
-
suggestion: `Use a theme color: ${themeSuggestion(utility, hue, step)}. See \`get_theme_tokens({ topics: ["colors"] })\`.`
|
|
824
|
-
}, position);
|
|
825
|
-
continue;
|
|
826
|
-
}
|
|
827
|
-
const blackWhite = BLACK_WHITE_CLASS.exec(token);
|
|
828
|
-
if (blackWhite) {
|
|
829
|
-
reportBlackWhite(token, blackWhite[1], blackWhite[2], background, position, context);
|
|
830
|
-
continue;
|
|
831
|
-
}
|
|
832
|
-
if (ARBITRARY_COLOR_CLASS.test(token)) {
|
|
833
|
-
context.report({
|
|
834
|
-
rule: "hardcoded-color",
|
|
835
|
-
severity: "warning",
|
|
836
|
-
message: `\`${token}\` hard-codes a color.`,
|
|
837
|
-
suggestion: "Use a theme color class (`bg-primary`, `text-muted`...) or `var(--maz-*)`. For a brand color missing from the theme, define it once in your Tailwind `@theme` or in the preset."
|
|
838
|
-
}, position);
|
|
839
|
-
continue;
|
|
840
|
-
}
|
|
841
|
-
if (TAILWIND_V3_CLASS.test(token)) {
|
|
842
|
-
context.report({
|
|
843
|
-
rule: "tailwind-v3",
|
|
844
|
-
severity: "warning",
|
|
845
|
-
message: `\`${token}\` is a Tailwind v3 utility removed in Tailwind v4.`,
|
|
846
|
-
suggestion: token.includes("opacity") ? "Use the opacity modifier: `bg-primary/50`." : "Use `shrink-0`, `grow`, `text-ellipsis` or `box-decoration-slice`."
|
|
847
|
-
}, position);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
function checkClassStrings(code, offset, context) {
|
|
852
|
-
for (const match of code.matchAll(/(['"`])((?:(?!\1)[^\\\n]|\\.)*)\1/g)) {
|
|
853
|
-
const content = match[2];
|
|
854
|
-
if (/(?:^|\s)(?:[\w-]+:)*(?:maz:|(?:bg|text|border|ring|fill|stroke|from|to|via|outline|divide)-)/.test(content))
|
|
855
|
-
checkClassList(content, offset + (match.index ?? 0) + 1, context);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const UTILS_NAMES = {
|
|
860
|
-
debounce: "`debounce` from `@maz-ui/utils/helpers/debounce`",
|
|
861
|
-
throttle: "`throttle` from `@maz-ui/utils/helpers/throttle`",
|
|
862
|
-
isEqual: "`isEqual` from `@maz-ui/utils/helpers/isEqual`",
|
|
863
|
-
capitalize: "`capitalize` from `@maz-ui/utils/helpers/capitalize`",
|
|
864
|
-
upperFirst: "`upperFirst` from `@maz-ui/utils/helpers/upperFirst`",
|
|
865
|
-
camelCase: "`camelCase` from `@maz-ui/utils/helpers/camelCase`",
|
|
866
|
-
kebabCase: "`kebabCase` from `@maz-ui/utils/helpers/kebabCase`",
|
|
867
|
-
snakeCase: "`snakeCase` from `@maz-ui/utils/helpers/snakeCase`",
|
|
868
|
-
startCase: "`pascalCase` or `normalizeString` from `@maz-ui/utils`",
|
|
869
|
-
delay: "`sleep` from `@maz-ui/utils/helpers/sleep`"
|
|
870
|
-
};
|
|
871
|
-
const THIRD_PARTY_RULES = [
|
|
872
|
-
{ packages: ["lodash", "lodash-es", "lodash/", "lodash-es/", "underscore", "radash", "es-toolkit"], names: UTILS_NAMES, severity: "warning", rule: "prefer-utils" },
|
|
873
|
-
{ packages: ["lodash.debounce", "just-debounce-it", "debounce", "p-debounce"], replacement: UTILS_NAMES.debounce, severity: "warning", rule: "prefer-utils" },
|
|
874
|
-
{ packages: ["lodash.throttle", "just-throttle", "throttle-debounce"], replacement: "`debounce` / `throttle` from `@maz-ui/utils`", severity: "warning", rule: "prefer-utils" },
|
|
875
|
-
{ packages: ["js-cookie", "universal-cookie", "vue-cookies"], replacement: "`getCookie`, `setCookie`, `deleteCookie` from `@maz-ui/utils/helpers/cookie`", severity: "warning", rule: "prefer-utils" },
|
|
876
|
-
{
|
|
877
|
-
packages: ["@vueuse/core"],
|
|
878
|
-
names: {
|
|
879
|
-
useWindowSize: "`useWindowSize` from `maz-ui/composables/useWindowSize`",
|
|
880
|
-
useBreakpoints: "`useBreakpoints` from `maz-ui/composables/useBreakpoints`",
|
|
881
|
-
onClickOutside: "the `vClickOutside` directive (`maz-ui/directives/vClickOutside`)",
|
|
882
|
-
useIdle: "`useIdleTimeout` from `maz-ui/composables/useIdleTimeout`",
|
|
883
|
-
useSwipe: "`useSwipe` from `maz-ui/composables/useSwipe`",
|
|
884
|
-
useDocumentVisibility: "`useUserVisibility` from `maz-ui/composables/useUserVisibility`",
|
|
885
|
-
useDropZone: "MazDropzone or `useDropzone` (`maz-ui/composables/useDropzone`)",
|
|
886
|
-
useDark: "`useTheme` from `@maz-ui/themes` (`isDark`, `toggleDarkMode`)",
|
|
887
|
-
useColorMode: "`useTheme` from `@maz-ui/themes` (`colorMode`, `setColorMode`)",
|
|
888
|
-
usePreferredDark: "`useTheme` from `@maz-ui/themes`",
|
|
889
|
-
useDebounceFn: "`debounce` from `@maz-ui/utils/helpers/debounce`",
|
|
890
|
-
useThrottleFn: "`throttle` from `@maz-ui/utils/helpers/throttle`"
|
|
891
|
-
},
|
|
892
|
-
severity: "info",
|
|
893
|
-
rule: "prefer-ecosystem"
|
|
894
|
-
},
|
|
895
|
-
{ packages: ["vue-toastification", "vue-sonner", "vue3-toastify", "notivue", "@kyvg/vue3-notification", "vue-toast-notification"], replacement: "`useToast()` (maz-ui toast plugin)", severity: "warning", rule: "prefer-ecosystem" },
|
|
896
|
-
{ packages: ["@heroicons/vue", "lucide-vue-next", "@lucide/vue", "@iconify/vue", "@phosphor-icons/vue", "@tabler/icons-vue", "vue-feather-icons", "@fortawesome/", "unplugin-icons", "~icons/", "virtual:icons/", "@mdi/js", "oh-vue-icons"], replacement: "@maz-ui/icons (`find_icons`)", severity: "warning", rule: "prefer-ecosystem" },
|
|
897
|
-
{ packages: ["floating-vue", "v-tooltip", "tippy.js", "vue-tippy"], replacement: "the `vTooltip` directive or MazPopover", severity: "warning", rule: "prefer-ecosystem" },
|
|
898
|
-
{ packages: ["@floating-ui/vue", "@floating-ui/dom", "@popperjs/core"], replacement: "MazPopover (floating panel with positioning, triggers and focus management) or `vTooltip`", severity: "info", rule: "prefer-ecosystem" },
|
|
899
|
-
{ packages: ["vue-chartjs", "vue-chart-3", "apexcharts", "vue3-apexcharts", "echarts", "vue-echarts"], replacement: "MazChart (chart.js based)", severity: "info", rule: "prefer-ecosystem" },
|
|
900
|
-
{ packages: ["@vuepic/vue-datepicker", "vue-datepicker-next", "v-calendar", "vue-flatpickr-component", "flatpickr"], replacement: "MazDatePicker", severity: "warning", rule: "prefer-ecosystem" },
|
|
901
|
-
{ packages: ["vue-tel-input", "vue-phone-number-input", "vue-country-code"], replacement: "MazInputPhoneNumber / MazSelectCountry", severity: "warning", rule: "prefer-ecosystem" },
|
|
902
|
-
{ packages: ["vue-multiselect", "vue-select", "@vueform/multiselect", "vue3-select-component"], replacement: "MazSelect (search, multiple, groups, custom options)", severity: "warning", rule: "prefer-ecosystem" },
|
|
903
|
-
{ packages: ["vue-final-modal", "@kouts/vue-modal", "vue-js-modal"], replacement: "MazDialog, MazDialogConfirm or MazBottomSheet", severity: "warning", rule: "prefer-ecosystem" },
|
|
904
|
-
{ packages: ["vue3-dropzone", "vue-dropzone", "dropzone", "vue-upload-component", "filepond", "vue-filepond"], replacement: "MazDropzone", severity: "warning", rule: "prefer-ecosystem" },
|
|
905
|
-
{ packages: ["vue3-otp-input", "vue-otp-input"], replacement: "MazInputCode", severity: "warning", rule: "prefer-ecosystem" },
|
|
906
|
-
{ packages: ["vue3-carousel", "vue-agile", "swiper/vue"], replacement: "MazCarousel", severity: "info", rule: "prefer-ecosystem" },
|
|
907
|
-
{ packages: ["vue-skeletor", "vue-content-loader", "vue-loading-skeleton"], replacement: "MazSkeleton", severity: "warning", rule: "prefer-ecosystem" },
|
|
908
|
-
{ packages: ["vue-loading-overlay", "vue-spinner", "vue-spinners", "epic-spinners"], replacement: "MazSpinner, MazFullscreenLoader or `useWait`", severity: "warning", rule: "prefer-ecosystem" },
|
|
909
|
-
{ packages: ["vue-easy-lightbox", "vue-image-zoomer", "medium-zoom", "viewerjs", "v-viewer"], replacement: "the `vZoomImg` / `vFullscreenImg` directives or MazGallery", severity: "warning", rule: "prefer-ecosystem" },
|
|
910
|
-
{ packages: ["aos"], replacement: "the maz-ui AOS plugin (`useAos`)", severity: "warning", rule: "prefer-ecosystem" },
|
|
911
|
-
{ packages: ["vee-validate", "@vuelidate/core", "vuelidate"], replacement: "`useFormValidator` / `useFormField` with your Standard Schema library (Zod, Valibot, ArkType)", severity: "info", rule: "prefer-ecosystem" }
|
|
912
|
-
];
|
|
913
|
-
const IMPORT_REGEX = /import\s+(type\s+)?(?:([\w$]+)\s*(?:,\s*)?)?(?:\{([^}]*)\}|\*\s+as\s+[\w$]+)?\s*from\s*['"]([^'"]+)['"]|import\s*['"]([^'"]+)['"]/g;
|
|
914
|
-
function parseImports(code) {
|
|
915
|
-
return [...code.matchAll(IMPORT_REGEX)].map((match) => {
|
|
916
|
-
const [, typeKeyword, defaultName, named, specifier, sideEffect] = match;
|
|
917
|
-
const names = (named ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry && !entry.startsWith("type ")).map((entry) => entry.split(/\s+as\s+/)[0].trim());
|
|
918
|
-
return {
|
|
919
|
-
specifier: specifier ?? sideEffect,
|
|
920
|
-
names: defaultName ? [defaultName, ...names] : names,
|
|
921
|
-
typeOnly: !!typeKeyword,
|
|
922
|
-
offset: match.index ?? 0
|
|
923
|
-
};
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
function matchesPackage(specifier, packages) {
|
|
927
|
-
return packages.some((name) => name.endsWith("/") ? specifier.startsWith(name) : specifier === name || specifier.startsWith(`${name}/`));
|
|
928
|
-
}
|
|
929
|
-
function checkEntryPoint(entry, offset, context) {
|
|
930
|
-
if (/^(?:maz-ui|@maz-ui\/[\w-]+)\/(?:src|dist)(?:\/|$)/.test(entry.specifier)) {
|
|
931
|
-
context.report({
|
|
932
|
-
rule: "internal-import",
|
|
933
|
-
severity: "error",
|
|
934
|
-
message: `\`${entry.specifier}\` imports internal files of the package.`,
|
|
935
|
-
suggestion: "Use the public entries: `maz-ui/components/MazBtn`, `maz-ui/composables/useToast`, `@maz-ui/utils/helpers/debounce`..."
|
|
936
|
-
}, offset);
|
|
937
|
-
return true;
|
|
938
|
-
}
|
|
939
|
-
if (entry.specifier === "maz-ui" && !entry.typeOnly && entry.names.length > 0) {
|
|
940
|
-
context.report({
|
|
941
|
-
rule: "root-import",
|
|
942
|
-
severity: "error",
|
|
943
|
-
message: "`maz-ui` (root entry) only exports types: runtime imports from it are undefined.",
|
|
944
|
-
suggestion: "Import from the entries: `maz-ui/components/MazBtn`, `maz-ui/composables/useToast`, `maz-ui/directives/vTooltip`, `maz-ui/plugins/maz-ui`. Use `import type` for types."
|
|
945
|
-
}, offset);
|
|
946
|
-
return true;
|
|
947
|
-
}
|
|
948
|
-
return false;
|
|
949
|
-
}
|
|
950
|
-
function checkComponentImport(entry, offset, context) {
|
|
951
|
-
const name = /^maz-ui\/components\/(Maz\w+)$/.exec(entry.specifier)?.[1];
|
|
952
|
-
if (!name || context.knowledge.componentsByName.has(name))
|
|
953
|
-
return;
|
|
954
|
-
const near = closest(name, context.knowledge.componentsByName.keys(), { max: 3, limit: 2 });
|
|
955
|
-
context.report({
|
|
956
|
-
rule: "unknown-component",
|
|
957
|
-
severity: "error",
|
|
958
|
-
message: `\`${name}\` is not a maz-ui component.`,
|
|
959
|
-
suggestion: near.length > 0 ? `Did you mean ${near.map((candidate) => `\`${candidate}\``).join(" or ")}?` : "Find the right one with `search_docs`."
|
|
960
|
-
}, offset);
|
|
961
|
-
}
|
|
962
|
-
function checkIconImport(entry, offset, context) {
|
|
963
|
-
if (entry.specifier !== "@maz-ui/icons" && !entry.specifier.startsWith("@maz-ui/icons/"))
|
|
964
|
-
return;
|
|
965
|
-
const fromPath = /^@maz-ui\/icons\/(?:static|lazy|raw)\/(\w+)$/.exec(entry.specifier)?.[1];
|
|
966
|
-
const icons = [...entry.names, ...fromPath ? [fromPath] : []].map((name) => name.replace(/^Lazy(?=Maz)/, "")).filter((name) => name.startsWith("Maz") && !context.knowledge.iconNames.has(name));
|
|
967
|
-
for (const icon of new Set(icons)) {
|
|
968
|
-
const near = closest(icon, context.knowledge.iconNames, { max: 3, limit: 3 });
|
|
969
|
-
context.report({
|
|
970
|
-
rule: "unknown-icon",
|
|
971
|
-
severity: "error",
|
|
972
|
-
message: `\`${icon}\` does not exist in @maz-ui/icons.`,
|
|
973
|
-
suggestion: near.length > 0 ? `Did you mean ${near.map((name) => `\`${name}\``).join(" or ")}? Search with \`find_icons\`.` : 'Search with `find_icons({ query: "..." })`.'
|
|
974
|
-
}, offset);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
function isNuxtAutoImported(entry) {
|
|
978
|
-
if (/^maz-ui\/(?:components(?:\/Maz\w+)?|composables(?:\/use\w+)?)$/.test(entry.specifier))
|
|
979
|
-
return true;
|
|
980
|
-
return ["@maz-ui/themes", "@maz-ui/translations"].includes(entry.specifier) && entry.names.some((name) => ["useTheme", "useTranslations"].includes(name));
|
|
981
|
-
}
|
|
982
|
-
function checkEcosystemImport(entry, offset, context) {
|
|
983
|
-
if (checkEntryPoint(entry, offset, context))
|
|
984
|
-
return;
|
|
985
|
-
checkComponentImport(entry, offset, context);
|
|
986
|
-
checkIconImport(entry, offset, context);
|
|
987
|
-
if (context.framework === "nuxt" && !entry.typeOnly && isNuxtAutoImported(entry)) {
|
|
988
|
-
context.report({
|
|
989
|
-
rule: "nuxt-auto-import",
|
|
990
|
-
severity: "info",
|
|
991
|
-
message: `\`${entry.specifier}\` is auto-imported by @maz-ui/nuxt.`,
|
|
992
|
-
suggestion: "Remove the import (keep `import type` imports)."
|
|
993
|
-
}, offset);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
function checkThirdPartyImport(entry, offset, context) {
|
|
997
|
-
if (entry.typeOnly)
|
|
998
|
-
return;
|
|
999
|
-
for (const rule of THIRD_PARTY_RULES) {
|
|
1000
|
-
if (!matchesPackage(entry.specifier, rule.packages))
|
|
1001
|
-
continue;
|
|
1002
|
-
if (rule.names) {
|
|
1003
|
-
const lodashPath = /^lodash(?:-es)?\/(\w+)$/.exec(entry.specifier)?.[1];
|
|
1004
|
-
const flagged = [...entry.names, ...lodashPath ? [lodashPath] : []].filter((name) => rule.names?.[name]);
|
|
1005
|
-
for (const name of flagged) {
|
|
1006
|
-
context.report({
|
|
1007
|
-
rule: rule.rule,
|
|
1008
|
-
severity: rule.severity,
|
|
1009
|
-
message: `\`${name}\` from \`${entry.specifier}\` duplicates the maz-ui ecosystem.`,
|
|
1010
|
-
suggestion: `Use ${rule.names[name]}.`
|
|
1011
|
-
}, offset);
|
|
1012
|
-
}
|
|
1013
|
-
continue;
|
|
1014
|
-
}
|
|
1015
|
-
context.report({
|
|
1016
|
-
rule: rule.rule,
|
|
1017
|
-
severity: rule.severity,
|
|
1018
|
-
message: `\`${entry.specifier}\` duplicates a feature of the maz-ui ecosystem.`,
|
|
1019
|
-
suggestion: `Use ${rule.replacement} instead of adding a dependency.`
|
|
1020
|
-
}, offset);
|
|
1021
|
-
return;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
const DARK_MODE_PATTERNS = [
|
|
1025
|
-
{ regex: /classList\.(?:add|remove|toggle)\(\s*['"`]dark['"`]/g, message: "Toggles the `dark` class by hand." },
|
|
1026
|
-
{ regex: /localStorage\.(?:get|set)Item\(\s*['"`](?:theme|color-?mode|dark(?:-?mode)?)['"`]/gi, message: "Stores the color mode by hand." },
|
|
1027
|
-
{ regex: /matchMedia\(\s*['"`]\(prefers-color-scheme/g, message: "Reads `prefers-color-scheme` by hand." }
|
|
1028
|
-
];
|
|
1029
|
-
function reviewScript(code, offset, context) {
|
|
1030
|
-
for (const entry of parseImports(code)) {
|
|
1031
|
-
const position = offset + entry.offset;
|
|
1032
|
-
if (/^(?:maz-ui|@maz-ui\/)/.test(entry.specifier))
|
|
1033
|
-
checkEcosystemImport(entry, position, context);
|
|
1034
|
-
else
|
|
1035
|
-
checkThirdPartyImport(entry, position, context);
|
|
1036
|
-
}
|
|
1037
|
-
for (const { regex, message } of DARK_MODE_PATTERNS) {
|
|
1038
|
-
for (const match of code.matchAll(regex)) {
|
|
1039
|
-
context.report({
|
|
1040
|
-
rule: "manual-dark-mode",
|
|
1041
|
-
severity: "warning",
|
|
1042
|
-
message,
|
|
1043
|
-
suggestion: '`useTheme()` from `@maz-ui/themes` handles it: `toggleDarkMode()`, `setColorMode("dark" | "light" | "auto")`, `isDark`. The color mode is persisted and applied without flash.'
|
|
1044
|
-
}, offset + (match.index ?? 0));
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
checkClassStrings(code.replaceAll(/^\s*import\s[^\n]*$/gm, (line) => " ".repeat(line.length)), offset, context);
|
|
1048
|
-
}
|
|
1049
|
-
function mazUiName(imported, source) {
|
|
1050
|
-
if (!/^(?:maz-ui|@maz-ui\/icons)(?:\/|$)/.test(source))
|
|
1051
|
-
return void 0;
|
|
1052
|
-
return imported ?? source.split("/").at(-1);
|
|
1053
|
-
}
|
|
1054
|
-
function localComponents(scripts) {
|
|
1055
|
-
const components = /* @__PURE__ */ new Map();
|
|
1056
|
-
for (const [, clause, source] of scripts.matchAll(/import\s+(?!type\s)([^'";]*?)\s+from\s+["']([^"']+)["']/g)) {
|
|
1057
|
-
const named = /\{([^}]*)\}/.exec(clause)?.[1] ?? "";
|
|
1058
|
-
const defaultName = /^\s*([A-Z]\w*)/.exec(clause.replace(/\{[^}]*\}/, ""))?.[1];
|
|
1059
|
-
if (defaultName)
|
|
1060
|
-
components.set(defaultName, mazUiName(void 0, source));
|
|
1061
|
-
for (const specifier of named.split(",")) {
|
|
1062
|
-
const [imported, local = imported] = specifier.replace(/^\s*type\s+/, "").split(/\s+as\s+/).map((part) => part.trim());
|
|
1063
|
-
if (/^[A-Z]/.test(local ?? ""))
|
|
1064
|
-
components.set(local, mazUiName(imported, source));
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
return components;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
const COLOR_LITERAL = /#[\da-f]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\(/i;
|
|
1071
|
-
const COLOR_PROPERTIES = /^(?:color|background(?:-color)?|border(?:-[a-z]+)*-color|border(?:-(?:top|right|bottom|left|inline|block)(?:-(?:start|end))?)?|outline(?:-color)?|fill|stroke|box-shadow|text-shadow|caret-color|accent-color|text-decoration-color|column-rule-color)$/;
|
|
1072
|
-
function blankComments$1(css) {
|
|
1073
|
-
return css.replaceAll(/\/\*[\s\S]*?\*\//g, (match) => match.replaceAll(/[^\n]/g, " "));
|
|
1074
|
-
}
|
|
1075
|
-
const BLACK_OR_WHITE = /^#(?:f{3}|f{6}|f{8}|0{3}|0{6}|0{8})$/i;
|
|
1076
|
-
function keyframesRanges(css) {
|
|
1077
|
-
const ranges = [];
|
|
1078
|
-
for (const match of css.matchAll(/@keyframes[^{]*\{/g)) {
|
|
1079
|
-
const start = match.index ?? 0;
|
|
1080
|
-
let depth = 0;
|
|
1081
|
-
for (let index = start + match[0].length - 1; index < css.length; index++) {
|
|
1082
|
-
if (css[index] === "{") {
|
|
1083
|
-
depth++;
|
|
1084
|
-
} else if (css[index] === "}" && --depth === 0) {
|
|
1085
|
-
ranges.push([start, index]);
|
|
1086
|
-
break;
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
return ranges;
|
|
1091
|
-
}
|
|
1092
|
-
function checkStyleDeclarations(css, offset, context) {
|
|
1093
|
-
const keyframes = keyframesRanges(css);
|
|
1094
|
-
for (const match of css.matchAll(/(^|[;{\s])([a-z-]+)\s*:\s*([^;{}]+)/g)) {
|
|
1095
|
-
const [, prefix, property, value] = match;
|
|
1096
|
-
if (property.startsWith("--") || !COLOR_PROPERTIES.test(property) || /var\(--/.test(value))
|
|
1097
|
-
continue;
|
|
1098
|
-
const color = COLOR_LITERAL.exec(value);
|
|
1099
|
-
if (!color)
|
|
1100
|
-
continue;
|
|
1101
|
-
const index = match.index ?? 0;
|
|
1102
|
-
const inKeyframes = keyframes.some(([start, end]) => index > start && index < end);
|
|
1103
|
-
const blackOrWhite = BLACK_OR_WHITE.test(color[0]);
|
|
1104
|
-
context.report({
|
|
1105
|
-
rule: "hardcoded-color",
|
|
1106
|
-
severity: inKeyframes || blackOrWhite ? "info" : "warning",
|
|
1107
|
-
message: `\`${property}\` uses a hard-coded color (\`${color[0].replace("(", "")}\`): it ignores the theme and dark mode.`,
|
|
1108
|
-
suggestion: `Use a theme variable: \`var(--maz-primary)\`, \`var(--maz-surface)\`, \`var(--maz-foreground)\`, \`var(--maz-muted)\`, \`var(--maz-divider)\`... (\`color-mix(in oklab, var(--maz-primary) 20%, transparent)\` for tints).${blackOrWhite ? " Keep it if the element is over media (image, video) or a fixed dark background." : ""}`
|
|
1109
|
-
}, offset + index + prefix.length);
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
function reviewStyle(source, offset, context, { sfc = false } = {}) {
|
|
1113
|
-
const css = blankComments$1(source);
|
|
1114
|
-
const applies = [...css.matchAll(/@apply\s+([^;]+);/g)];
|
|
1115
|
-
for (const apply of applies) {
|
|
1116
|
-
checkClassList(apply[1], offset + (apply.index ?? 0) + apply[0].indexOf(apply[1]), context);
|
|
1117
|
-
}
|
|
1118
|
-
if (sfc && applies.length > 0 && !/@reference\s/.test(css)) {
|
|
1119
|
-
context.report({
|
|
1120
|
-
rule: "missing-reference",
|
|
1121
|
-
severity: "error",
|
|
1122
|
-
message: "`@apply` in a Vue SFC `<style>` block needs a `@reference` to your Tailwind entry with Tailwind v4.",
|
|
1123
|
-
suggestion: 'Start the block with `@reference "<relative path to your Tailwind entry css>";`.'
|
|
1124
|
-
}, offset + (applies[0].index ?? 0));
|
|
1125
|
-
}
|
|
1126
|
-
const withoutThemeBlocks = css.replaceAll(/@theme[^{]*\{[^}]*\}/g, (match) => match.replaceAll(/[^\n]/g, " "));
|
|
1127
|
-
checkStyleDeclarations(withoutThemeBlocks, offset, context);
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
const GLOBAL_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
1131
|
-
"id",
|
|
1132
|
-
"class",
|
|
1133
|
-
"style",
|
|
1134
|
-
"title",
|
|
1135
|
-
"role",
|
|
1136
|
-
"tabindex",
|
|
1137
|
-
"hidden",
|
|
1138
|
-
"lang",
|
|
1139
|
-
"dir",
|
|
1140
|
-
"draggable",
|
|
1141
|
-
"translate",
|
|
1142
|
-
"inert",
|
|
1143
|
-
"popover",
|
|
1144
|
-
"slot",
|
|
1145
|
-
"part",
|
|
1146
|
-
"is",
|
|
1147
|
-
"key",
|
|
1148
|
-
"ref",
|
|
1149
|
-
"name",
|
|
1150
|
-
"type",
|
|
1151
|
-
"value",
|
|
1152
|
-
"placeholder",
|
|
1153
|
-
"autocomplete",
|
|
1154
|
-
"autofocus",
|
|
1155
|
-
"autocapitalize",
|
|
1156
|
-
"autocorrect",
|
|
1157
|
-
"spellcheck",
|
|
1158
|
-
"inputmode",
|
|
1159
|
-
"enterkeyhint",
|
|
1160
|
-
"maxlength",
|
|
1161
|
-
"minlength",
|
|
1162
|
-
"max",
|
|
1163
|
-
"min",
|
|
1164
|
-
"step",
|
|
1165
|
-
"pattern",
|
|
1166
|
-
"readonly",
|
|
1167
|
-
"required",
|
|
1168
|
-
"disabled",
|
|
1169
|
-
"multiple",
|
|
1170
|
-
"accept",
|
|
1171
|
-
"capture",
|
|
1172
|
-
"form",
|
|
1173
|
-
"list",
|
|
1174
|
-
"size",
|
|
1175
|
-
"rows",
|
|
1176
|
-
"cols",
|
|
1177
|
-
"wrap",
|
|
1178
|
-
"checked",
|
|
1179
|
-
"selected",
|
|
1180
|
-
"href",
|
|
1181
|
-
"target",
|
|
1182
|
-
"rel",
|
|
1183
|
-
"download",
|
|
1184
|
-
"src",
|
|
1185
|
-
"srcset",
|
|
1186
|
-
"sizes",
|
|
1187
|
-
"alt",
|
|
1188
|
-
"width",
|
|
1189
|
-
"height",
|
|
1190
|
-
"loading",
|
|
1191
|
-
"decoding",
|
|
1192
|
-
"for",
|
|
1193
|
-
"label",
|
|
1194
|
-
"contenteditable",
|
|
1195
|
-
"accesskey",
|
|
1196
|
-
"nonce"
|
|
1197
|
-
]);
|
|
1198
|
-
const NATIVE_EVENTS = /* @__PURE__ */ new Set([
|
|
1199
|
-
"click",
|
|
1200
|
-
"dblclick",
|
|
1201
|
-
"contextmenu",
|
|
1202
|
-
"mousedown",
|
|
1203
|
-
"mouseup",
|
|
1204
|
-
"mouseenter",
|
|
1205
|
-
"mouseleave",
|
|
1206
|
-
"mouseover",
|
|
1207
|
-
"mouseout",
|
|
1208
|
-
"mousemove",
|
|
1209
|
-
"pointerdown",
|
|
1210
|
-
"pointerup",
|
|
1211
|
-
"pointerenter",
|
|
1212
|
-
"pointerleave",
|
|
1213
|
-
"pointermove",
|
|
1214
|
-
"pointercancel",
|
|
1215
|
-
"touchstart",
|
|
1216
|
-
"touchend",
|
|
1217
|
-
"touchmove",
|
|
1218
|
-
"touchcancel",
|
|
1219
|
-
"keydown",
|
|
1220
|
-
"keyup",
|
|
1221
|
-
"keypress",
|
|
1222
|
-
"focus",
|
|
1223
|
-
"blur",
|
|
1224
|
-
"focusin",
|
|
1225
|
-
"focusout",
|
|
1226
|
-
"input",
|
|
1227
|
-
"change",
|
|
1228
|
-
"submit",
|
|
1229
|
-
"reset",
|
|
1230
|
-
"invalid",
|
|
1231
|
-
"scroll",
|
|
1232
|
-
"wheel",
|
|
1233
|
-
"resize",
|
|
1234
|
-
"drag",
|
|
1235
|
-
"dragstart",
|
|
1236
|
-
"dragend",
|
|
1237
|
-
"dragenter",
|
|
1238
|
-
"dragleave",
|
|
1239
|
-
"dragover",
|
|
1240
|
-
"drop",
|
|
1241
|
-
"copy",
|
|
1242
|
-
"cut",
|
|
1243
|
-
"paste",
|
|
1244
|
-
"load",
|
|
1245
|
-
"error",
|
|
1246
|
-
"animationstart",
|
|
1247
|
-
"animationend",
|
|
1248
|
-
"transitionend",
|
|
1249
|
-
"select"
|
|
1250
|
-
]);
|
|
1251
|
-
function isPassthroughAttribute(name) {
|
|
1252
|
-
return GLOBAL_ATTRIBUTES.has(name) || name.startsWith("data-") || name.startsWith("aria-") || name.startsWith("on");
|
|
1253
|
-
}
|
|
1254
|
-
function nativeReplacement(tag, type) {
|
|
1255
|
-
switch (tag) {
|
|
1256
|
-
case "button":
|
|
1257
|
-
return { component: "MazBtn", severity: "warning" };
|
|
1258
|
-
case "select":
|
|
1259
|
-
return { component: "MazSelect", severity: "warning" };
|
|
1260
|
-
case "textarea":
|
|
1261
|
-
return { component: "MazTextarea", severity: "warning" };
|
|
1262
|
-
case "dialog":
|
|
1263
|
-
return { component: "MazDialog", severity: "warning" };
|
|
1264
|
-
case "table":
|
|
1265
|
-
return { component: "MazTable", severity: "info" };
|
|
1266
|
-
case "progress":
|
|
1267
|
-
return { component: "MazLoadingBar or MazCircularProgressBar", severity: "info" };
|
|
1268
|
-
case "details":
|
|
1269
|
-
return { component: "MazAccordion or MazExpandAnimation", severity: "info" };
|
|
1270
|
-
case "input":
|
|
1271
|
-
return inputReplacement(type);
|
|
1272
|
-
default:
|
|
1273
|
-
return void 0;
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
function inputReplacement(type = "text") {
|
|
1277
|
-
switch (type) {
|
|
1278
|
-
case "hidden":
|
|
1279
|
-
case "submit":
|
|
1280
|
-
case "reset":
|
|
1281
|
-
case "image":
|
|
1282
|
-
case "color":
|
|
1283
|
-
return void 0;
|
|
1284
|
-
case "checkbox":
|
|
1285
|
-
return { component: "MazCheckbox (or MazSwitch for a toggle)", severity: "warning" };
|
|
1286
|
-
case "radio":
|
|
1287
|
-
return { component: "MazRadio or MazRadioButtons", severity: "warning" };
|
|
1288
|
-
case "range":
|
|
1289
|
-
return { component: "MazSlider", severity: "warning" };
|
|
1290
|
-
case "file":
|
|
1291
|
-
return { component: "MazDropzone", severity: "info" };
|
|
1292
|
-
case "tel":
|
|
1293
|
-
return { component: "MazInputPhoneNumber", severity: "warning" };
|
|
1294
|
-
case "number":
|
|
1295
|
-
return { component: "MazInputNumber (or MazInputPrice for amounts)", severity: "warning" };
|
|
1296
|
-
case "date":
|
|
1297
|
-
case "datetime-local":
|
|
1298
|
-
case "month":
|
|
1299
|
-
case "week":
|
|
1300
|
-
case "time":
|
|
1301
|
-
return { component: "MazDatePicker", severity: "warning" };
|
|
1302
|
-
default:
|
|
1303
|
-
return { component: "MazInput", severity: "warning" };
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
function toCamel(value) {
|
|
1308
|
-
return value.replaceAll(/-([a-z\d])/g, (_, char) => char.toUpperCase());
|
|
1309
|
-
}
|
|
1310
|
-
function toPascal(tag) {
|
|
1311
|
-
const camel = toCamel(tag);
|
|
1312
|
-
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
1313
|
-
}
|
|
1314
|
-
function isElement(node) {
|
|
1315
|
-
return node.type === NodeTypes.ELEMENT;
|
|
1316
|
-
}
|
|
1317
|
-
function staticAttribute(element, name) {
|
|
1318
|
-
return element.props.find((prop) => prop.type === NodeTypes.ATTRIBUTE && prop.name === name);
|
|
1319
|
-
}
|
|
1320
|
-
function directiveArg(directive) {
|
|
1321
|
-
return directive.arg?.type === NodeTypes.SIMPLE_EXPRESSION && directive.arg.isStatic ? directive.arg.content : void 0;
|
|
1322
|
-
}
|
|
1323
|
-
function directiveExpression(directive) {
|
|
1324
|
-
return directive.exp?.type === NodeTypes.SIMPLE_EXPRESSION ? { content: directive.exp.content, offset: directive.exp.loc.start.offset } : void 0;
|
|
1325
|
-
}
|
|
1326
|
-
function valueOffset(attribute, source) {
|
|
1327
|
-
const start = attribute.value?.loc.start.offset ?? attribute.loc.start.offset;
|
|
1328
|
-
return /["']/.test(source[start] ?? "") ? start + 1 : start;
|
|
1329
|
-
}
|
|
1330
|
-
function findProp(component, name) {
|
|
1331
|
-
const camel = toCamel(name);
|
|
1332
|
-
return component.props.find((prop) => prop.name === camel);
|
|
1333
|
-
}
|
|
1334
|
-
function mazTagName(tag) {
|
|
1335
|
-
const pascal = toPascal(tag).replace(/^Lazy(?=Maz)/, "");
|
|
1336
|
-
return /^Maz[A-Z\d]/.test(pascal) ? pascal : void 0;
|
|
1337
|
-
}
|
|
1338
|
-
function checkPropValue(prop, value, offset, component, context) {
|
|
1339
|
-
if (prop.type === "boolean" && (value === "false" || value === "true")) {
|
|
1340
|
-
context.report({
|
|
1341
|
-
rule: "boolean-prop-string",
|
|
1342
|
-
severity: value === "false" ? "error" : "info",
|
|
1343
|
-
message: `\`${toKebab(prop.name)}="${value}"\` on ${component.name} passes the string "${value}"${value === "false" ? ", which is truthy" : ""}.`,
|
|
1344
|
-
suggestion: value === "false" ? `Remove the attribute or bind it: \`:${toKebab(prop.name)}="false"\`.` : `Write \`${toKebab(prop.name)}\` alone.`
|
|
1345
|
-
}, offset);
|
|
1346
|
-
return;
|
|
1347
|
-
}
|
|
1348
|
-
if (prop.strictValues && prop.values && !prop.values.includes(value)) {
|
|
1349
|
-
const near = closest(value, prop.values, { max: 2, limit: 1 });
|
|
1350
|
-
context.report({
|
|
1351
|
-
rule: "invalid-prop-value",
|
|
1352
|
-
severity: "error",
|
|
1353
|
-
message: `"${value}" is not a valid \`${toKebab(prop.name)}\` for ${component.name}.`,
|
|
1354
|
-
suggestion: `Accepted values: ${prop.values.map((entry) => `"${entry}"`).join(", ")}.${near[0] ? ` Did you mean "${near[0]}"?` : ""}`
|
|
1355
|
-
}, offset);
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
const PROP_SYNONYMS = {
|
|
1359
|
-
items: ["options"],
|
|
1360
|
-
choices: ["options"],
|
|
1361
|
-
list: ["options"],
|
|
1362
|
-
data: ["options", "rows"],
|
|
1363
|
-
columns: ["headers"],
|
|
1364
|
-
value: ["modelValue"],
|
|
1365
|
-
checked: ["modelValue"],
|
|
1366
|
-
selected: ["modelValue"],
|
|
1367
|
-
open: ["modelValue"],
|
|
1368
|
-
isOpen: ["modelValue"],
|
|
1369
|
-
show: ["modelValue"],
|
|
1370
|
-
visible: ["modelValue"],
|
|
1371
|
-
active: ["modelValue"],
|
|
1372
|
-
text: ["label", "title", "content"],
|
|
1373
|
-
helperText: ["hint"],
|
|
1374
|
-
helper: ["hint"],
|
|
1375
|
-
message: ["hint"],
|
|
1376
|
-
errorMessage: ["hint", "error"],
|
|
1377
|
-
variant: ["color", "fill", "outlined"],
|
|
1378
|
-
image: ["src"],
|
|
1379
|
-
leftIcon: ["startIcon"],
|
|
1380
|
-
rightIcon: ["endIcon"],
|
|
1381
|
-
prependIcon: ["startIcon"],
|
|
1382
|
-
appendIcon: ["endIcon"]
|
|
1383
|
-
};
|
|
1384
|
-
function nameWords(name) {
|
|
1385
|
-
return toKebab(name).split("-").filter(Boolean);
|
|
1386
|
-
}
|
|
1387
|
-
function propSuggestions(name, component, { containing = true } = {}) {
|
|
1388
|
-
const camel = toCamel(name);
|
|
1389
|
-
const props = component.props.map((prop) => prop.name);
|
|
1390
|
-
const synonyms = (PROP_SYNONYMS[camel] ?? []).filter((prop) => props.includes(prop));
|
|
1391
|
-
const words = nameWords(camel.replace(/^(?:is|has)(?=[A-Z])/, ""));
|
|
1392
|
-
const withWords = containing ? props.filter((prop) => words.every((word) => nameWords(prop).includes(word))).sort((a, b) => a.length - b.length) : [];
|
|
1393
|
-
const near = closest(camel, props, { max: camel.length <= 3 ? 1 : 2, limit: 2 });
|
|
1394
|
-
return [.../* @__PURE__ */ new Set([...synonyms, ...withWords, ...near])].slice(0, 3);
|
|
1395
|
-
}
|
|
1396
|
-
function reportUnknownProp(name, offset, component, context) {
|
|
1397
|
-
const passthrough = isPassthroughAttribute(name);
|
|
1398
|
-
const near = propSuggestions(name, component, { containing: !passthrough });
|
|
1399
|
-
if (passthrough && near.length === 0)
|
|
1400
|
-
return;
|
|
1401
|
-
const names = near.map((entry) => entry === "modelValue" ? "`v-model`" : `\`${toKebab(entry)}\``).join(" or ");
|
|
1402
|
-
context.report({
|
|
1403
|
-
rule: "unknown-prop",
|
|
1404
|
-
severity: "warning",
|
|
1405
|
-
message: passthrough ? `\`${name}\` is not a prop of ${component.name}: it is only forwarded as an HTML attribute (the component may not use it).` : `${component.name} has no prop \`${name}\`: it will only be forwarded as an HTML attribute.`,
|
|
1406
|
-
suggestion: near.length > 0 ? `Did you mean ${names}? Check \`get_component_api({ names: ["${component.name}"] })\`.` : `Check the real API with \`get_component_api({ names: ["${component.name}"] })\`.`
|
|
1407
|
-
}, offset);
|
|
1408
|
-
}
|
|
1409
|
-
function checkModel(directive, component, context) {
|
|
1410
|
-
const arg = directiveArg(directive) ?? "modelValue";
|
|
1411
|
-
const prop = findProp(component, arg);
|
|
1412
|
-
if (!prop) {
|
|
1413
|
-
const models = component.props.filter((entry) => entry.model).map((entry) => entry.name === "modelValue" ? "v-model" : `v-model:${toKebab(entry.name)}`);
|
|
1414
|
-
context.report({
|
|
1415
|
-
rule: "unknown-model",
|
|
1416
|
-
severity: "error",
|
|
1417
|
-
message: `${component.name} has no \`${arg === "modelValue" ? "v-model" : `v-model:${arg}`}\`.`,
|
|
1418
|
-
suggestion: models.length > 0 ? `Available: ${models.map((entry) => `\`${entry}\``).join(", ")}.` : `${component.name} has no v-model.`
|
|
1419
|
-
}, directive.loc.start.offset);
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
function checkEvent(directive, component, context) {
|
|
1423
|
-
const name = directiveArg(directive);
|
|
1424
|
-
if (!name)
|
|
1425
|
-
return;
|
|
1426
|
-
const kebab = toKebab(name);
|
|
1427
|
-
const events = component.events.map((event) => event.name);
|
|
1428
|
-
if (events.includes(kebab) || events.includes(name) || NATIVE_EVENTS.has(kebab.split(".")[0]))
|
|
1429
|
-
return;
|
|
1430
|
-
const updateMatch = /^update:(.+)$/.exec(kebab);
|
|
1431
|
-
if (updateMatch) {
|
|
1432
|
-
if (!findProp(component, updateMatch[1])) {
|
|
1433
|
-
context.report({
|
|
1434
|
-
rule: "unknown-event",
|
|
1435
|
-
severity: "error",
|
|
1436
|
-
message: `${component.name} never emits \`${name}\`: it has no \`${toCamel(updateMatch[1])}\` prop.`,
|
|
1437
|
-
suggestion: `Events: ${events.map((event) => `\`@${event}\``).join(", ") || "none"}.`
|
|
1438
|
-
}, directive.loc.start.offset);
|
|
1439
|
-
}
|
|
1440
|
-
return;
|
|
1441
|
-
}
|
|
1442
|
-
const near = closest(kebab, [...events, ...NATIVE_EVENTS], { max: 2, limit: 1 });
|
|
1443
|
-
context.report({
|
|
1444
|
-
rule: "unknown-event",
|
|
1445
|
-
severity: near.length > 0 ? "error" : "warning",
|
|
1446
|
-
message: `${component.name} does not emit \`${name}\` and it is not a DOM event.`,
|
|
1447
|
-
suggestion: near.length > 0 ? `Did you mean \`@${near[0]}\`?` : `Events: ${events.map((event) => `\`@${event}\``).join(", ") || "none (DOM events like @click still work)"}.`
|
|
1448
|
-
}, directive.loc.start.offset);
|
|
1449
|
-
}
|
|
1450
|
-
function isStaticSlot(prop) {
|
|
1451
|
-
return prop.type === NodeTypes.DIRECTIVE && prop.name === "slot" && (!prop.arg || prop.arg.type !== NodeTypes.SIMPLE_EXPRESSION || prop.arg.isStatic);
|
|
1452
|
-
}
|
|
1453
|
-
function usedSlots(element) {
|
|
1454
|
-
const templates = element.children.filter((child) => isElement(child) && child.tag === "template");
|
|
1455
|
-
return [element, ...templates].flatMap((node) => node.props.filter(isStaticSlot)).map((prop) => ({ name: directiveArg(prop) ?? "default", offset: prop.loc.start.offset }));
|
|
1456
|
-
}
|
|
1457
|
-
function slotMatches(slot, name) {
|
|
1458
|
-
if (!slot.includes("{"))
|
|
1459
|
-
return slot === name;
|
|
1460
|
-
const [prefix, suffix] = slot.split(/\{[^}]*\}/);
|
|
1461
|
-
return name.length > prefix.length + suffix.length && name.startsWith(prefix) && name.endsWith(suffix);
|
|
1462
|
-
}
|
|
1463
|
-
function checkSlots(element, component, context) {
|
|
1464
|
-
const slots = component.slots.map((slot) => slot.name);
|
|
1465
|
-
for (const { name, offset } of usedSlots(element)) {
|
|
1466
|
-
if (slots.some((slot) => slotMatches(slot, name)) || name === "default" && slots.length === 0)
|
|
1467
|
-
continue;
|
|
1468
|
-
const near = closest(name, slots, { max: 3, limit: 2 });
|
|
1469
|
-
context.report({
|
|
1470
|
-
rule: "unknown-slot",
|
|
1471
|
-
severity: "error",
|
|
1472
|
-
message: `${component.name} has no \`#${name}\` slot: its content will not be rendered.`,
|
|
1473
|
-
suggestion: near.length > 0 ? `Did you mean ${near.map((entry) => `\`#${entry}\``).join(" or ")}?` : `Slots: ${slots.map((slot) => `\`#${slot}\``).join(", ") || "none"}.`
|
|
1474
|
-
}, offset);
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
const STYLE_BINDINGS = /* @__PURE__ */ new Set(["class", "style", "key"]);
|
|
1478
|
-
function checkStaticAttribute(attribute, component, source, context) {
|
|
1479
|
-
if (STYLE_BINDINGS.has(attribute.name))
|
|
1480
|
-
return;
|
|
1481
|
-
const definition = findProp(component, attribute.name);
|
|
1482
|
-
if (!definition)
|
|
1483
|
-
reportUnknownProp(attribute.name, attribute.loc.start.offset, component, context);
|
|
1484
|
-
else if (attribute.value)
|
|
1485
|
-
checkPropValue(definition, attribute.value.content, valueOffset(attribute, source), component, context);
|
|
1486
|
-
}
|
|
1487
|
-
function checkBoundProp(directive, component, context) {
|
|
1488
|
-
const name = directiveArg(directive);
|
|
1489
|
-
if (!name || STYLE_BINDINGS.has(name))
|
|
1490
|
-
return;
|
|
1491
|
-
const definition = findProp(component, name);
|
|
1492
|
-
if (!definition) {
|
|
1493
|
-
reportUnknownProp(name, directive.loc.start.offset, component, context);
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
const expression = directiveExpression(directive);
|
|
1497
|
-
const literal = expression?.content.trim();
|
|
1498
|
-
if (expression && literal && /^'[^']*'$|^"[^"]*"$/.test(literal) && definition.type !== "boolean")
|
|
1499
|
-
checkPropValue(definition, literal.slice(1, -1), expression.offset, component, context);
|
|
1500
|
-
}
|
|
1501
|
-
function checkMazComponent(element, component, source, context) {
|
|
1502
|
-
for (const prop of element.props) {
|
|
1503
|
-
if (prop.type === NodeTypes.ATTRIBUTE)
|
|
1504
|
-
checkStaticAttribute(prop, component, source, context);
|
|
1505
|
-
else if (prop.name === "bind")
|
|
1506
|
-
checkBoundProp(prop, component, context);
|
|
1507
|
-
else if (prop.name === "model")
|
|
1508
|
-
checkModel(prop, component, context);
|
|
1509
|
-
else if (prop.name === "on")
|
|
1510
|
-
checkEvent(prop, component, context);
|
|
1511
|
-
}
|
|
1512
|
-
checkSlots(element, component, context);
|
|
1513
|
-
}
|
|
1514
|
-
const SVG_SHAPES = /* @__PURE__ */ new Set(["path", "circle", "rect", "polyline", "polygon", "line", "ellipse"]);
|
|
1515
|
-
const ICON_MAX_VIEWBOX = 32;
|
|
1516
|
-
function descendantsOf(element) {
|
|
1517
|
-
return element.children.filter(isElement).flatMap((child) => [child, ...descendantsOf(child)]);
|
|
1518
|
-
}
|
|
1519
|
-
function isAnimated(element) {
|
|
1520
|
-
const classes = staticAttribute(element, "class")?.value?.content ?? "";
|
|
1521
|
-
return element.tag.startsWith("animate") || /\banimate-|\[animation/.test(classes) || element.props.some((prop) => prop.name.startsWith("stroke-dash"));
|
|
1522
|
-
}
|
|
1523
|
-
function isIconLikeSvg(svg) {
|
|
1524
|
-
const descendants = descendantsOf(svg);
|
|
1525
|
-
if (!descendants.some((child) => SVG_SHAPES.has(child.tag)))
|
|
1526
|
-
return false;
|
|
1527
|
-
const viewBox = staticAttribute(svg, "viewBox")?.value?.content.trim().split(/[\s,]+/).map(Number);
|
|
1528
|
-
if (viewBox?.length === 4 && (viewBox[2] > ICON_MAX_VIEWBOX || viewBox[3] > ICON_MAX_VIEWBOX))
|
|
1529
|
-
return false;
|
|
1530
|
-
return ![svg, ...descendants].some((node) => isAnimated(node) || node !== svg && node.props.some((prop) => prop.type === NodeTypes.DIRECTIVE));
|
|
1531
|
-
}
|
|
1532
|
-
const LAYOUT_TAGS = /* @__PURE__ */ new Set(["div", "p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "dl", "img", "picture", "video", "canvas", "section", "article", "header", "footer", "figure", "table"]);
|
|
1533
|
-
const NEUTRAL_COMPONENTS = /* @__PURE__ */ new Set(["Transition", "TransitionGroup", "KeepAlive", "Teleport", "Suspense", "ClientOnly", "NuxtLink", "RouterLink"]);
|
|
1534
|
-
function contentOf(element) {
|
|
1535
|
-
return element.children.filter((child) => child.type !== NodeTypes.COMMENT && !(child.type === NodeTypes.TEXT && child.content.trim() === ""));
|
|
1536
|
-
}
|
|
1537
|
-
function isIconOrDecoration(element, context) {
|
|
1538
|
-
const pascal = toPascal(element.tag).replace(/^Lazy/, "");
|
|
1539
|
-
return element.tag === "svg" || element.tag === "component" || context.knowledge.iconNames.has(pascal) || pascal.endsWith("Icon") || ["i", "span"].includes(element.tag) && contentOf(element).length === 0;
|
|
1540
|
-
}
|
|
1541
|
-
function isComponentTag(element) {
|
|
1542
|
-
return /^[A-Z]/.test(element.tag) || element.tag.includes("-");
|
|
1543
|
-
}
|
|
1544
|
-
function isCompositeButton(button, context) {
|
|
1545
|
-
const descendants = descendantsOf(button);
|
|
1546
|
-
if (descendants.some((node) => LAYOUT_TAGS.has(node.tag)))
|
|
1547
|
-
return true;
|
|
1548
|
-
if (descendants.some((node) => isComponentTag(node) && !NEUTRAL_COMPONENTS.has(toPascal(node.tag)) && !isIconOrDecoration(node, context)))
|
|
1549
|
-
return true;
|
|
1550
|
-
return hasStackedLabels(button, context);
|
|
1551
|
-
}
|
|
1552
|
-
function hasStackedLabels(element, context) {
|
|
1553
|
-
const content = contentOf(element);
|
|
1554
|
-
const labels = content.filter((child) => isElement(child) && !isIconOrDecoration(child, context));
|
|
1555
|
-
const isSentence = content.some((child) => !isElement(child));
|
|
1556
|
-
return labels.length >= 2 && !isSentence || labels.some((label) => hasStackedLabels(label, context));
|
|
1557
|
-
}
|
|
1558
|
-
function checkNativeElement(element, context) {
|
|
1559
|
-
if (element.tag === "svg" && isIconLikeSvg(element)) {
|
|
1560
|
-
context.report({
|
|
1561
|
-
rule: "inline-svg",
|
|
1562
|
-
severity: "warning",
|
|
1563
|
-
message: "Inline SVG icon.",
|
|
1564
|
-
suggestion: 'Use an icon from @maz-ui/icons: `find_icons({ query: "..." })`.'
|
|
1565
|
-
}, element.loc.start.offset);
|
|
1566
|
-
return;
|
|
1567
|
-
}
|
|
1568
|
-
if (element.tag === "button" && (contentOf(element).length === 0 || isCompositeButton(element, context)))
|
|
1569
|
-
return;
|
|
1570
|
-
const replacement = nativeReplacement(element.tag, staticAttribute(element, "type")?.value?.content);
|
|
1571
|
-
if (replacement) {
|
|
1572
|
-
context.report({
|
|
1573
|
-
rule: "prefer-maz-component",
|
|
1574
|
-
severity: replacement.severity,
|
|
1575
|
-
message: `Native \`<${element.tag}>\`${element.tag === "input" ? ` (type "${staticAttribute(element, "type")?.value?.content ?? "text"}")` : ""}.`,
|
|
1576
|
-
suggestion: `Use ${replacement.component}: themed, accessible and consistent with the rest of the UI. Add \`<!-- maz-ui-review-ignore -->\` above it if the native element is intended.`
|
|
1577
|
-
}, element.loc.start.offset);
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
function checkClassBindings(element, source, context, inherited) {
|
|
1581
|
-
for (const prop of element.props) {
|
|
1582
|
-
if (prop.type === NodeTypes.ATTRIBUTE && prop.name === "class" && prop.value) {
|
|
1583
|
-
checkClassList(prop.value.content, valueOffset(prop, source), context, inherited);
|
|
1584
|
-
} else if (prop.type === NodeTypes.ATTRIBUTE && prop.name === "style" && prop.value) {
|
|
1585
|
-
checkStyleDeclarations(prop.value.content, valueOffset(prop, source), context);
|
|
1586
|
-
} else if (prop.type === NodeTypes.DIRECTIVE && prop.name === "bind" && directiveArg(prop) === "class") {
|
|
1587
|
-
const expression = directiveExpression(prop);
|
|
1588
|
-
if (expression)
|
|
1589
|
-
checkClassStrings(expression.content, expression.offset, context);
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
function checkMazTag(element, mazName, source, context) {
|
|
1594
|
-
const component = context.knowledge.componentsByName.get(mazName);
|
|
1595
|
-
if (component) {
|
|
1596
|
-
checkMazComponent(element, component, source, context);
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
if (context.knowledge.iconNames.has(mazName))
|
|
1600
|
-
return;
|
|
1601
|
-
const near = closest(mazName, [...context.knowledge.componentsByName.keys(), ...context.knowledge.iconNames], { max: 3, limit: 3 });
|
|
1602
|
-
context.report({
|
|
1603
|
-
rule: "unknown-component",
|
|
1604
|
-
severity: "error",
|
|
1605
|
-
message: `\`<${element.tag}>\` is neither a maz-ui component nor a @maz-ui/icons icon.`,
|
|
1606
|
-
suggestion: near.length > 0 ? `Did you mean ${near.map((entry) => `\`${entry}\``).join(" or ")}?` : "Use `search_docs` or `find_icons` to find the right name."
|
|
1607
|
-
}, element.loc.start.offset);
|
|
1608
|
-
}
|
|
1609
|
-
function walk(node, source, context, background) {
|
|
1610
|
-
for (const child of node.children) {
|
|
1611
|
-
if (!isElement(child))
|
|
1612
|
-
continue;
|
|
1613
|
-
const pascal = /[A-Z-]/.test(child.tag) ? toPascal(child.tag) : void 0;
|
|
1614
|
-
const mazName = pascal && context.localComponents?.has(pascal) ? mazTagName(context.localComponents.get(pascal) ?? "") : mazTagName(child.tag);
|
|
1615
|
-
if (mazName)
|
|
1616
|
-
checkMazTag(child, mazName, source, context);
|
|
1617
|
-
else
|
|
1618
|
-
checkNativeElement(child, context);
|
|
1619
|
-
checkClassBindings(child, source, context, background);
|
|
1620
|
-
walk(child, source, context, backgroundOf(staticAttribute(child, "class")?.value?.content ?? "") ?? background);
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
function reviewTemplate(root, source, context) {
|
|
1624
|
-
walk(root, source, context);
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
|
|
1628
|
-
|
|
1629
|
-
function detectKind(code, filename) {
|
|
1630
|
-
const extension = filename?.split(".").pop()?.toLowerCase();
|
|
1631
|
-
if (extension === "vue")
|
|
1632
|
-
return "vue";
|
|
1633
|
-
if (["ts", "tsx", "js", "jsx", "mjs", "mts", "cjs", "cts"].includes(extension ?? ""))
|
|
1634
|
-
return "script";
|
|
1635
|
-
if (["css", "scss", "pcss", "postcss", "sass", "less"].includes(extension ?? ""))
|
|
1636
|
-
return "style";
|
|
1637
|
-
if (extension === "html")
|
|
1638
|
-
return "template";
|
|
1639
|
-
if (/<template[\s>]|<script[\s>]/.test(code))
|
|
1640
|
-
return "vue";
|
|
1641
|
-
if (/^\s*</.test(code))
|
|
1642
|
-
return "template";
|
|
1643
|
-
if (/^\s*(?:@import|@theme|[.#:@\w-][^{;]*\{)/.test(code) && !/(?:^|[^@\w])(?:import|const|function|export)\s/.test(code))
|
|
1644
|
-
return "style";
|
|
1645
|
-
return "script";
|
|
1646
|
-
}
|
|
1647
|
-
function lineStarts(code) {
|
|
1648
|
-
const starts = [0];
|
|
1649
|
-
for (let index = 0; index < code.length; index++) {
|
|
1650
|
-
if (code[index] === "\n")
|
|
1651
|
-
starts.push(index + 1);
|
|
1652
|
-
}
|
|
1653
|
-
return starts;
|
|
1654
|
-
}
|
|
1655
|
-
function locate(starts, offset) {
|
|
1656
|
-
let low = 0;
|
|
1657
|
-
let high = starts.length - 1;
|
|
1658
|
-
while (low < high) {
|
|
1659
|
-
const middle = Math.ceil((low + high) / 2);
|
|
1660
|
-
if (starts[middle] <= offset)
|
|
1661
|
-
low = middle;
|
|
1662
|
-
else
|
|
1663
|
-
high = middle - 1;
|
|
1664
|
-
}
|
|
1665
|
-
return { line: low + 1, column: offset - starts[low] + 1 };
|
|
1666
|
-
}
|
|
1667
|
-
function ignoredRules(lines) {
|
|
1668
|
-
const byLine = /* @__PURE__ */ new Map();
|
|
1669
|
-
let file;
|
|
1670
|
-
const parseRules = (rest) => {
|
|
1671
|
-
const [directive] = rest.replace(/-->|\*\/|\*\}|\}/g, "").split(/\s--\s/);
|
|
1672
|
-
const rules = directive.split(/[\s,]+/).filter((rule) => /^[a-z][a-z\d-]+$/.test(rule));
|
|
1673
|
-
return rules.length > 0 ? new Set(rules) : "all";
|
|
1674
|
-
};
|
|
1675
|
-
for (const [index, line] of lines.entries()) {
|
|
1676
|
-
const fileMatch = /maz-ui-review-ignore-file\b(.*)/.exec(line);
|
|
1677
|
-
if (fileMatch) {
|
|
1678
|
-
file = parseRules(fileMatch[1]);
|
|
1679
|
-
continue;
|
|
1680
|
-
}
|
|
1681
|
-
const match = /maz-ui-review-ignore\b(.*)/.exec(line);
|
|
1682
|
-
if (!match)
|
|
1683
|
-
continue;
|
|
1684
|
-
const isAlone = /^\s*(?:<!--|\/\/|\/\*|\{\/\*)\s*maz-ui-review-ignore/.test(line);
|
|
1685
|
-
byLine.set(isAlone ? index + 2 : index + 1, parseRules(match[1]));
|
|
1686
|
-
}
|
|
1687
|
-
return { file, byLine };
|
|
1688
|
-
}
|
|
1689
|
-
function isIgnored(rule, scope) {
|
|
1690
|
-
return scope === "all" || scope instanceof Set && scope.has(rule);
|
|
1691
|
-
}
|
|
1692
|
-
function blocksOf(root) {
|
|
1693
|
-
return root.children.filter((child) => child.type === NodeTypes.ELEMENT);
|
|
1694
|
-
}
|
|
1695
|
-
function blockContent(block) {
|
|
1696
|
-
const first = block.children[0];
|
|
1697
|
-
const last = block.children.at(-1);
|
|
1698
|
-
if (!first || !last)
|
|
1699
|
-
return { content: "", offset: block.loc.end.offset };
|
|
1700
|
-
const start = first.loc.start.offset;
|
|
1701
|
-
const end = last.loc.end.offset;
|
|
1702
|
-
return { content: block.loc.source.slice(start - block.loc.start.offset, end - block.loc.start.offset), offset: start };
|
|
1703
|
-
}
|
|
1704
|
-
function reviewCode(code, options) {
|
|
1705
|
-
const starts = lineStarts(code);
|
|
1706
|
-
const findings = [];
|
|
1707
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1708
|
-
const context = {
|
|
1709
|
-
...options,
|
|
1710
|
-
report: (finding, offset) => {
|
|
1711
|
-
const position = locate(starts, offset);
|
|
1712
|
-
const key = `${finding.rule}:${position.line}:${position.column}:${finding.message}`;
|
|
1713
|
-
if (seen.has(key))
|
|
1714
|
-
return;
|
|
1715
|
-
seen.add(key);
|
|
1716
|
-
findings.push({ ...finding, ...position });
|
|
1717
|
-
}
|
|
1718
|
-
};
|
|
1719
|
-
const kind = detectKind(code, options.filename);
|
|
1720
|
-
if (kind === "script") {
|
|
1721
|
-
reviewScript(code, 0, context);
|
|
1722
|
-
} else if (kind === "style") {
|
|
1723
|
-
reviewStyle(code, 0, context);
|
|
1724
|
-
} else if (kind === "template") {
|
|
1725
|
-
reviewTemplate(parse(code), code, context);
|
|
1726
|
-
} else {
|
|
1727
|
-
const root = parse(code, { parseMode: "sfc" });
|
|
1728
|
-
const blocks = blocksOf(root);
|
|
1729
|
-
context.localComponents = localComponents(blocks.filter((block) => block.tag === "script").map((block) => blockContent(block).content).join("\n"));
|
|
1730
|
-
for (const block of blocks) {
|
|
1731
|
-
if (block.tag === "template") {
|
|
1732
|
-
reviewTemplate(block, code, context);
|
|
1733
|
-
} else if (block.tag === "script") {
|
|
1734
|
-
const { content, offset } = blockContent(block);
|
|
1735
|
-
reviewScript(content, offset, context);
|
|
1736
|
-
} else if (block.tag === "style") {
|
|
1737
|
-
const { content, offset } = blockContent(block);
|
|
1738
|
-
reviewStyle(content, offset, context, { sfc: true });
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
const { file, byLine } = ignoredRules(code.split("\n"));
|
|
1743
|
-
return findings.filter((finding) => !isIgnored(finding.rule, file) && !isIgnored(finding.rule, byLine.get(finding.line))).sort((a, b) => a.line - b.line || a.column - b.column || SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
|
|
1744
|
-
}
|
|
1745
|
-
function countBySeverity(findings) {
|
|
1746
|
-
return {
|
|
1747
|
-
error: findings.filter((finding) => finding.severity === "error").length,
|
|
1748
|
-
warning: findings.filter((finding) => finding.severity === "warning").length,
|
|
1749
|
-
info: findings.filter((finding) => finding.severity === "info").length
|
|
1750
|
-
};
|
|
1751
|
-
}
|
|
1752
|
-
function filterBySeverity(findings, minimum) {
|
|
1753
|
-
return findings.filter((finding) => SEVERITY_ORDER[finding.severity] <= SEVERITY_ORDER[minimum]);
|
|
1754
|
-
}
|
|
1755
|
-
function formatFindings(findings, label) {
|
|
1756
|
-
const counts = countBySeverity(findings);
|
|
1757
|
-
if (findings.length === 0)
|
|
1758
|
-
return `${label}: no issue found.`;
|
|
1759
|
-
const summary = ["error", "warning", "info"].filter((severity) => counts[severity] > 0).map((severity) => `${counts[severity]} ${severity}${counts[severity] > 1 ? "s" : ""}`).join(", ");
|
|
1760
|
-
return [
|
|
1761
|
-
`${label}: ${summary}`,
|
|
1762
|
-
...findings.map((finding) => `- L${finding.line}:${finding.column} ${finding.severity} [${finding.rule}] ${finding.message}${finding.suggestion ? `
|
|
1763
|
-
-> ${finding.suggestion}` : ""}`)
|
|
1764
|
-
].join("\n");
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
const REVIEWABLE_EXTENSIONS = ["vue", "ts", "tsx", "js", "jsx", "mjs", "mts", "css", "scss", "pcss", "postcss"];
|
|
1768
|
-
const MAX_REVIEWED_FILES = 200;
|
|
1769
|
-
function detectFramework(fromDirectory) {
|
|
1770
|
-
let directory = fromDirectory;
|
|
1771
|
-
for (; ; ) {
|
|
1772
|
-
if (["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"].some((file) => existsSync(join(directory, file))))
|
|
1773
|
-
return "nuxt";
|
|
1774
|
-
if (existsSync(join(directory, "package.json")))
|
|
1775
|
-
return "vue";
|
|
1776
|
-
const parent = dirname(directory);
|
|
1777
|
-
if (parent === directory)
|
|
1778
|
-
return "vue";
|
|
1779
|
-
directory = parent;
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
async function expandPaths(patterns, cwd) {
|
|
1783
|
-
const files = /* @__PURE__ */ new Set();
|
|
1784
|
-
const globs = [];
|
|
1785
|
-
for (const pattern of patterns) {
|
|
1786
|
-
const absolute = isAbsolute(pattern) ? pattern : resolve(cwd, pattern);
|
|
1787
|
-
if (existsSync(absolute) && statSync(absolute).isFile())
|
|
1788
|
-
files.add(absolute);
|
|
1789
|
-
else if (existsSync(absolute) && statSync(absolute).isDirectory())
|
|
1790
|
-
globs.push(`${relative(cwd, absolute) || "."}/**/*.{${REVIEWABLE_EXTENSIONS.join(",")}}`);
|
|
1791
|
-
else
|
|
1792
|
-
globs.push(pattern);
|
|
1793
|
-
}
|
|
1794
|
-
if (globs.length > 0) {
|
|
1795
|
-
const matches = await glob(globs, { cwd, absolute: true, ignore: ["**/node_modules/**", "**/dist/**", "**/.nuxt/**", "**/.output/**", "**/coverage/**"] });
|
|
1796
|
-
for (const match of matches) files.add(match);
|
|
1797
|
-
}
|
|
1798
|
-
return [...files].filter((file) => REVIEWABLE_EXTENSIONS.includes(file.split(".").pop() ?? "")).sort();
|
|
1799
|
-
}
|
|
1800
|
-
function displayPath(cwd, file) {
|
|
1801
|
-
const path = relative(cwd, file);
|
|
1802
|
-
return path && !path.startsWith("..") && !isAbsolute(path) ? path : file;
|
|
1803
|
-
}
|
|
1804
|
-
async function reviewFiles(patterns, { knowledge, cwd, framework, minSeverity = "info" }) {
|
|
1805
|
-
const files = await expandPaths(patterns, cwd);
|
|
1806
|
-
return {
|
|
1807
|
-
truncated: files.length > MAX_REVIEWED_FILES,
|
|
1808
|
-
reviews: files.slice(0, MAX_REVIEWED_FILES).map((file) => ({
|
|
1809
|
-
file: displayPath(cwd, file),
|
|
1810
|
-
findings: filterBySeverity(
|
|
1811
|
-
reviewCode(readFileSync(file, "utf8"), { knowledge, filename: file, framework: framework ?? detectFramework(dirname(file)) }),
|
|
1812
|
-
minSeverity
|
|
1813
|
-
)
|
|
1814
|
-
}))
|
|
1815
|
-
};
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
const FENCE = /^\s*(?:`{3,}|~{3,})/;
|
|
1819
|
-
function stripFrontmatter(markdown) {
|
|
1820
|
-
return markdown.replace(/^---\n[\s\S]*?\n---\n+/, "");
|
|
1821
|
-
}
|
|
1822
|
-
function linksToDocIds(markdown, doc) {
|
|
1823
|
-
const base = doc.file.split("/").slice(0, -1);
|
|
1824
|
-
return markdown.replaceAll(/\]\((\.{1,2}\/[^)\s]+?\.md)(#[^)\s]*)?\)/g, (_, path, anchor = "") => {
|
|
1825
|
-
const parts = [...base];
|
|
1826
|
-
for (const segment of path.split("/")) {
|
|
1827
|
-
if (segment === "..")
|
|
1828
|
-
parts.pop();
|
|
1829
|
-
else if (segment !== ".")
|
|
1830
|
-
parts.push(segment);
|
|
1831
|
-
}
|
|
1832
|
-
const id = parts.join("/").replace(/\.md$/, "").replace(/\/index$/, "");
|
|
1833
|
-
return `](${id}${anchor})`;
|
|
1834
|
-
});
|
|
1835
|
-
}
|
|
1836
|
-
function listSections(markdown) {
|
|
1837
|
-
const lines = markdown.split("\n");
|
|
1838
|
-
const sections = [];
|
|
1839
|
-
let inFence = false;
|
|
1840
|
-
for (const [index, line] of lines.entries()) {
|
|
1841
|
-
if (FENCE.test(line)) {
|
|
1842
|
-
inFence = !inFence;
|
|
1843
|
-
continue;
|
|
1844
|
-
}
|
|
1845
|
-
const match = !inFence && /^(#{2,4})\s+(\S.*)$/.exec(line);
|
|
1846
|
-
if (match)
|
|
1847
|
-
sections.push({ title: match[2].trim(), level: match[1].length, start: index, end: lines.length });
|
|
1848
|
-
}
|
|
1849
|
-
for (const [index, section] of sections.entries()) {
|
|
1850
|
-
const next = sections.slice(index + 1).find((candidate) => candidate.level <= section.level);
|
|
1851
|
-
section.end = next ? next.start : lines.length;
|
|
1852
|
-
}
|
|
1853
|
-
return sections;
|
|
1854
|
-
}
|
|
1855
|
-
function normalize(value) {
|
|
1856
|
-
return value.toLowerCase().replaceAll(/[^a-z\d]+/g, " ").trim();
|
|
1857
|
-
}
|
|
1858
|
-
function findSection(markdown, title) {
|
|
1859
|
-
const sections = listSections(markdown);
|
|
1860
|
-
const wanted = normalize(title);
|
|
1861
|
-
return sections.find((section) => normalize(section.title) === wanted) ?? sections.find((section) => normalize(section.title).startsWith(wanted)) ?? sections.find((section) => normalize(section.title).includes(wanted));
|
|
1862
|
-
}
|
|
1863
|
-
function sectionContent(markdown, section) {
|
|
1864
|
-
return markdown.split("\n").slice(section.start, section.end).join("\n").trim();
|
|
1865
|
-
}
|
|
1866
|
-
function outline(markdown) {
|
|
1867
|
-
return listSections(markdown).filter((section) => section.level <= 3).map((section) => `${section.level === 3 ? " " : ""}- ${section.title}`).join("\n");
|
|
1868
|
-
}
|
|
1869
|
-
function truncateText(value, max) {
|
|
1870
|
-
return value.length <= max ? value : `${value.slice(0, max - 3).trimEnd()}...`;
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
class ToolInputError extends Error {
|
|
1874
|
-
}
|
|
1875
|
-
function defineTool(definition) {
|
|
1876
|
-
return definition;
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
const CATEGORY_TITLES = {
|
|
1880
|
-
guide: "Guides",
|
|
1881
|
-
component: "Components (maz-ui)",
|
|
1882
|
-
composable: "Composables (maz-ui)",
|
|
1883
|
-
directive: "Directives (maz-ui)",
|
|
1884
|
-
plugin: "Plugins (maz-ui)",
|
|
1885
|
-
ecosystem: "Ecosystem packages",
|
|
1886
|
-
util: "Helpers (@maz-ui/utils)",
|
|
1887
|
-
node: "Node utilities (@maz-ui/node)",
|
|
1888
|
-
reference: "References"
|
|
1889
|
-
};
|
|
1890
|
-
function describeSource(context) {
|
|
1891
|
-
const { source, version } = context.knowledge;
|
|
1892
|
-
if (source.kind === "project")
|
|
1893
|
-
return `Docs of maz-ui ${version}, read from the project (node_modules/maz-ui/docs).`;
|
|
1894
|
-
if (source.kind === "custom")
|
|
1895
|
-
return `Docs of maz-ui ${version}, read from ${source.directory}.`;
|
|
1896
|
-
if (source.projectVersion && source.projectVersion !== version)
|
|
1897
|
-
return `The project uses maz-ui ${source.projectVersion}, which does not ship its docs: these are the docs of maz-ui ${version} (bundled with @maz-ui/mcp). Double check APIs that may differ.`;
|
|
1898
|
-
return `Docs of maz-ui ${version} (bundled with @maz-ui/mcp).`;
|
|
1899
|
-
}
|
|
1900
|
-
function listDocs(context, { category }) {
|
|
1901
|
-
const categories = category ? [category] : docCategories;
|
|
1902
|
-
const sections = categories.map((current) => {
|
|
1903
|
-
const docs = context.knowledge.docs.filter((doc) => doc.category === current);
|
|
1904
|
-
if (docs.length === 0)
|
|
1905
|
-
return "";
|
|
1906
|
-
const lines = docs.map((doc) => `- ${doc.title} \`${doc.id}\`: ${truncateText(doc.description, 120)}`);
|
|
1907
|
-
return `## ${CATEGORY_TITLES[current]} (${docs.length})
|
|
1908
|
-
|
|
1909
|
-
${lines.join("\n")}`;
|
|
1910
|
-
}).filter(Boolean);
|
|
1911
|
-
return [
|
|
1912
|
-
`# maz-ui ecosystem catalog`,
|
|
1913
|
-
"",
|
|
1914
|
-
describeSource(context),
|
|
1915
|
-
"",
|
|
1916
|
-
...sections.flatMap((section) => [section, ""]),
|
|
1917
|
-
"Read a page with `get_doc({ id })`, a component API with `get_component_api({ names })`."
|
|
1918
|
-
].join("\n");
|
|
1919
|
-
}
|
|
1920
|
-
const listDocsTool = defineTool({
|
|
1921
|
-
name: "list_docs",
|
|
1922
|
-
title: "maz-ui catalog",
|
|
1923
|
-
description: "Catalog of everything maz-ui and its ecosystem provide (installed version), one line per item with its id: components, composables, directives, plugins, @maz-ui/utils helpers, ecosystem packages (themes, translations, icons, nuxt), guides and references. Use it to get an overview; prefer `search_docs` to find something for a given need.",
|
|
1924
|
-
inputSchema: {
|
|
1925
|
-
category: z.enum(docCategories).optional().describe("Only list this category")
|
|
1926
|
-
},
|
|
1927
|
-
run: listDocs
|
|
1928
|
-
});
|
|
1929
|
-
|
|
1930
|
-
function buildInstructions(context) {
|
|
1931
|
-
return `maz-ui ${context.knowledge.version}: docs, component APIs, icons, Tailwind theme tokens and code review for the maz-ui ecosystem (Vue 3 and Nuxt UI library: components, composables, directives, plugins, @maz-ui/themes, @maz-ui/icons, @maz-ui/utils, @maz-ui/translations, @maz-ui/nuxt). ${describeSource(context)}
|
|
1932
|
-
|
|
1933
|
-
Use these tools whenever you write, refactor or review Vue/Nuxt UI code, styles or helpers in a project that uses maz-ui. Most UI elements, helpers and icons already exist: reuse them instead of re-implementing them.
|
|
1934
|
-
|
|
1935
|
-
Workflow:
|
|
1936
|
-
1. search_docs("<the need in plain words>") BEFORE creating a component, composable, helper, icon or style ("confirm before delete", "phone input", "debounce search", "dark mode toggle").
|
|
1937
|
-
2. get_component_api(["MazBtn", ...]) BEFORE writing a template with Maz* components: use only documented props, values, events and slots.
|
|
1938
|
-
3. find_icons("<meaning>") for icons: never inline SVG nor another icon library.
|
|
1939
|
-
4. get_theme_tokens() BEFORE styling: it reads the project theme (preset, @theme tokens, variants, utilities) and lists the maz-ui classes. Theme colors only (bg-surface, text-foreground, text-muted, border-divider, bg-primary text-primary-foreground, bg-container...), radius, shadow-elevation, breakpoints mob-*/tab-*/lap-*.
|
|
1940
|
-
5. review_code({ paths: [...] }) on the files you changed, then fix every error and warning.
|
|
1941
|
-
Other tools: get_doc (pages and guides: guide/tailwind, ecosystem/themes, ecosystem/translations, ecosystem/nuxt, guide/vue, guide/migration-v5...), list_docs (catalog), get_setup_guide (install in Vue or Nuxt).
|
|
1942
|
-
|
|
1943
|
-
Rules:
|
|
1944
|
-
- Maz* components instead of native or hand-made elements (MazBtn, MazInput, MazSelect, MazDialog, MazDialogConfirm, MazCard, MazTable, MazAvatar, MazBadge, MazAlert, MazSkeleton, MazDatePicker, MazDropzone...). useToast for notifications, useDialog for confirmations.
|
|
1945
|
-
- @maz-ui/utils and maz-ui composables instead of lodash, VueUse or custom code (debounce, throttle, formatCurrency, formatDate, isEqual, cookies, useBreakpoints, useTimer...).
|
|
1946
|
-
- Tailwind: never the default palette (bg-blue-500, text-gray-600, bg-white) nor hard-coded colors; theme colors already handle dark mode (no dark: color overrides); never the internal \`maz:\` prefix. Custom CSS uses var(--maz-*).
|
|
1947
|
-
- Theme through a @maz-ui/themes preset and useTheme() (toggleDarkMode, setColorMode), never by hand.
|
|
1948
|
-
- Deep imports (maz-ui/components/MazBtn, maz-ui/composables/useToast, @maz-ui/utils/helpers/debounce, @maz-ui/icons/static/MazTrash); the maz-ui root only exports types. With @maz-ui/nuxt, components and composables are auto-imported.`;
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
function userMessage(text) {
|
|
1952
|
-
return { messages: [{ role: "user", content: { type: "text", text } }] };
|
|
1953
|
-
}
|
|
1954
|
-
function registerPrompts(server) {
|
|
1955
|
-
server.registerPrompt("build", {
|
|
1956
|
-
title: "Build with maz-ui",
|
|
1957
|
-
description: "Implement a feature, page or component with the maz-ui ecosystem instead of re-implementing it",
|
|
1958
|
-
argsSchema: {
|
|
1959
|
-
task: z.string().describe("What to build")
|
|
1960
|
-
}
|
|
1961
|
-
}, ({ task }) => userMessage(`Implement the following with the maz-ui ecosystem: ${task}
|
|
1962
|
-
|
|
1963
|
-
1. Split the task into UI elements, behaviors, helpers, icons and styles.
|
|
1964
|
-
2. For each one, call \`search_docs\` with the need in plain words and pick what maz-ui provides (components, composables, directives, plugins, @maz-ui/utils helpers).
|
|
1965
|
-
3. Call \`get_component_api\` (batch) for every Maz* component you will use, \`find_icons\` for icons and \`get_theme_tokens\` for the Tailwind classes. Read \`get_doc\` examples when the usage is not obvious.
|
|
1966
|
-
4. Write the code: Maz* components, theme colors only, deep imports (or Nuxt auto-imports), no new dependency for something maz-ui covers.
|
|
1967
|
-
5. Run \`review_code\` on the files you changed and fix every error and warning.`));
|
|
1968
|
-
server.registerPrompt("audit", {
|
|
1969
|
-
title: "Audit maz-ui usage",
|
|
1970
|
-
description: "Find and fix what re-implements maz-ui in existing code (native elements, other libraries, default Tailwind palette, wrong APIs)",
|
|
1971
|
-
argsSchema: {
|
|
1972
|
-
paths: z.string().describe('Files, folders or globs to audit, comma separated (e.g. "src/components, src/pages")')
|
|
1973
|
-
}
|
|
1974
|
-
}, ({ paths }) => userMessage(`Audit how these files use the maz-ui ecosystem: ${paths}
|
|
1975
|
-
|
|
1976
|
-
1. Run \`review_code\` with these paths.
|
|
1977
|
-
2. For each finding, check the replacement with \`search_docs\`, \`get_component_api\`, \`find_icons\` or \`get_theme_tokens\`, then fix it without changing the behavior.
|
|
1978
|
-
3. Also look for hand-made code that maz-ui already provides (formatting, debounce, toasts, dialogs, loaders, skeletons, breakpoints, dark mode) and replace it.
|
|
1979
|
-
4. Run \`review_code\` again until no error or warning remains, and summarize the changes.`));
|
|
1980
|
-
server.registerPrompt("setup", {
|
|
1981
|
-
title: "Set up maz-ui",
|
|
1982
|
-
description: "Install and configure maz-ui, its theme, translations and Tailwind integration in this project",
|
|
1983
|
-
argsSchema: {
|
|
1984
|
-
framework: z.enum(["vue", "nuxt"]).describe("Framework of the project")
|
|
1985
|
-
}
|
|
1986
|
-
}, ({ framework }) => userMessage(`Set up maz-ui in this ${framework} project.
|
|
1987
|
-
|
|
1988
|
-
1. Read the project (package.json, ${framework === "nuxt" ? "nuxt.config" : "main.ts"}, CSS entry, Tailwind setup) to know what is already there.
|
|
1989
|
-
2. Call \`get_setup_guide({ framework: "${framework}" })\` and apply it: dependencies, ${framework === "nuxt" ? "the @maz-ui/nuxt module options" : "the MazUi plugin with a theme preset and translations"}, CSS imports and Tailwind theme tokens.
|
|
1990
|
-
3. Run \`npx @maz-ui/mcp init\` so every AI agent of the project gets the maz-ui rules and this MCP server.
|
|
1991
|
-
4. Verify that the app builds and that a MazBtn renders with the theme.`));
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
function findIcons(context, { query, limit = 24 }) {
|
|
1995
|
-
const { iconNames } = context.knowledge;
|
|
1996
|
-
const exact = [...iconNames].find((name) => name.toLowerCase() === query.trim().toLowerCase());
|
|
1997
|
-
const names = exact ? [exact, ...searchIcons(iconNames, query, limit).filter((name) => name !== exact)].slice(0, limit) : searchIcons(iconNames, query, limit);
|
|
1998
|
-
if (names.length === 0) {
|
|
1999
|
-
const near = closest(query.startsWith("Maz") ? query : `Maz${query}`, iconNames, { max: 4, limit: 8 });
|
|
2000
|
-
return [
|
|
2001
|
-
`No icon matches "${query}" in @maz-ui/icons (${iconNames.size} icons).`,
|
|
2002
|
-
near.length > 0 ? `Close names: ${near.join(", ")}.` : "",
|
|
2003
|
-
'Try another English word (icons follow the heroicons naming: trash, x-mark, magnifying-glass, cog, bars-3, chevron-down...). Full list: `get_doc({ id: "reference/icons" })`.'
|
|
2004
|
-
].filter(Boolean).join("\n");
|
|
2005
|
-
}
|
|
2006
|
-
const [first] = names;
|
|
2007
|
-
return [
|
|
2008
|
-
`# @maz-ui/icons matching "${query}"`,
|
|
2009
|
-
"",
|
|
2010
|
-
names.map((name) => `- \`${name}\``).join("\n"),
|
|
2011
|
-
"",
|
|
2012
|
-
"## Import",
|
|
2013
|
-
"",
|
|
2014
|
-
"```ts",
|
|
2015
|
-
`import { ${first} } from '@maz-ui/icons/static/${first}' // tree-shakable component`,
|
|
2016
|
-
`import { ${first} } from '@maz-ui/icons/lazy/${first}' // lazy-loaded async component`,
|
|
2017
|
-
`import { ${first} } from '@maz-ui/icons/raw/${first}' // raw SVG string`,
|
|
2018
|
-
"```",
|
|
2019
|
-
"",
|
|
2020
|
-
`Render \`<${first} class="size-5 text-primary" />\` (size and color from CSS, the SVG uses \`currentColor\`), or pass it to an icon prop: \`<MazBtn :start-icon="${first}">\`. Never inline SVG markup nor add another icon library.`
|
|
2021
|
-
].join("\n");
|
|
2022
|
-
}
|
|
2023
|
-
const findIconsTool = defineTool({
|
|
2024
|
-
name: "find_icons",
|
|
2025
|
-
title: "Find @maz-ui/icons",
|
|
2026
|
-
description: 'Find icons in @maz-ui/icons (860+ heroicons-based icons, brand logos and country flags) from what they represent ("delete", "close", "settings", "user", "france flag", "github") and get their import. Use it instead of inline SVG or another icon library.',
|
|
2027
|
-
inputSchema: {
|
|
2028
|
-
query: z.string().min(1).describe("What the icon represents, in English, or an icon name"),
|
|
2029
|
-
limit: z.number().int().min(1).max(60).optional().describe("Maximum number of icons (default 24)")
|
|
2030
|
-
},
|
|
2031
|
-
run: findIcons
|
|
2032
|
-
});
|
|
2033
|
-
|
|
2034
|
-
function getComponentApi(context, { names }) {
|
|
2035
|
-
const blocks = [];
|
|
2036
|
-
const unknown = [];
|
|
2037
|
-
for (const name of [...new Set(names)]) {
|
|
2038
|
-
const component = resolveComponent(context.knowledge, name);
|
|
2039
|
-
if (!component) {
|
|
2040
|
-
unknown.push(name);
|
|
2041
|
-
continue;
|
|
2042
|
-
}
|
|
2043
|
-
const doc = context.knowledge.docsById.get(component.docId);
|
|
2044
|
-
const markdown = doc ? linksToDocIds(stripFrontmatter(context.knowledge.readDoc(doc.id)), doc) : "";
|
|
2045
|
-
const section = findSection(markdown, `${component.name} API`);
|
|
2046
|
-
const api = section ? sectionContent(markdown, section) : `## ${component.name} API
|
|
2047
|
-
|
|
2048
|
-
\`${component.import}\``;
|
|
2049
|
-
const parent = component.parent ? `
|
|
2050
|
-
|
|
2051
|
-
Building block of \`${component.parent}\`: see \`get_doc({ id: "${context.knowledge.componentsByName.get(component.parent)?.docId}" })\` for complete examples.` : "";
|
|
2052
|
-
blocks.push(`${api}${parent}
|
|
2053
|
-
|
|
2054
|
-
Examples: \`get_doc({ id: "${component.docId}" })\``);
|
|
2055
|
-
}
|
|
2056
|
-
if (blocks.length === 0) {
|
|
2057
|
-
const suggestions = unknown.flatMap((name) => closest(name, context.knowledge.componentsByName.keys(), { max: 4 }));
|
|
2058
|
-
throw new ToolInputError([
|
|
2059
|
-
`Unknown maz-ui component${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}.`,
|
|
2060
|
-
suggestions.length > 0 ? `Did you mean: ${[...new Set(suggestions)].join(", ")}?` : "",
|
|
2061
|
-
'Use `search_docs` to find a component from a need, or `list_docs({ category: "component" })`.'
|
|
2062
|
-
].filter(Boolean).join("\n"));
|
|
2063
|
-
}
|
|
2064
|
-
if (unknown.length > 0) {
|
|
2065
|
-
const suggestions = unknown.flatMap((name) => closest(name, context.knowledge.componentsByName.keys(), { max: 4 }));
|
|
2066
|
-
blocks.push(`Unknown: ${unknown.join(", ")}.${suggestions.length > 0 ? ` Did you mean ${[...new Set(suggestions)].join(", ")}?` : ""}`);
|
|
2067
|
-
}
|
|
2068
|
-
return [
|
|
2069
|
-
`Props are listed in kebab-case (use \`:prop\` for dynamic values). Values marked "non exhaustive" accept other values. Never use undocumented props, events or slots.`,
|
|
2070
|
-
...blocks
|
|
2071
|
-
].join("\n\n---\n\n");
|
|
2072
|
-
}
|
|
2073
|
-
const getComponentApiTool = defineTool({
|
|
2074
|
-
name: "get_component_api",
|
|
2075
|
-
title: "maz-ui component API",
|
|
2076
|
-
description: "Exact API of one or more maz-ui components (installed version): import, props with types, defaults and accepted values, v-model, events with payload, slots with bindings, exposed methods. Call it before writing a template with a Maz* component; batch up to 10 components in one call.",
|
|
2077
|
-
inputSchema: {
|
|
2078
|
-
names: z.array(z.string().min(1)).min(1).max(10).describe('Component names: ["MazBtn", "MazDialog"] (kebab-case and short names like "btn" work)')
|
|
2079
|
-
},
|
|
2080
|
-
run: getComponentApi
|
|
2081
|
-
});
|
|
2082
|
-
|
|
2083
|
-
const WEAK_MATCH_SCORE = 150;
|
|
2084
|
-
function nextCall(doc) {
|
|
2085
|
-
if (doc.category === "component")
|
|
2086
|
-
return `get_component_api({ names: ["${doc.title}"] }) for the API, get_doc({ id: "${doc.id}" }) for examples`;
|
|
2087
|
-
if (doc.id === "reference/tailwind-tokens")
|
|
2088
|
-
return "get_theme_tokens()";
|
|
2089
|
-
if (doc.id === "reference/icons")
|
|
2090
|
-
return 'find_icons({ query: "..." })';
|
|
2091
|
-
return `get_doc({ id: "${doc.id}" })`;
|
|
2092
|
-
}
|
|
2093
|
-
function formatDocLine(doc, index) {
|
|
2094
|
-
const prefix = index === void 0 ? "-" : `${index + 1}.`;
|
|
2095
|
-
const lines = [
|
|
2096
|
-
`${prefix} **${doc.title}** (${doc.category}, \`${doc.id}\`): ${truncateText(doc.description, 220)}`
|
|
2097
|
-
];
|
|
2098
|
-
if (doc.imports[0])
|
|
2099
|
-
lines.push(` \`${doc.imports[0]}\``);
|
|
2100
|
-
lines.push(` Next: ${nextCall(doc)}`);
|
|
2101
|
-
return lines.join("\n");
|
|
2102
|
-
}
|
|
2103
|
-
function searchDocs(context, { query, category, limit = 8 }) {
|
|
2104
|
-
const hits = context.search.search(query, { category, limit });
|
|
2105
|
-
const header = `# maz-ui ${context.knowledge.version} results for "${query}"${category ? ` (category: ${category})` : ""}`;
|
|
2106
|
-
if (hits.length === 0) {
|
|
2107
|
-
return [
|
|
2108
|
-
header,
|
|
2109
|
-
"",
|
|
2110
|
-
'No result. Try other words describing the need (e.g. "modal" or "dialog", "notification" or "toast"), remove the category filter, or browse with `list_docs`.',
|
|
2111
|
-
"If maz-ui does not provide it, build it with maz-ui components and the theme tokens (`get_theme_tokens`)."
|
|
2112
|
-
].join("\n");
|
|
2113
|
-
}
|
|
2114
|
-
const lines = [header, "", ...hits.map((hit, index) => formatDocLine(hit.doc, index))];
|
|
2115
|
-
if (hits[0].score < WEAK_MATCH_SCORE) {
|
|
2116
|
-
lines.push("", "These results are weak matches: maz-ui probably has no dedicated feature for this need. Compose it from maz-ui components (MazCard, MazBtn, MazIcon...) and theme tokens (`get_theme_tokens`) rather than adding another library.");
|
|
2117
|
-
}
|
|
2118
|
-
if (/\bicons?\b/i.test(query)) {
|
|
2119
|
-
const icons = searchIcons(context.knowledge.iconNames, query, 8);
|
|
2120
|
-
if (icons.length > 0)
|
|
2121
|
-
lines.push("", `Icons: ${icons.join(", ")}. Use \`find_icons\` for imports.`);
|
|
2122
|
-
}
|
|
2123
|
-
return lines.join("\n");
|
|
2124
|
-
}
|
|
2125
|
-
const searchDocsTool = defineTool({
|
|
2126
|
-
name: "search_docs",
|
|
2127
|
-
title: "Search maz-ui",
|
|
2128
|
-
description: 'Find what maz-ui and its ecosystem already provide for a need, BEFORE writing a component, composable, helper or style. Describe the need in plain words ("user avatar with initials", "confirm before delete", "phone number input", "debounce search", "dark mode toggle", "tailwind breakpoints"); names (MazBtn, useToast) and aliases from other libraries (modal, snackbar, combobox) work too. Returns ranked components, composables, directives, plugins, helpers (@maz-ui/utils), guides and references with their import and the next tool to call.',
|
|
2129
|
-
inputSchema: {
|
|
2130
|
-
query: z.string().min(1).describe("The need or the name to look for, in English"),
|
|
2131
|
-
category: z.enum(docCategories).optional().describe("Restrict to one category"),
|
|
2132
|
-
limit: z.number().int().min(1).max(20).optional().describe("Number of results (default 8)")
|
|
2133
|
-
},
|
|
2134
|
-
run: searchDocs
|
|
2135
|
-
});
|
|
2136
|
-
|
|
2137
|
-
const MAX_FULL_DOC_CHARS = 24e3;
|
|
2138
|
-
const PREVIEW_CHARS = 6e3;
|
|
2139
|
-
function notFoundMessage(context, query) {
|
|
2140
|
-
const suggestions = context.search.search(query, { limit: 5 });
|
|
2141
|
-
return [
|
|
2142
|
-
`No maz-ui doc matches "${query}".`,
|
|
2143
|
-
...suggestions.length > 0 ? ["", "Closest docs:", ...suggestions.map((hit) => formatDocLine(hit.doc))] : [],
|
|
2144
|
-
"",
|
|
2145
|
-
'Pass an id from `search_docs` or `list_docs` (e.g. "components/maz-btn", "composables/use-toast", "guide/tailwind").'
|
|
2146
|
-
].join("\n");
|
|
2147
|
-
}
|
|
2148
|
-
function related(context, doc) {
|
|
2149
|
-
const docs = doc.related.map((id) => context.knowledge.docsById.get(id)).filter((entry) => !!entry);
|
|
2150
|
-
return docs.length > 0 ? `
|
|
2151
|
-
|
|
2152
|
-
Related: ${docs.map((entry) => `\`${entry.id}\` (${entry.title})`).join(", ")}` : "";
|
|
2153
|
-
}
|
|
2154
|
-
function getDoc(context, { id, sections, full = false }) {
|
|
2155
|
-
const doc = resolveDoc(context.knowledge, id);
|
|
2156
|
-
if (!doc)
|
|
2157
|
-
throw new ToolInputError(notFoundMessage(context, id));
|
|
2158
|
-
const markdown = linksToDocIds(stripFrontmatter(context.knowledge.readDoc(doc.id)), doc);
|
|
2159
|
-
if (sections?.length) {
|
|
2160
|
-
const found = sections.map((title) => ({ title, section: findSection(markdown, title) }));
|
|
2161
|
-
const missing = found.filter((entry) => !entry.section).map((entry) => entry.title);
|
|
2162
|
-
if (missing.length === found.length) {
|
|
2163
|
-
throw new ToolInputError(`No section "${missing.join('", "')}" in \`${doc.id}\`. Available sections:
|
|
2164
|
-
${outline(markdown)}`);
|
|
2165
|
-
}
|
|
2166
|
-
return [
|
|
2167
|
-
`# ${doc.title} (\`${doc.id}\`)`,
|
|
2168
|
-
...found.filter((entry) => entry.section).map((entry) => sectionContent(markdown, entry.section)),
|
|
2169
|
-
...missing.length > 0 ? [`Sections not found: ${missing.join(", ")}.`] : []
|
|
2170
|
-
].join("\n\n");
|
|
2171
|
-
}
|
|
2172
|
-
if (full || markdown.length <= MAX_FULL_DOC_CHARS)
|
|
2173
|
-
return `${markdown.trim()}${related(context, doc)}`;
|
|
2174
|
-
const firstSection = listSections(markdown)[0];
|
|
2175
|
-
const intro = markdown.slice(0, Math.min(firstSection ? markdown.split("\n").slice(0, firstSection.start).join("\n").length : PREVIEW_CHARS, PREVIEW_CHARS));
|
|
2176
|
-
const apiHint = doc.category === "component" ? `
|
|
2177
|
-
|
|
2178
|
-
For the props, events and slots: \`get_component_api({ names: ${JSON.stringify(doc.symbols)} })\`.` : "";
|
|
2179
|
-
return [
|
|
2180
|
-
intro.trim(),
|
|
2181
|
-
"",
|
|
2182
|
-
`This page is long (${markdown.length} characters). Request the sections you need with \`get_doc({ id: "${doc.id}", sections: ["..."] })\`, or \`full: true\` for everything.${apiHint}`,
|
|
2183
|
-
"",
|
|
2184
|
-
"## Sections",
|
|
2185
|
-
"",
|
|
2186
|
-
outline(markdown)
|
|
2187
|
-
].join("\n") + related(context, doc);
|
|
2188
|
-
}
|
|
2189
|
-
const getDocTool = defineTool({
|
|
2190
|
-
name: "get_doc",
|
|
2191
|
-
title: "Read a maz-ui doc page",
|
|
2192
|
-
description: 'Read a maz-ui documentation page (usage, examples, options) of the installed version: a component, composable, directive, plugin, helper, guide (tailwind, vue, getting-started, migration-v5...) or ecosystem package (themes, translations, icons, nuxt). Accepts an id ("components/maz-btn", "guide/tailwind", "ecosystem/themes") or a name (MazBtn, useToast, vTooltip, debounce). Long pages return an outline: then ask for specific sections.',
|
|
2193
|
-
inputSchema: {
|
|
2194
|
-
id: z.string().min(1).describe('Doc id or name: "components/maz-btn", "MazBtn", "useToast", "guide/tailwind", "ecosystem/themes"'),
|
|
2195
|
-
sections: z.array(z.string()).max(10).optional().describe('Only return these sections (heading titles, e.g. ["Basic usage", "Loading"])'),
|
|
2196
|
-
full: z.boolean().optional().describe("Return the whole page even when it is long")
|
|
2197
|
-
},
|
|
2198
|
-
run: getDoc
|
|
2199
|
-
});
|
|
2200
|
-
|
|
2201
|
-
function sections(context, id, titles) {
|
|
2202
|
-
const doc = context.knowledge.docsById.get(id);
|
|
2203
|
-
if (!doc)
|
|
2204
|
-
return [];
|
|
2205
|
-
const markdown = linksToDocIds(stripFrontmatter(context.knowledge.readDoc(id)), doc);
|
|
2206
|
-
return titles.map((title) => findSection(markdown, title)).filter((section) => !!section).map((section) => sectionContent(markdown, section));
|
|
2207
|
-
}
|
|
2208
|
-
function getSetupGuide(context, { framework, tailwind = true }) {
|
|
2209
|
-
const parts = [`# Set up maz-ui ${context.knowledge.version} in a ${framework === "nuxt" ? "Nuxt" : "Vue"} app`];
|
|
2210
|
-
if (framework === "nuxt")
|
|
2211
|
-
parts.push(...sections(context, "ecosystem/nuxt", ["Installation", "Minimal Setup"]));
|
|
2212
|
-
else
|
|
2213
|
-
parts.push(...sections(context, "guide/vue", ["Quick Start"]));
|
|
2214
|
-
if (tailwind) {
|
|
2215
|
-
parts.push(
|
|
2216
|
-
"## Tailwind CSS",
|
|
2217
|
-
...sections(context, "guide/tailwind", ["Setup"]),
|
|
2218
|
-
`Then style with the theme tokens only (\`get_theme_tokens\`).${framework === "nuxt" ? " With Tailwind, set `mazUi.css.injectCss: false` and import `maz-ui/style.css` first in your Tailwind entry to control the CSS order." : ""}`
|
|
2219
|
-
);
|
|
2220
|
-
}
|
|
2221
|
-
parts.push(
|
|
2222
|
-
"## AI agents",
|
|
2223
|
-
"Run `npx @maz-ui/mcp init` in the project: it registers this MCP server and writes the maz-ui rules for your agents (Claude Code, Cursor, Copilot, Codex...) without touching your own instructions.",
|
|
2224
|
-
`Full guides: \`get_doc({ id: "${framework === "nuxt" ? "ecosystem/nuxt" : "guide/vue"}" })\`, \`get_doc({ id: "ecosystem/themes" })\` (presets, dark mode), \`get_doc({ id: "ecosystem/translations" })\`.`
|
|
2225
|
-
);
|
|
2226
|
-
return parts.join("\n\n");
|
|
2227
|
-
}
|
|
2228
|
-
const getSetupGuideTool = defineTool({
|
|
2229
|
-
name: "get_setup_guide",
|
|
2230
|
-
title: "Set up maz-ui",
|
|
2231
|
-
description: "Installation and configuration steps of maz-ui for a Vue (Vite) or Nuxt app, with or without Tailwind CSS: packages to install, plugin or Nuxt module options, theme preset, translations, CSS entry and AI agent setup.",
|
|
2232
|
-
inputSchema: {
|
|
2233
|
-
framework: z.enum(["vue", "nuxt"]).describe("Target framework"),
|
|
2234
|
-
tailwind: z.boolean().optional().describe("Include the Tailwind CSS integration (default true)")
|
|
2235
|
-
},
|
|
2236
|
-
run: getSetupGuide
|
|
2237
|
-
});
|
|
2238
|
-
|
|
2239
|
-
function sourceOf(node, source) {
|
|
2240
|
-
return source.slice(node.start ?? 0, node.end ?? 0).replaceAll(/\s+/g, " ");
|
|
2241
|
-
}
|
|
2242
|
-
function keyOf(node) {
|
|
2243
|
-
if (node.type !== "ObjectProperty" || node.computed)
|
|
2244
|
-
return void 0;
|
|
2245
|
-
if (node.key.type === "Identifier")
|
|
2246
|
-
return node.key.name;
|
|
2247
|
-
return node.key.type === "StringLiteral" || node.key.type === "NumericLiteral" ? String(node.key.value) : void 0;
|
|
2248
|
-
}
|
|
2249
|
-
function unwrap(node) {
|
|
2250
|
-
return node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSNonNullExpression" || node.type === "ParenthesizedExpression" ? unwrap(node.expression) : node;
|
|
2251
|
-
}
|
|
2252
|
-
function literalValue(node, source) {
|
|
2253
|
-
const expression = unwrap(node);
|
|
2254
|
-
switch (expression.type) {
|
|
2255
|
-
case "StringLiteral":
|
|
2256
|
-
case "NumericLiteral":
|
|
2257
|
-
case "BooleanLiteral":
|
|
2258
|
-
return expression.value;
|
|
2259
|
-
case "NullLiteral":
|
|
2260
|
-
return null;
|
|
2261
|
-
case "TemplateLiteral":
|
|
2262
|
-
return expression.expressions.length === 0 ? expression.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join("") : `<expr: ${sourceOf(expression, source)}>`;
|
|
2263
|
-
case "ArrayExpression":
|
|
2264
|
-
return expression.elements.map((element) => element && element.type !== "SpreadElement" ? literalValue(element, source) : null);
|
|
2265
|
-
case "ObjectExpression":
|
|
2266
|
-
return Object.fromEntries(expression.properties.flatMap((property) => {
|
|
2267
|
-
const key = keyOf(property);
|
|
2268
|
-
return key && property.type === "ObjectProperty" ? [[key, literalValue(property.value, source)]] : [];
|
|
2269
|
-
}));
|
|
2270
|
-
default:
|
|
2271
|
-
return `<expr: ${sourceOf(expression, source)}>`;
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
function isDefinePreset(node) {
|
|
2275
|
-
return node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "definePreset";
|
|
2276
|
-
}
|
|
2277
|
-
function findPresetCalls(node, found, name) {
|
|
2278
|
-
if (isDefinePreset(node)) {
|
|
2279
|
-
found.push({ call: node, name });
|
|
2280
|
-
return;
|
|
2281
|
-
}
|
|
2282
|
-
const variableName = node.type === "VariableDeclarator" && node.id.type === "Identifier" ? node.id.name : name;
|
|
2283
|
-
for (const [key, value] of Object.entries(node)) {
|
|
2284
|
-
if (key === "loc" || key === "leadingComments" || key === "trailingComments" || key === "innerComments")
|
|
2285
|
-
continue;
|
|
2286
|
-
for (const child of Array.isArray(value) ? value : [value]) {
|
|
2287
|
-
if (child && typeof child === "object" && typeof child.type === "string")
|
|
2288
|
-
findPresetCalls(child, found, variableName);
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
function readPresets(file, source) {
|
|
2293
|
-
let program;
|
|
2294
|
-
try {
|
|
2295
|
-
program = parse$1(source, { sourceType: "module", plugins: ["typescript"], errorRecovery: true }).program;
|
|
2296
|
-
} catch {
|
|
2297
|
-
return [];
|
|
2298
|
-
}
|
|
2299
|
-
const calls = [];
|
|
2300
|
-
findPresetCalls(program, calls);
|
|
2301
|
-
return calls.flatMap(({ call, name }) => {
|
|
2302
|
-
const [argument] = call.arguments;
|
|
2303
|
-
if (!argument || argument.type !== "ObjectExpression")
|
|
2304
|
-
return [];
|
|
2305
|
-
const options = literalValue(argument, source);
|
|
2306
|
-
const base = options.base;
|
|
2307
|
-
const overrides = options.overrides;
|
|
2308
|
-
return [{
|
|
2309
|
-
file,
|
|
2310
|
-
exportName: name,
|
|
2311
|
-
base: typeof base === "string" ? base.replace(/^<expr: (.*)>$/, "$1") : void 0,
|
|
2312
|
-
overrides: overrides && typeof overrides === "object" && !Array.isArray(overrides) ? overrides : {}
|
|
2313
|
-
}];
|
|
2314
|
-
});
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
const BLOCK_AT_RULES = /^@(?:utility|keyframes|custom-variant)\s/;
|
|
2318
|
-
function blankComments(css, comments) {
|
|
2319
|
-
return css.replaceAll(/\/\*([\s\S]*?)\*\//g, (match, text, index) => {
|
|
2320
|
-
comments.push({ end: index + match.length, text: text.replaceAll(/\s*\n\s*\*?\s*/g, " ").trim() });
|
|
2321
|
-
return match.replaceAll(/[^\n]/g, " ");
|
|
2322
|
-
});
|
|
2323
|
-
}
|
|
2324
|
-
function commentBefore(css, comments, start) {
|
|
2325
|
-
const comment = comments.findLast((entry) => entry.end <= start);
|
|
2326
|
-
return comment && css.slice(comment.end, start).trim() === "" ? comment.text : void 0;
|
|
2327
|
-
}
|
|
2328
|
-
function parseCss(source) {
|
|
2329
|
-
const comments = [];
|
|
2330
|
-
const css = blankComments(source, comments);
|
|
2331
|
-
const result = { declarations: [], statements: [], blocks: [] };
|
|
2332
|
-
const stack = [];
|
|
2333
|
-
let start = 0;
|
|
2334
|
-
let parens = 0;
|
|
2335
|
-
let quote;
|
|
2336
|
-
const flush = (end) => {
|
|
2337
|
-
const raw = css.slice(start, end);
|
|
2338
|
-
const statement = raw.trim();
|
|
2339
|
-
const position = start + raw.indexOf(statement);
|
|
2340
|
-
if (statement.startsWith("@")) {
|
|
2341
|
-
result.statements.push(statement.replaceAll(/\s+/g, " "));
|
|
2342
|
-
return;
|
|
2343
|
-
}
|
|
2344
|
-
const declaration = /^(--[\w*-]+|[a-z-]+)\s*:([\s\S]*)$/.exec(statement);
|
|
2345
|
-
if (declaration && stack.length > 0) {
|
|
2346
|
-
result.declarations.push({
|
|
2347
|
-
property: declaration[1],
|
|
2348
|
-
value: declaration[2].trim().replaceAll(/\s+/g, " "),
|
|
2349
|
-
context: stack.map((entry) => entry.prelude),
|
|
2350
|
-
comment: commentBefore(css, comments, position)
|
|
2351
|
-
});
|
|
2352
|
-
}
|
|
2353
|
-
};
|
|
2354
|
-
for (let index = 0; index < css.length; index++) {
|
|
2355
|
-
const char = css[index];
|
|
2356
|
-
if (quote) {
|
|
2357
|
-
if (char === quote && css[index - 1] !== "\\")
|
|
2358
|
-
quote = void 0;
|
|
2359
|
-
continue;
|
|
2360
|
-
}
|
|
2361
|
-
if (char === '"' || char === "'") {
|
|
2362
|
-
quote = char;
|
|
2363
|
-
} else if (char === "(") {
|
|
2364
|
-
parens++;
|
|
2365
|
-
} else if (char === ")") {
|
|
2366
|
-
parens = Math.max(0, parens - 1);
|
|
2367
|
-
} else if (parens > 0) {
|
|
2368
|
-
continue;
|
|
2369
|
-
} else if (char === ";") {
|
|
2370
|
-
flush(index);
|
|
2371
|
-
start = index + 1;
|
|
2372
|
-
} else if (char === "{") {
|
|
2373
|
-
stack.push({ prelude: css.slice(start, index).trim().replaceAll(/\s+/g, " "), open: index });
|
|
2374
|
-
start = index + 1;
|
|
2375
|
-
} else if (char === "}") {
|
|
2376
|
-
flush(index);
|
|
2377
|
-
start = index + 1;
|
|
2378
|
-
const block = stack.pop();
|
|
2379
|
-
if (block && BLOCK_AT_RULES.test(block.prelude))
|
|
2380
|
-
result.blocks.push({ prelude: block.prelude, body: css.slice(block.open + 1, index).trim().replaceAll(/\s+/g, " ") });
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
return result;
|
|
2384
|
-
}
|
|
2385
|
-
const NAMESPACES = [
|
|
2386
|
-
{ prefix: "--color-", topic: "colors", usage: (name) => `bg-${name} text-${name} border-${name}...` },
|
|
2387
|
-
{ prefix: "--font-weight-", topic: "typography", usage: (name) => `font-${name}` },
|
|
2388
|
-
{ prefix: "--font-", topic: "typography", usage: (name) => `font-${name}` },
|
|
2389
|
-
{ prefix: "--text-shadow-", topic: "shadows", usage: (name) => `text-shadow-${name}` },
|
|
2390
|
-
{ prefix: "--text-", topic: "typography", usage: (name) => `text-${name}` },
|
|
2391
|
-
{ prefix: "--tracking-", topic: "typography", usage: (name) => `tracking-${name}` },
|
|
2392
|
-
{ prefix: "--leading-", topic: "typography", usage: (name) => `leading-${name}` },
|
|
2393
|
-
{ prefix: "--animate-", topic: "typography", usage: (name) => `animate-${name}` },
|
|
2394
|
-
{ prefix: "--ease-", topic: "typography", usage: (name) => `ease-${name}` },
|
|
2395
|
-
{ prefix: "--duration-", topic: "typography", usage: (name) => `duration-(--duration-${name})` },
|
|
2396
|
-
{ prefix: "--radius-", topic: "radius", usage: (name) => `rounded-${name}` },
|
|
2397
|
-
{ prefix: "--inset-shadow-", topic: "shadows", usage: (name) => `inset-shadow-${name}` },
|
|
2398
|
-
{ prefix: "--drop-shadow-", topic: "shadows", usage: (name) => `drop-shadow-${name}` },
|
|
2399
|
-
{ prefix: "--shadow-", topic: "shadows", usage: (name) => `shadow-${name}` },
|
|
2400
|
-
{ prefix: "--spacing-", topic: "spacing", usage: (name) => `p-${name} gap-${name} w-${name}...` },
|
|
2401
|
-
{ prefix: "--breakpoint-", topic: "breakpoints", usage: (name) => `${name}:` },
|
|
2402
|
-
{ prefix: "--container-", topic: "layout", usage: (name) => `max-w-${name} @${name}:` },
|
|
2403
|
-
{ prefix: "--aspect-", topic: "layout", usage: (name) => `aspect-${name}` },
|
|
2404
|
-
{ prefix: "--z-", topic: "z-index", usage: (name) => `z-${name}` }
|
|
2405
|
-
];
|
|
2406
|
-
function tokenOf(variable, value, { inline = false, comment } = {}) {
|
|
2407
|
-
if (variable === "--spacing")
|
|
2408
|
-
return { variable, value, topic: "spacing", usage: "p-4 gap-2 w-8... (base step)", inline, comment };
|
|
2409
|
-
const namespace = NAMESPACES.find((entry) => variable.startsWith(entry.prefix));
|
|
2410
|
-
return namespace ? { variable, value, topic: namespace.topic, usage: namespace.usage(variable.slice(namespace.prefix.length)), inline, comment } : { variable, value, topic: "css-variables", usage: `var(${variable})`, inline, comment };
|
|
2411
|
-
}
|
|
2412
|
-
function importPath(statement) {
|
|
2413
|
-
return /^@import\s+(?:url\()?\s*["']([^"']+)["']/.exec(statement)?.[1];
|
|
2414
|
-
}
|
|
2415
|
-
function themeContext(context) {
|
|
2416
|
-
return context.find((prelude) => /^@theme\b/.test(prelude));
|
|
2417
|
-
}
|
|
2418
|
-
function readStylesheet(file, source) {
|
|
2419
|
-
const { declarations, statements, blocks } = parseCss(source);
|
|
2420
|
-
const stylesheet = { file, imports: [], tokens: [], resets: [], variants: [], utilities: [], keyframes: [], mazVariables: [] };
|
|
2421
|
-
for (const statement of statements) {
|
|
2422
|
-
const path = importPath(statement);
|
|
2423
|
-
const variant = /^@custom-variant ([\w-]+) (.+)$/.exec(statement);
|
|
2424
|
-
if (path)
|
|
2425
|
-
stylesheet.imports.push(path);
|
|
2426
|
-
else if (variant)
|
|
2427
|
-
stylesheet.variants.push({ name: variant[1], definition: variant[2] });
|
|
2428
|
-
}
|
|
2429
|
-
for (const { prelude, body } of blocks) {
|
|
2430
|
-
const [, kind, name] = /^@([\w-]+) ([\w*-]+)/.exec(prelude) ?? [];
|
|
2431
|
-
if (kind === "utility")
|
|
2432
|
-
stylesheet.utilities.push({ name, css: body });
|
|
2433
|
-
else if (kind === "keyframes")
|
|
2434
|
-
stylesheet.keyframes.push(name);
|
|
2435
|
-
else if (kind === "custom-variant")
|
|
2436
|
-
stylesheet.variants.push({ name, definition: `{ ${body} }` });
|
|
2437
|
-
}
|
|
2438
|
-
for (const { property, value, context, comment } of declarations) {
|
|
2439
|
-
const theme = themeContext(context);
|
|
2440
|
-
if (theme && property.startsWith("--")) {
|
|
2441
|
-
if (value === "initial")
|
|
2442
|
-
stylesheet.resets.push(property);
|
|
2443
|
-
else if (!/^--[\w-]+--/.test(property))
|
|
2444
|
-
stylesheet.tokens.push(tokenOf(property, value, { inline: /\binline\b/.test(theme), comment }));
|
|
2445
|
-
} else if (!theme && property.startsWith("--maz-")) {
|
|
2446
|
-
stylesheet.mazVariables.push({ variable: property, value, selector: context.filter((prelude) => !prelude.startsWith("@layer")).join(" ") });
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
return stylesheet;
|
|
2450
|
-
}
|
|
2451
|
-
function isTailwindStylesheet(source) {
|
|
2452
|
-
return /@import\s+(?:url\()?\s*["'](?:tailwindcss|maz-ui\/tailwindcss)/.test(source) || /@(?:theme|custom-variant|utility)\b/.test(source);
|
|
2453
|
-
}
|
|
2454
|
-
|
|
2455
|
-
const IGNORED = [
|
|
2456
|
-
"**/node_modules/**",
|
|
2457
|
-
"**/dist/**",
|
|
2458
|
-
"**/build/**",
|
|
2459
|
-
"**/coverage/**",
|
|
2460
|
-
"**/public/**",
|
|
2461
|
-
"**/.*/**",
|
|
2462
|
-
"**/*.min.css",
|
|
2463
|
-
"**/*.d.ts",
|
|
2464
|
-
"**/*.{spec,test}.*"
|
|
2465
|
-
];
|
|
2466
|
-
const MAX_FILE_SIZE = 3e5;
|
|
2467
|
-
const PATTERNS = ["*.css", "*.ts", "*.mts", "*.cts", "*.js", "*.mjs"];
|
|
2468
|
-
const IGNORED_PATH = /(?:^|\/)(?:node_modules|dist|build|coverage|public|fixtures|__tests__|\.[^/]+)\/|\.min\.css$|\.d\.ts$|\.(?:spec|test)\.\w+$/;
|
|
2469
|
-
function projectFiles(rootDir) {
|
|
2470
|
-
try {
|
|
2471
|
-
return execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "--", ...PATTERNS], { cwd: rootDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024 }).split("\n").filter((file) => file && !IGNORED_PATH.test(file));
|
|
2472
|
-
} catch {
|
|
2473
|
-
return globSync(PATTERNS.map((pattern) => `**/${pattern}`), { cwd: rootDir, ignore: IGNORED, deep: 8 }).filter((file) => !IGNORED_PATH.test(file));
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
function readSmallFile(path) {
|
|
2477
|
-
try {
|
|
2478
|
-
return statSync(path).size <= MAX_FILE_SIZE ? readFileSync(path, "utf8") : void 0;
|
|
2479
|
-
} catch {
|
|
2480
|
-
return void 0;
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
function readProjectTheme(rootDir) {
|
|
2484
|
-
if (!existsSync(join(rootDir, "package.json")))
|
|
2485
|
-
return { stylesheets: [], presets: [] };
|
|
2486
|
-
const files = projectFiles(rootDir).sort();
|
|
2487
|
-
const theme = { stylesheets: [], presets: [] };
|
|
2488
|
-
for (const file of files) {
|
|
2489
|
-
const source = readSmallFile(join(rootDir, file));
|
|
2490
|
-
if (!source)
|
|
2491
|
-
continue;
|
|
2492
|
-
if (file.endsWith(".css")) {
|
|
2493
|
-
if (isTailwindStylesheet(source))
|
|
2494
|
-
theme.stylesheets.push(readStylesheet(file, source));
|
|
2495
|
-
} else if (source.includes("definePreset(")) {
|
|
2496
|
-
theme.presets.push(...readPresets(file, source));
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
return theme;
|
|
2500
|
-
}
|
|
2501
|
-
const FOUNDATION_USAGE = {
|
|
2502
|
-
"base-font-size": { topic: "typography", usage: "root font size: every `rem` and the `text-*` sizes" },
|
|
2503
|
-
"border-width": { topic: "typography", usage: "`border` (default width)" },
|
|
2504
|
-
"font-family": { topic: "typography", usage: "`font-sans` (default font)" },
|
|
2505
|
-
"font-mono-stack": { topic: "typography", usage: "`font-mono`" },
|
|
2506
|
-
"font-display-stack": { topic: "typography", usage: "`font-display`" },
|
|
2507
|
-
"motion-fast": { topic: "typography", usage: "`duration-(--duration-fast)`" },
|
|
2508
|
-
"motion-normal": { topic: "typography", usage: "`duration-(--duration-normal)` and the default transition duration" },
|
|
2509
|
-
"motion-slow": { topic: "typography", usage: "`duration-(--duration-slow)`" },
|
|
2510
|
-
"easing-out": { topic: "typography", usage: "`ease-out` and the default transition easing" },
|
|
2511
|
-
"easing-in": { topic: "typography", usage: "`ease-in`" },
|
|
2512
|
-
"easing-in-out": { topic: "typography", usage: "`ease-in-out`" },
|
|
2513
|
-
"disabled-opacity": { topic: "utilities", usage: "`disabled-opacity`, `disabled-state`" },
|
|
2514
|
-
"disabled-cursor": { topic: "utilities", usage: "`disabled-cursor`, `disabled-state`" },
|
|
2515
|
-
"space": { topic: "spacing", usage: "every spacing utility (`p-4` = 4 x space)" }
|
|
2516
|
-
};
|
|
2517
|
-
const COMPONENT_USAGE = {
|
|
2518
|
-
"btn.font-weight": "font weight of MazBtn",
|
|
2519
|
-
"container.bg": "`bg-container`: MazCard, MazContainer, MazDialog, MazPopover, MazDrawer...",
|
|
2520
|
-
"input.bg": "`bg-input`: MazInput, MazSelect, MazTextarea, MazCheckbox...",
|
|
2521
|
-
"input.top-label-font-weight": "font weight of the `top-label` of the inputs",
|
|
2522
|
-
"dialog.max-width": "max width of MazDialog",
|
|
2523
|
-
"dialog.min-width": "min width of MazDialog"
|
|
2524
|
-
};
|
|
2525
|
-
function code(value) {
|
|
2526
|
-
return `\`${value.replaceAll("`", "'").replaceAll("|", "\\|")}\``;
|
|
2527
|
-
}
|
|
2528
|
-
function shorten(value, max = 70) {
|
|
2529
|
-
return value.length > max ? `${value.slice(0, max - 3)}...` : value;
|
|
2530
|
-
}
|
|
2531
|
-
function display(value) {
|
|
2532
|
-
if (value === void 0)
|
|
2533
|
-
return "-";
|
|
2534
|
-
return code(shorten(typeof value === "object" && value !== null ? JSON.stringify(value) : String(value)));
|
|
2535
|
-
}
|
|
2536
|
-
function asRecord(value) {
|
|
2537
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2538
|
-
}
|
|
2539
|
-
const COLOR_CLASSES = {
|
|
2540
|
-
shadow: "`shadow-shadow/20`",
|
|
2541
|
-
overlay: "`bg-overlay`",
|
|
2542
|
-
divider: "`border-divider`"
|
|
2543
|
-
};
|
|
2544
|
-
function colorClasses(name) {
|
|
2545
|
-
if (COLOR_CLASSES[name])
|
|
2546
|
-
return COLOR_CLASSES[name];
|
|
2547
|
-
return name.endsWith("-foreground") ? code(`text-${name}`) : `${code(`bg-${name}`)} ${code(`text-${name}`)}`;
|
|
2548
|
-
}
|
|
2549
|
-
function presetColors(preset) {
|
|
2550
|
-
const colors = asRecord(preset.overrides.colors);
|
|
2551
|
-
const light = asRecord(colors.light);
|
|
2552
|
-
const dark = asRecord(colors.dark);
|
|
2553
|
-
const names = [.../* @__PURE__ */ new Set([...Object.keys(light), ...Object.keys(dark)])];
|
|
2554
|
-
if (names.length === 0)
|
|
2555
|
-
return [];
|
|
2556
|
-
return [
|
|
2557
|
-
"| Color | Classes | Light | Dark |",
|
|
2558
|
-
"| --- | --- | --- | --- |",
|
|
2559
|
-
...names.map((name) => `| \`${name}\` | ${colorClasses(name)} | ${display(light[name])} | ${display(dark[name])} |`)
|
|
2560
|
-
];
|
|
2561
|
-
}
|
|
2562
|
-
function presetFoundation(preset, wanted) {
|
|
2563
|
-
const rows = Object.entries(asRecord(preset.overrides.foundation)).filter(([key]) => wanted(FOUNDATION_USAGE[key]?.topic ?? "css-variables")).map(([key, value]) => `| \`${key}\` | \`--maz-${key}\` | ${FOUNDATION_USAGE[key]?.usage ?? "-"} | ${display(value)} |`);
|
|
2564
|
-
return rows.length > 0 ? ["| Foundation | CSS variable | Used by | Value |", "| --- | --- | --- | --- |", ...rows] : [];
|
|
2565
|
-
}
|
|
2566
|
-
function presetScales(preset, wanted) {
|
|
2567
|
-
const scales = asRecord(preset.overrides.scales);
|
|
2568
|
-
const rows = [
|
|
2569
|
-
...wanted("radius") ? Object.entries(asRecord(scales.rounded)).map(([key, value]) => `| ${code(`rounded-${key}`)} | ${display(value)} |`) : [],
|
|
2570
|
-
...wanted("shadows") ? Object.entries(asRecord(scales.shadow)).map(([key, value]) => `| ${code(`shadow-${key}`)} | ${display(value)} |`) : []
|
|
2571
|
-
];
|
|
2572
|
-
return rows.length > 0 ? ["| Class | Value |", "| --- | --- |", ...rows] : [];
|
|
2573
|
-
}
|
|
2574
|
-
function flatten(value, path) {
|
|
2575
|
-
const record = asRecord(value);
|
|
2576
|
-
const keys = Object.keys(record);
|
|
2577
|
-
if (keys.length === 0 || keys.every((key) => key === "light" || key === "dark"))
|
|
2578
|
-
return [[path, value]];
|
|
2579
|
-
return keys.flatMap((key) => flatten(record[key], path ? `${path}.${key}` : key));
|
|
2580
|
-
}
|
|
2581
|
-
function presetComponents(preset) {
|
|
2582
|
-
const rows = flatten(asRecord(preset.overrides.components), "").filter(([path]) => path).map(([path, value]) => `| \`components.${path}\` | ${COMPONENT_USAGE[path] ?? "-"} | ${display(value)} |`);
|
|
2583
|
-
return rows.length > 0 ? ["| Component token | Used by | Value |", "| --- | --- | --- |", ...rows] : [];
|
|
2584
|
-
}
|
|
2585
|
-
function renderPreset(preset, wanted, filtered) {
|
|
2586
|
-
const name = typeof preset.overrides.name === "string" ? ` \`${preset.overrides.name}\`` : "";
|
|
2587
|
-
const origin = [preset.exportName && `\`${preset.exportName}\``, preset.base && `base \`${preset.base}\``].filter(Boolean).join(", ");
|
|
2588
|
-
const tables = [
|
|
2589
|
-
wanted("colors") ? presetColors(preset) : [],
|
|
2590
|
-
presetFoundation(preset, wanted),
|
|
2591
|
-
presetScales(preset, wanted),
|
|
2592
|
-
wanted("colors") ? presetComponents(preset) : []
|
|
2593
|
-
].filter((table) => table.length > 0);
|
|
2594
|
-
if (filtered && tables.length === 0)
|
|
2595
|
-
return void 0;
|
|
2596
|
-
return [
|
|
2597
|
-
`### Preset${name} - \`${preset.file}\`${origin ? ` (${origin})` : ""}`,
|
|
2598
|
-
"The preset sets the **values** of the maz-ui tokens at runtime, the class names stay the same: `bg-primary` uses the preset `primary`, `font-display` the preset `font-display-stack`. Brand colors, fonts, radius, shadows and motion belong here, not in CSS.",
|
|
2599
|
-
...tables.map((table) => table.join("\n"))
|
|
2600
|
-
].join("\n\n");
|
|
2601
|
-
}
|
|
2602
|
-
function mazUiThemeVariables(tailwind) {
|
|
2603
|
-
const variables = /* @__PURE__ */ new Map();
|
|
2604
|
-
const named = (entries, toVariable) => {
|
|
2605
|
-
for (const entry of entries) variables.set(toVariable(entry.name), entry.value);
|
|
2606
|
-
};
|
|
2607
|
-
for (const color of tailwind.colors) variables.set(`--color-${color.name}`, `var(${color.variable})`);
|
|
2608
|
-
named(tailwind.fonts, (name) => `--${name}`);
|
|
2609
|
-
named(tailwind.radius, (name) => name.replace(/^rounded-/, "--radius-"));
|
|
2610
|
-
named(tailwind.shadows, (name) => `--${name}`);
|
|
2611
|
-
named(tailwind.breakpoints, (name) => `--breakpoint-${name.replace(/:$/, "")}`);
|
|
2612
|
-
named(tailwind.easings, (name) => `--${name}`);
|
|
2613
|
-
named(tailwind.zIndex, (name) => `--${name}`);
|
|
2614
|
-
named(tailwind.durations, (name) => /--duration-[\w-]+/.exec(name)?.[0] ?? name);
|
|
2615
|
-
return variables;
|
|
2616
|
-
}
|
|
2617
|
-
function tokenNote(token, mazUi) {
|
|
2618
|
-
const original = mazUi.get(token.variable);
|
|
2619
|
-
const notes = [
|
|
2620
|
-
token.comment,
|
|
2621
|
-
original && original !== token.value ? `Redefines the maz-ui token (maz-ui: ${code(shorten(original, 50))}): this class no longer follows the preset.` : void 0
|
|
2622
|
-
];
|
|
2623
|
-
return notes.filter(Boolean).join(" ").replaceAll("|", "\\|") || "-";
|
|
2624
|
-
}
|
|
2625
|
-
function renderStylesheet(stylesheet, mazUi, wanted, animated) {
|
|
2626
|
-
const sections = [];
|
|
2627
|
-
const problems = stylesheetProblems(stylesheet);
|
|
2628
|
-
if (problems.length > 0)
|
|
2629
|
-
sections.push(problems.map((problem) => `> ${problem}`).join("\n>\n"));
|
|
2630
|
-
const tokens = stylesheet.tokens.filter((token) => wanted(token.topic));
|
|
2631
|
-
if (tokens.length > 0) {
|
|
2632
|
-
sections.push([
|
|
2633
|
-
`#### Project tokens (\`@theme\`)`,
|
|
2634
|
-
"",
|
|
2635
|
-
"| Classes | Variable | Value | Note |",
|
|
2636
|
-
"| --- | --- | --- | --- |",
|
|
2637
|
-
...tokens.map((token) => `| ${code(token.usage)} | \`${token.variable}\` | ${code(shorten(token.value, 60))} | ${tokenNote(token, mazUi)} |`)
|
|
2638
|
-
].join("\n"));
|
|
2639
|
-
}
|
|
2640
|
-
if (stylesheet.resets.length > 0 && wanted("colors"))
|
|
2641
|
-
sections.push(`Reset namespaces: ${stylesheet.resets.map((reset) => code(reset)).join(", ")}: the default Tailwind tokens of these namespaces do not exist.`);
|
|
2642
|
-
if (stylesheet.variants.length > 0 && (wanted("variants") || wanted("breakpoints")))
|
|
2643
|
-
sections.push(["#### Custom variants", "", "| Variant | Definition |", "| --- | --- |", ...stylesheet.variants.map((variant) => `| \`${variant.name}:\` | ${code(shorten(variant.definition))} |`)].join("\n"));
|
|
2644
|
-
if (stylesheet.utilities.length > 0 && wanted("utilities"))
|
|
2645
|
-
sections.push(["#### Custom utilities", "", "| Class | CSS |", "| --- | --- |", ...stylesheet.utilities.map((utility) => `| \`${utility.name}\` | ${code(shorten(utility.css))} |`)].join("\n"));
|
|
2646
|
-
const keyframes = stylesheet.keyframes.filter((name) => !animated.has(name));
|
|
2647
|
-
if (keyframes.length > 0 && wanted("typography"))
|
|
2648
|
-
sections.push(`Keyframes without \`animate-*\` token: ${keyframes.map((name) => code(name)).join(", ")}. Use them in CSS or add a \`--animate-<name>\` token to the \`@theme\` to get a class.`);
|
|
2649
|
-
if (stylesheet.mazVariables.length > 0 && (wanted("layout") || wanted("css-variables"))) {
|
|
2650
|
-
sections.push([
|
|
2651
|
-
"#### maz-ui variables set in CSS",
|
|
2652
|
-
"",
|
|
2653
|
-
"| Variable | Value | Selector |",
|
|
2654
|
-
"| --- | --- | --- |",
|
|
2655
|
-
...stylesheet.mazVariables.map((variable) => `| \`${variable.variable}\` | ${code(shorten(variable.value, 50))} | ${code(variable.selector)} |`)
|
|
2656
|
-
].join("\n"));
|
|
2657
|
-
}
|
|
2658
|
-
return sections;
|
|
2659
|
-
}
|
|
2660
|
-
function renderStylesheetSection(stylesheet, mazUi, topics, animated) {
|
|
2661
|
-
const sections = renderStylesheet(stylesheet, mazUi, (topic) => !topics?.length || topics.includes(topic), animated);
|
|
2662
|
-
if (topics?.length && sections.length === 0)
|
|
2663
|
-
return void 0;
|
|
2664
|
-
const imports = stylesheet.imports.length > 0 ? [`Imports: ${stylesheet.imports.map((path) => code(path)).join(", ")}`] : [];
|
|
2665
|
-
return [`### Tailwind \`${stylesheet.file}\``, ...imports, ...sections].join("\n\n");
|
|
2666
|
-
}
|
|
2667
|
-
function stylesheetProblems(stylesheet) {
|
|
2668
|
-
if (!stylesheet.imports.includes("tailwindcss"))
|
|
2669
|
-
return [];
|
|
2670
|
-
const mazUiImports = stylesheet.imports.filter((path) => path.startsWith("maz-ui/tailwindcss"));
|
|
2671
|
-
const problems = [];
|
|
2672
|
-
if (mazUiImports.length === 0)
|
|
2673
|
-
problems.push("The maz-ui theme is not imported: `bg-primary`, `rounded-md`, `mob-m:`... do not exist. Add `@import 'maz-ui/tailwindcss/theme.css';` after `@import 'tailwindcss';`.");
|
|
2674
|
-
else if (!mazUiImports.includes("maz-ui/tailwindcss/utilities.css"))
|
|
2675
|
-
problems.push("`maz-ui/tailwindcss/utilities.css` is not imported: `flex-center`, `padded-container`, `disabled-state`... do not exist.");
|
|
2676
|
-
return problems;
|
|
2677
|
-
}
|
|
2678
|
-
const HOW_IT_WORKS = `How your theme combines with maz-ui:
|
|
2679
|
-
|
|
2680
|
-
1. **The preset** (\`definePreset\` of \`@maz-ui/themes\`) sets the runtime values of the maz-ui tokens (\`--maz-*\`): brand colors, fonts, radius, shadows, motion. The maz-ui classes keep their names and follow it, in light and dark mode.
|
|
2681
|
-
2. **The Tailwind entry** imports the maz-ui theme, then extends it with \`@theme\` tokens (each one generates classes: \`--animate-rise\` -> \`animate-rise\`, \`--font-display\` -> \`font-display\`), \`@custom-variant\` and \`@utility\`.
|
|
2682
|
-
3. **Pick classes in this order**: the project tokens below for project-specific needs, then the maz-ui classes, never the default Tailwind palette, hard-coded values or arbitrary values. When a value is missing, add a token to the project \`@theme\` (or a brand value to the preset) and use its class.`;
|
|
2683
|
-
function renderProjectTheme(theme, tailwind, topics) {
|
|
2684
|
-
if (theme.stylesheets.length === 0 && theme.presets.length === 0)
|
|
2685
|
-
return void 0;
|
|
2686
|
-
const wanted = (topic) => !topics?.length || topics.includes(topic);
|
|
2687
|
-
const mazUi = mazUiThemeVariables(tailwind);
|
|
2688
|
-
const animated = new Set(theme.stylesheets.flatMap((stylesheet) => stylesheet.tokens.filter((token) => token.variable.startsWith("--animate-")).map((token) => token.value.split(/\s/)[0])));
|
|
2689
|
-
return [
|
|
2690
|
-
"## Your project theme",
|
|
2691
|
-
HOW_IT_WORKS,
|
|
2692
|
-
...theme.presets.map((preset) => renderPreset(preset, wanted, !!topics?.length)),
|
|
2693
|
-
...theme.stylesheets.map((stylesheet) => renderStylesheetSection(stylesheet, mazUi, topics, animated))
|
|
2694
|
-
].filter(Boolean).join("\n\n");
|
|
2695
|
-
}
|
|
2696
|
-
|
|
2697
|
-
const THEME_TOPICS = {
|
|
2698
|
-
"setup": "Setup",
|
|
2699
|
-
"colors": "Colors",
|
|
2700
|
-
"radius": "Border radius",
|
|
2701
|
-
"shadows": "Shadows",
|
|
2702
|
-
"spacing": "Spacing",
|
|
2703
|
-
"breakpoints": "Breakpoints",
|
|
2704
|
-
"typography": "Typography and motion",
|
|
2705
|
-
"z-index": "z-index",
|
|
2706
|
-
"utilities": "Custom utilities",
|
|
2707
|
-
"variants": "Variants",
|
|
2708
|
-
"layout": "Layout variables",
|
|
2709
|
-
"transitions": "Vue transitions",
|
|
2710
|
-
"css-variables": "CSS variables in custom CSS"
|
|
2711
|
-
};
|
|
2712
|
-
function getThemeTokens(context, { topics, project = true }) {
|
|
2713
|
-
const reference = context.knowledge.docsById.get("reference/tailwind-tokens");
|
|
2714
|
-
if (!reference)
|
|
2715
|
-
throw new ToolInputError('This maz-ui version does not ship the Tailwind reference. Read `get_doc({ id: "guide/tailwind" })`.');
|
|
2716
|
-
const markdown = stripFrontmatter(context.knowledge.readDoc(reference.id));
|
|
2717
|
-
const projectTheme = project ? renderProjectTheme(readProjectTheme(context.cwd), context.knowledge.manifest.tailwind, topics) : void 0;
|
|
2718
|
-
const footer = '\n\nHow to extend the theme (project colors, fonts, animations, variants, utilities, container width) and how it combines with the preset: `get_doc({ id: "guide/tailwind" })`. Brand colors, fonts and radius belong to a preset: `get_doc({ id: "ecosystem/themes", sections: ["Creating Custom Themes"] })`.';
|
|
2719
|
-
const mazUiTheme = topics?.length ? `# maz-ui ${context.knowledge.version} Tailwind theme
|
|
2720
|
-
|
|
2721
|
-
${topics.map((topic) => {
|
|
2722
|
-
const section = findSection(markdown, THEME_TOPICS[topic]);
|
|
2723
|
-
return section ? sectionContent(markdown, section) : "";
|
|
2724
|
-
}).filter(Boolean).join("\n\n")}` : markdown.trim();
|
|
2725
|
-
return projectTheme ? `${projectTheme}
|
|
2726
|
-
|
|
2727
|
-
---
|
|
2728
|
-
|
|
2729
|
-
${mazUiTheme}${footer}` : `${mazUiTheme}${footer}`;
|
|
2730
|
-
}
|
|
2731
|
-
const getThemeTokensTool = defineTool({
|
|
2732
|
-
name: "get_theme_tokens",
|
|
2733
|
-
title: "maz-ui Tailwind theme tokens",
|
|
2734
|
-
description: "The Tailwind theme of the project: its own tokens, preset, variants and utilities (read from its Tailwind entry and `definePreset`), then every Tailwind CSS class and CSS variable of the maz-ui theme (installed version): theme colors (bg-primary, text-muted, border-divider, bg-surface... with -foreground and 50-950 scales), radius, shadows (shadow-elevation), spacing, breakpoints (mob-*, tab-*, lap-*), fonts, durations, z-index, custom utilities (flex-center, padded-container), dark variant and CSS variables. Use it before styling: never the default Tailwind palette, hard-coded colors or the internal `maz:` prefix.",
|
|
2735
|
-
inputSchema: {
|
|
2736
|
-
topics: z.array(z.enum(Object.keys(THEME_TOPICS))).optional().describe("Only these topics (default: everything)"),
|
|
2737
|
-
project: z.boolean().optional().describe("Include the theme of the project: preset, `@theme` tokens, variants, utilities (default: true)")
|
|
2738
|
-
},
|
|
2739
|
-
run: getThemeTokens
|
|
2740
|
-
});
|
|
2741
|
-
|
|
2742
|
-
const LEGEND = "Fix every error and warning (info are suggestions). If a native element or a rule is intended, add a `maz-ui-review-ignore [rule]` comment on the line above.";
|
|
2743
|
-
async function reviewCodeTool(context, { paths, code, filename, framework, minSeverity = "info" }) {
|
|
2744
|
-
if (!paths?.length && !code)
|
|
2745
|
-
throw new ToolInputError("Pass `paths` (files, folders or globs, relative to the project) or `code` (with `filename` to pick the parser).");
|
|
2746
|
-
const sections = [];
|
|
2747
|
-
if (code) {
|
|
2748
|
-
const findings = filterBySeverity(
|
|
2749
|
-
reviewCode(code, { knowledge: context.knowledge, filename, framework: framework ?? detectFramework(context.cwd) }),
|
|
2750
|
-
minSeverity
|
|
2751
|
-
);
|
|
2752
|
-
sections.push(formatFindings(findings, filename ?? "code"));
|
|
2753
|
-
}
|
|
2754
|
-
if (paths?.length) {
|
|
2755
|
-
const { reviews, truncated } = await reviewFiles(paths, { knowledge: context.knowledge, cwd: context.cwd, framework, minSeverity });
|
|
2756
|
-
if (reviews.length === 0)
|
|
2757
|
-
throw new ToolInputError(`No .vue, .ts, .js or .css file matches ${paths.join(", ")} (from ${context.cwd}).`);
|
|
2758
|
-
const withIssues = reviews.filter((review) => review.findings.length > 0);
|
|
2759
|
-
const totals = countBySeverity(reviews.flatMap((review) => review.findings));
|
|
2760
|
-
sections.push(`Reviewed ${reviews.length} file${reviews.length > 1 ? "s" : ""}: ${totals.error} errors, ${totals.warning} warnings, ${totals.info} info.${truncated ? ` Only the first ${MAX_REVIEWED_FILES} files were reviewed: narrow the paths.` : ""}`);
|
|
2761
|
-
sections.push(...withIssues.map((review) => formatFindings(review.findings, review.file)));
|
|
2762
|
-
}
|
|
2763
|
-
return `# maz-ui review (maz-ui ${context.knowledge.version})
|
|
2764
|
-
|
|
2765
|
-
${sections.join("\n\n")}
|
|
2766
|
-
|
|
2767
|
-
${LEGEND}`;
|
|
2768
|
-
}
|
|
2769
|
-
const reviewCodeToolDefinition = defineTool({
|
|
2770
|
-
name: "review_code",
|
|
2771
|
-
title: "Review code against maz-ui",
|
|
2772
|
-
description: "Static review of Vue SFCs, TS/JS and CSS against the installed maz-ui: unknown components, props, v-model, events or slots, invalid prop values, unknown icons, native elements or third-party libraries that maz-ui already covers (buttons, inputs, dialogs, toasts, icons, date pickers, lodash debounce...), default Tailwind palette and hard-coded colors instead of theme tokens, the internal `maz:` prefix, Tailwind v3 utilities, manual dark mode, wrong imports. Run it on the files you created or changed before finishing.",
|
|
2773
|
-
inputSchema: {
|
|
2774
|
-
paths: z.array(z.string()).max(50).optional().describe('Files, folders or globs relative to the project root: ["src/components/LoginForm.vue", "app/pages/**/*.vue"]'),
|
|
2775
|
-
code: z.string().optional().describe("Source to review when it is not on disk"),
|
|
2776
|
-
filename: z.string().optional().describe('Name of `code` (e.g. "Card.vue") to select the parser'),
|
|
2777
|
-
framework: z.enum(["vue", "nuxt"]).optional().describe("Detected from nuxt.config when omitted"),
|
|
2778
|
-
minSeverity: z.enum(["error", "warning", "info"]).optional().describe("Hide findings below this severity (default info)")
|
|
2779
|
-
},
|
|
2780
|
-
run: reviewCodeTool
|
|
2781
|
-
});
|
|
2782
|
-
|
|
2783
|
-
const TOOLS = [
|
|
2784
|
-
searchDocsTool,
|
|
2785
|
-
getComponentApiTool,
|
|
2786
|
-
findIconsTool,
|
|
2787
|
-
getThemeTokensTool,
|
|
2788
|
-
getDocTool,
|
|
2789
|
-
listDocsTool,
|
|
2790
|
-
getSetupGuideTool,
|
|
2791
|
-
reviewCodeToolDefinition
|
|
2792
|
-
];
|
|
2793
|
-
async function runTool(context, tool, args) {
|
|
2794
|
-
try {
|
|
2795
|
-
return { content: [{ type: "text", text: await tool.run(context, args) }] };
|
|
2796
|
-
} catch (error) {
|
|
2797
|
-
const message = error instanceof ToolInputError ? error.message : `Unexpected error in ${tool.name}: ${getErrorMessage(error)}. Retry with other arguments or fall back to the docs in node_modules/maz-ui/docs.`;
|
|
2798
|
-
return { content: [{ type: "text", text: message }], isError: true };
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
function createMazUiMcpServer(options = {}) {
|
|
2802
|
-
const context = createContext(options);
|
|
2803
|
-
const server = new McpServer(
|
|
2804
|
-
{ name: "maz-ui", title: "maz-ui", version: readOwnVersion() },
|
|
2805
|
-
{ instructions: buildInstructions(context) }
|
|
2806
|
-
);
|
|
2807
|
-
for (const tool of TOOLS) {
|
|
2808
|
-
server.registerTool(tool.name, {
|
|
2809
|
-
title: tool.title,
|
|
2810
|
-
description: tool.description,
|
|
2811
|
-
inputSchema: tool.inputSchema,
|
|
2812
|
-
annotations: {
|
|
2813
|
-
title: tool.title,
|
|
2814
|
-
readOnlyHint: true,
|
|
2815
|
-
destructiveHint: false,
|
|
2816
|
-
idempotentHint: true,
|
|
2817
|
-
openWorldHint: false
|
|
2818
|
-
},
|
|
2819
|
-
_meta: { "anthropic/maxResultSizeChars": 8e4 }
|
|
2820
|
-
}, (args) => runTool(context, tool, args));
|
|
2821
|
-
}
|
|
2822
|
-
registerPrompts(server);
|
|
2823
|
-
return { server, context };
|
|
2824
|
-
}
|
|
2825
|
-
async function startStdioServer(options = {}) {
|
|
2826
|
-
const { server } = createMazUiMcpServer(options);
|
|
2827
|
-
await server.connect(new StdioServerTransport());
|
|
2828
|
-
return server;
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
const HELP = `maz-ui-mcp: maz-ui knowledge for AI coding agents
|
|
2832
|
-
|
|
2833
|
-
Usage:
|
|
2834
|
-
maz-ui-mcp [serve] [--docs <dir>] Start the MCP server (stdio)
|
|
2835
|
-
maz-ui-mcp init [options] Configure the AI agents of this project for maz-ui
|
|
2836
|
-
maz-ui-mcp review <paths...> Review files against maz-ui (CI friendly)
|
|
2837
|
-
maz-ui-mcp search <query> Search the maz-ui docs from the terminal
|
|
2838
|
-
|
|
2839
|
-
init options:
|
|
2840
|
-
--targets <list> Comma separated: ${AGENT_TARGETS.join(", ")} (default: detected, else claude,agents-md)
|
|
2841
|
-
--no-rules Only register the MCP server (no rule file, no AGENTS.md section)
|
|
2842
|
-
--no-mcp Do not register the MCP server
|
|
2843
|
-
--dry-run Show the changes without writing them
|
|
2844
|
-
--remove Remove what init added
|
|
2845
|
-
|
|
2846
|
-
review options:
|
|
2847
|
-
--min-severity <s> error | warning | info (default info)
|
|
2848
|
-
--framework <f> vue | nuxt (default: detected)
|
|
2849
|
-
--format <f> text | json (default text)
|
|
2850
|
-
--max-warnings <n> Exit with an error above this number of warnings
|
|
2851
|
-
--hook Claude Code PostToolUse hook mode (reads the edited file from stdin)
|
|
2852
|
-
|
|
2853
|
-
Common options:
|
|
2854
|
-
--docs <dir> Use this docs bundle instead of the maz-ui installed in the project
|
|
2855
|
-
--cwd <dir> Project root (default: current directory)
|
|
2856
|
-
-v, --version
|
|
2857
|
-
-h, --help
|
|
2858
|
-
`;
|
|
2859
|
-
async function readStdin() {
|
|
2860
|
-
const chunks = [];
|
|
2861
|
-
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
2862
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
2863
|
-
}
|
|
2864
|
-
function printChanges(changes, dryRun) {
|
|
2865
|
-
const labels = {
|
|
2866
|
-
create: dryRun ? "would create" : "created",
|
|
2867
|
-
update: dryRun ? "would update" : "updated",
|
|
2868
|
-
remove: dryRun ? "would remove" : "removed",
|
|
2869
|
-
unchanged: "up to date",
|
|
2870
|
-
skip: "skipped"
|
|
2871
|
-
};
|
|
2872
|
-
for (const change of changes) {
|
|
2873
|
-
console.log(` ${labels[change.action].padEnd(13)} ${change.file}${change.reason ? ` (${change.reason})` : ""}`);
|
|
2874
|
-
}
|
|
2875
|
-
}
|
|
2876
|
-
function runInit(values, cwd) {
|
|
2877
|
-
const requested = typeof values.targets === "string" ? values.targets.split(",").map((target) => target.trim()).filter(Boolean) : void 0;
|
|
2878
|
-
const invalid = requested?.filter((target) => !AGENT_TARGETS.includes(target)) ?? [];
|
|
2879
|
-
if (invalid.length > 0) {
|
|
2880
|
-
console.error(`Unknown target(s): ${invalid.join(", ")}. Available: ${AGENT_TARGETS.join(", ")}.`);
|
|
2881
|
-
return 1;
|
|
2882
|
-
}
|
|
2883
|
-
const detected = detectAgentTargets(cwd);
|
|
2884
|
-
const targets = requested ?? (detected.length > 0 ? detected : ["claude", "agents-md"]);
|
|
2885
|
-
const dryRun = values["dry-run"] === true;
|
|
2886
|
-
const result = setupAgents({
|
|
2887
|
-
rootDir: cwd,
|
|
2888
|
-
targets,
|
|
2889
|
-
rules: values["no-rules"] !== true,
|
|
2890
|
-
mcp: values["no-mcp"] !== true,
|
|
2891
|
-
dryRun,
|
|
2892
|
-
remove: values.remove === true,
|
|
2893
|
-
docsDir: typeof values.docs === "string" ? resolve(values.docs) : void 0
|
|
2894
|
-
});
|
|
2895
|
-
console.log(`maz-ui ${result.version ? `${result.version} ` : ""}agents setup (${targets.join(", ")})${dryRun ? " - dry run" : ""}`);
|
|
2896
|
-
printChanges(result.changes, dryRun);
|
|
2897
|
-
if (!values.remove && values["no-rules"] !== true && targets.includes("agents-md"))
|
|
2898
|
-
console.log("\nCodex and Windsurf read AGENTS.md. To give them the MCP server too, add `npx -y @maz-ui/mcp@latest` to their global MCP config.");
|
|
2899
|
-
return 0;
|
|
2900
|
-
}
|
|
2901
|
-
async function runReview(values, positionals, cwd) {
|
|
2902
|
-
const context = createContext({ cwd, docsDir: typeof values.docs === "string" ? resolve(values.docs) : void 0 });
|
|
2903
|
-
const framework = values.framework;
|
|
2904
|
-
if (values.hook) {
|
|
2905
|
-
const input = JSON.parse(await readStdin() || "{}");
|
|
2906
|
-
const file = input.tool_input?.file_path;
|
|
2907
|
-
if (!file || !REVIEWABLE_EXTENSIONS.includes(file.split(".").pop() ?? "") || /[\\/](?:node_modules|dist|\.nuxt|\.output)[\\/]/.test(file))
|
|
2908
|
-
return 0;
|
|
2909
|
-
const { reviews: reviews2 } = await reviewFiles([file], { knowledge: context.knowledge, cwd: input.cwd ?? cwd, framework, minSeverity: "warning" });
|
|
2910
|
-
const findings = reviews2.flatMap((review) => review.findings);
|
|
2911
|
-
if (findings.length === 0)
|
|
2912
|
-
return 0;
|
|
2913
|
-
console.error(`${formatFindings(findings, reviews2[0].file)}
|
|
2914
|
-
|
|
2915
|
-
Fix these maz-ui issues (see the maz-ui MCP tools), or add a \`maz-ui-review-ignore <rule> -- <reason>\` comment above the line when it is intended.`);
|
|
2916
|
-
return 2;
|
|
2917
|
-
}
|
|
2918
|
-
if (positionals.length === 0) {
|
|
2919
|
-
console.error('Pass the files, folders or globs to review: maz-ui-mcp review "src/**/*.vue"');
|
|
2920
|
-
return 1;
|
|
2921
|
-
}
|
|
2922
|
-
const { reviews, truncated } = await reviewFiles(positionals, {
|
|
2923
|
-
knowledge: context.knowledge,
|
|
2924
|
-
cwd,
|
|
2925
|
-
framework,
|
|
2926
|
-
minSeverity: values["min-severity"] ?? "info"
|
|
2927
|
-
});
|
|
2928
|
-
const totals = countBySeverity(reviews.flatMap((review) => review.findings));
|
|
2929
|
-
if (values.format === "json") {
|
|
2930
|
-
console.log(JSON.stringify({ version: context.knowledge.version, totals, truncated, files: reviews }, null, 2));
|
|
2931
|
-
} else {
|
|
2932
|
-
for (const review of reviews.filter((entry) => entry.findings.length > 0)) console.log(`${formatFindings(review.findings, review.file)}
|
|
2933
|
-
`);
|
|
2934
|
-
console.log(`${reviews.length} file(s) reviewed against maz-ui ${context.knowledge.version}: ${totals.error} error(s), ${totals.warning} warning(s), ${totals.info} info.`);
|
|
2935
|
-
}
|
|
2936
|
-
const maxWarnings = values["max-warnings"] === void 0 ? Number.POSITIVE_INFINITY : Number(values["max-warnings"]);
|
|
2937
|
-
return totals.error > 0 || totals.warning > maxWarnings ? 1 : 0;
|
|
2938
|
-
}
|
|
2939
|
-
async function runCli(argv = process.argv.slice(2)) {
|
|
2940
|
-
const { values, positionals } = parseArgs({
|
|
2941
|
-
args: argv,
|
|
2942
|
-
allowPositionals: true,
|
|
2943
|
-
strict: true,
|
|
2944
|
-
options: {
|
|
2945
|
-
"docs": { type: "string" },
|
|
2946
|
-
"cwd": { type: "string" },
|
|
2947
|
-
"targets": { type: "string" },
|
|
2948
|
-
"no-rules": { type: "boolean" },
|
|
2949
|
-
"no-mcp": { type: "boolean" },
|
|
2950
|
-
"dry-run": { type: "boolean" },
|
|
2951
|
-
"remove": { type: "boolean" },
|
|
2952
|
-
"min-severity": { type: "string" },
|
|
2953
|
-
"framework": { type: "string" },
|
|
2954
|
-
"format": { type: "string" },
|
|
2955
|
-
"max-warnings": { type: "string" },
|
|
2956
|
-
"hook": { type: "boolean" },
|
|
2957
|
-
"version": { type: "boolean", short: "v" },
|
|
2958
|
-
"help": { type: "boolean", short: "h" }
|
|
2959
|
-
}
|
|
2960
|
-
});
|
|
2961
|
-
const [command = "serve", ...rest] = positionals;
|
|
2962
|
-
const cwd = resolve(values.cwd ?? process.cwd());
|
|
2963
|
-
if (values.help) {
|
|
2964
|
-
console.log(HELP);
|
|
2965
|
-
return 0;
|
|
2966
|
-
}
|
|
2967
|
-
if (values.version) {
|
|
2968
|
-
console.log(readOwnVersion());
|
|
2969
|
-
return 0;
|
|
2970
|
-
}
|
|
2971
|
-
switch (command) {
|
|
2972
|
-
case "serve":
|
|
2973
|
-
await startStdioServer({ cwd, docsDir: values.docs ? resolve(values.docs) : void 0 });
|
|
2974
|
-
return -1;
|
|
2975
|
-
case "init":
|
|
2976
|
-
return runInit(values, cwd);
|
|
2977
|
-
case "review":
|
|
2978
|
-
return runReview(values, rest, cwd);
|
|
2979
|
-
case "search": {
|
|
2980
|
-
const context = createContext({ cwd, docsDir: values.docs ? resolve(values.docs) : void 0 });
|
|
2981
|
-
console.log(searchDocs(context, { query: rest.join(" ") || "button" }));
|
|
2982
|
-
return 0;
|
|
2983
|
-
}
|
|
2984
|
-
default:
|
|
2985
|
-
console.error(`Unknown command "${command}".
|
|
2986
|
-
|
|
2987
|
-
${HELP}`);
|
|
2988
|
-
return 1;
|
|
2989
|
-
}
|
|
2990
|
-
}
|
|
2991
|
-
async function main() {
|
|
2992
|
-
try {
|
|
2993
|
-
const code = await runCli();
|
|
2994
|
-
if (code >= 0)
|
|
2995
|
-
process.exitCode = code;
|
|
2996
|
-
} catch (error) {
|
|
2997
|
-
console.error(getErrorMessage(error));
|
|
2998
|
-
process.exitCode = 1;
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3001
|
-
|
|
3002
|
-
export { DocSearch as D, HELP as H, Knowledge as K, TOOLS as T, createContext as a, buildInstructions as b, countBySeverity as c, createMazUiMcpServer as d, findProjectMazUi as e, filterBySeverity as f, formatFindings as g, reviewFiles as h, runCli as i, startStdioServer as j, runInit as k, loadKnowledge as l, main as m, runReview as n, reviewCode as r, searchIcons as s };
|