@maz-ui/mcp 5.1.0-beta.0 → 5.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.mjs +3 -1
  3. package/dist/index.d.mts +11 -6
  4. package/dist/index.d.ts +11 -6
  5. package/dist/index.mjs +3 -1
  6. package/dist/shared/{mcp.DzqVbCaL.mjs → mcp.DseFuKdl.mjs} +696 -74
  7. package/docs/README.md +37 -37
  8. package/docs/agents/agents-md.md +4 -4
  9. package/docs/agents/claude-rule.md +4 -4
  10. package/docs/components/maz-accordion.md +2 -0
  11. package/docs/components/maz-btn-group.md +1 -0
  12. package/docs/components/maz-btn.md +3 -1
  13. package/docs/components/maz-card-spotlight.md +2 -2
  14. package/docs/components/maz-circular-progress-bar.md +2 -2
  15. package/docs/components/maz-drawer.md +3 -2
  16. package/docs/components/maz-fullscreen-loader.md +2 -2
  17. package/docs/components/maz-loading-bar.md +2 -2
  18. package/docs/components/maz-pagination.md +2 -2
  19. package/docs/components/maz-radio.md +2 -2
  20. package/docs/components/maz-sidebar-content.md +2 -2
  21. package/docs/components/maz-sidebar-footer.md +2 -2
  22. package/docs/components/maz-sidebar-group.md +2 -2
  23. package/docs/components/maz-sidebar-header.md +2 -2
  24. package/docs/components/maz-sidebar-menu-button.md +2 -2
  25. package/docs/components/maz-sidebar-menu-item.md +2 -2
  26. package/docs/components/maz-sidebar-menu-sub.md +2 -2
  27. package/docs/components/maz-sidebar-menu.md +2 -2
  28. package/docs/components/maz-sidebar-separator.md +2 -2
  29. package/docs/components/maz-sidebar-trigger.md +2 -2
  30. package/docs/components/maz-spinner.md +2 -2
  31. package/docs/components/maz-stepper.md +5 -0
  32. package/docs/components/maz-table-cell.md +2 -2
  33. package/docs/components/maz-table-row.md +2 -2
  34. package/docs/components/maz-table-title.md +2 -2
  35. package/docs/components/maz-table-virtualizer.md +2 -2
  36. package/docs/components/maz-tabs-bar.md +2 -2
  37. package/docs/components/maz-tabs-content-item.md +2 -2
  38. package/docs/components/maz-tabs-content.md +2 -2
  39. package/docs/components/maz-ui-provider.md +2 -2
  40. package/docs/composables/use-aos.md +2 -2
  41. package/docs/composables/use-breakpoints.md +2 -2
  42. package/docs/composables/use-toast.md +2 -2
  43. package/docs/composables/use-user-visibility.md +4 -4
  44. package/docs/composables/use-wait.md +2 -2
  45. package/docs/directives/fullscreen-img.md +4 -4
  46. package/docs/ecosystem/icons/icon-set.md +4 -4
  47. package/docs/ecosystem/icons/index.md +4 -4
  48. package/docs/ecosystem/mcp.md +1 -1
  49. package/docs/ecosystem/nuxt.md +5 -1
  50. package/docs/ecosystem/translations.md +2 -2
  51. package/docs/ecosystem/utils/country-code-to-unicode-flag.md +2 -2
  52. package/docs/guide/ai-agents.md +6 -4
  53. package/docs/guide/tailwind.md +25 -1
  54. package/docs/manifest.json +1 -1
  55. package/docs/reference/tailwind-tokens.md +1 -1
  56. package/docs/rules.md +5 -3
  57. package/package.json +4 -2
@@ -1,18 +1,34 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { join, dirname, resolve, relative, isAbsolute } from 'node:path';
3
3
  import { parseArgs } from 'node:util';
4
- import { findInstalledPackage, AGENT_TARGETS, detectAgentTargets, setupAgents } from '@maz-ui/node';
4
+ import { findProjectPackage, readPackageVersion, AGENT_TARGETS, detectAgentTargets, setupAgents } from '@maz-ui/node';
5
5
  import { getErrorMessage } from '@maz-ui/utils/helpers/getErrorMessage';
6
- import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
6
+ import { existsSync, readFileSync, statSync } from 'node:fs';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { z } from 'zod';
9
9
  import MiniSearch from 'minisearch';
10
10
  import { NodeTypes, parse } from '@vue/compiler-dom';
11
- import { glob } from 'tinyglobby';
11
+ import { glob, globSync } from 'tinyglobby';
12
12
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
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';
14
16
 
15
- const version = "5.0.1-beta.0";
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
+ }
16
32
 
17
33
  const SUPPORTED_SCHEMA_VERSION = 1;
18
34
  const docCategories = ["guide", "component", "composable", "directive", "plugin", "ecosystem", "util", "node", "reference"];
@@ -93,9 +109,16 @@ const manifestSchema = z.object({
93
109
  });
94
110
 
95
111
  class Knowledge {
96
- constructor(manifest, source) {
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) {
97
119
  this.manifest = manifest;
98
120
  this.source = source;
121
+ this.documentedVersion = documentedVersion;
99
122
  this.docsById = new Map(manifest.docs.map((doc) => [doc.id, doc]));
100
123
  this.componentsByName = new Map(manifest.components.map((component) => [component.name, component]));
101
124
  this.iconNames = new Set(manifest.icons.names);
@@ -105,7 +128,7 @@ class Knowledge {
105
128
  iconNames;
106
129
  cache = /* @__PURE__ */ new Map();
107
130
  get version() {
108
- return this.manifest.version;
131
+ return this.documentedVersion;
109
132
  }
110
133
  get docs() {
111
134
  return this.manifest.docs;
@@ -139,21 +162,8 @@ function readManifest(directory) {
139
162
  return parsed.success ? parsed.data : void 0;
140
163
  }
141
164
  function findProjectMazUi(cwd) {
142
- const bases = [cwd];
143
- for (const folder of ["apps", "packages"]) {
144
- const root = join(cwd, folder);
145
- if (existsSync(root)) {
146
- for (const entry of readdirSync(root, { withFileTypes: true })) {
147
- if (entry.isDirectory())
148
- bases.push(join(root, entry.name));
149
- }
150
- }
151
- }
152
- const directory = bases.map((base) => join(base, "node_modules", "maz-ui")).find((candidate) => existsSync(join(candidate, "package.json"))) ?? findInstalledPackage(cwd, "maz-ui");
153
- if (!directory)
154
- return void 0;
155
- const { version } = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
156
- return { directory, version };
165
+ const directory = findProjectPackage(cwd, "maz-ui");
166
+ return directory ? { directory, version: readPackageVersion(directory) ?? "0.0.0" } : void 0;
157
167
  }
158
168
  function loadKnowledge(options = {}) {
159
169
  const explicit = options.docsDir ?? process.env.MAZ_UI_DOCS_DIR;
@@ -161,20 +171,20 @@ function loadKnowledge(options = {}) {
161
171
  const manifest2 = readManifest(explicit);
162
172
  if (!manifest2)
163
173
  throw new Error(`No valid maz-ui docs bundle (manifest.json) in ${explicit}`);
164
- return new Knowledge(manifest2, { kind: "custom", directory: explicit });
174
+ return new Knowledge(manifest2, { kind: "custom", directory: explicit }, readPackageVersion(dirname(explicit)) ?? manifest2.version);
165
175
  }
166
176
  const project = findProjectMazUi(options.cwd ?? process.cwd());
167
177
  if (project) {
168
178
  const directory = join(project.directory, "docs");
169
179
  const manifest2 = readManifest(directory);
170
180
  if (manifest2)
171
- return new Knowledge(manifest2, { kind: "project", directory, projectVersion: project.version });
181
+ return new Knowledge(manifest2, { kind: "project", directory, projectVersion: project.version }, project.version);
172
182
  }
173
183
  const bundledDir = options.bundledDir ?? defaultBundledDir();
174
184
  const manifest = readManifest(bundledDir);
175
185
  if (!manifest)
176
186
  throw new Error(`The docs bundle of @maz-ui/mcp is missing or invalid (${bundledDir}). Reinstall the package.`);
177
- return new Knowledge(manifest, { kind: "bundled", directory: bundledDir, projectVersion: project?.version });
187
+ return new Knowledge(manifest, { kind: "bundled", directory: bundledDir, projectVersion: project?.version }, options.bundledVersion ?? readOwnVersion());
178
188
  }
179
189
 
180
190
  const CATEGORY_PRIORITY = ["component", "composable", "directive", "plugin", "util", "guide", "ecosystem", "reference", "node"];
@@ -418,6 +428,7 @@ function analyze(text) {
418
428
 
419
429
  const FIELD_BOOSTS = {
420
430
  name: 8,
431
+ symbols: 3,
421
432
  aliases: 5,
422
433
  description: 2.5,
423
434
  headings: 1.5,
@@ -436,8 +447,19 @@ const CATEGORY_HINTS = {
436
447
  function: "util",
437
448
  guide: "guide"
438
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.3;
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;
439
461
  const EXACT_MATCH_BOOST = 4;
440
- const EXACT_ALIAS_BOOST = 2.5;
462
+ const EXACT_ALIAS_BOOST = 4;
441
463
  const CATEGORY_HINT_BOOST = 1.4;
442
464
  function normalizedSynonyms() {
443
465
  const map = /* @__PURE__ */ new Map();
@@ -473,7 +495,8 @@ class DocSearch {
473
495
  ].join(" ") : "";
474
496
  return {
475
497
  id: doc.id,
476
- name: [doc.title, ...doc.symbols, doc.slug].join(" "),
498
+ name: [doc.title, doc.slug].join(" "),
499
+ symbols: doc.symbols.join(" "),
477
500
  aliases: doc.aliases.join(" . "),
478
501
  description: doc.description,
479
502
  headings: doc.headings.join(" . "),
@@ -497,10 +520,11 @@ class DocSearch {
497
520
  const expansion = [...new Set(terms.flatMap((term) => this.synonyms.get(term) ?? []))].filter((term) => !terms.includes(term));
498
521
  const exact = this.exactMatch(query);
499
522
  const normalizedQuery = query.trim().toLowerCase();
523
+ const hasValuableTerm = terms.some((term) => !LOW_VALUE_TERMS.has(term));
500
524
  const results = this.index.search({
501
525
  combineWith: "OR",
502
526
  queries: [
503
- { queries: [terms.join(" ")], combineWith: "OR" },
527
+ { queries: [terms.join(" ")], combineWith: "OR", boostTerm: (term) => LOW_VALUE_TERMS.has(term) ? LOW_VALUE_FACTOR : 1 },
504
528
  ...expansion.length > 0 ? [{ queries: [expansion.join(" ")], combineWith: "OR", boostTerm: () => 0.45 }] : []
505
529
  ]
506
530
  }, {
@@ -521,6 +545,11 @@ class DocSearch {
521
545
  score *= EXACT_ALIAS_BOOST;
522
546
  if (hintedCategory && doc.category === hintedCategory)
523
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;
524
553
  return { doc, score, matched };
525
554
  }).sort((a, b) => b.score - a.score).slice(0, limit);
526
555
  }
@@ -739,7 +768,39 @@ function themeSuggestion(utility, hue, step) {
739
768
  const scale = step === "500" ? "" : `-${step}`;
740
769
  return colors.map((color) => `\`${utility}-${color}${scale}\``).join(" or ");
741
770
  }
742
- function checkClassList(value, offset, context) {
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\`). Keep it over a fixed dark background (\`bg-overlay\`, image, video).` : `Use \`${utility}-foreground\` (text) or \`${utility}-contrast\`.`
800
+ }, position);
801
+ }
802
+ function checkClassList(value, offset, context, inherited) {
803
+ const background = backgroundOf(value) ?? inherited;
743
804
  for (const match of value.matchAll(/\S+/g)) {
744
805
  const token = match[0];
745
806
  const position = offset + (match.index ?? 0);
@@ -765,13 +826,7 @@ function checkClassList(value, offset, context) {
765
826
  }
766
827
  const blackWhite = BLACK_WHITE_CLASS.exec(token);
767
828
  if (blackWhite) {
768
- const [, utility, color] = blackWhite;
769
- context.report({
770
- rule: "theme-colors",
771
- severity: "warning",
772
- message: `\`${token}\` is a fixed color that does not follow the theme nor dark mode.`,
773
- suggestion: color === "white" ? `Use \`${utility}-surface\` (backgrounds) or the \`-foreground\` of the color behind (\`text-primary-foreground\`).` : `Use \`${utility}-foreground\` (text) or \`${utility}-contrast\`.`
774
- }, position);
829
+ reportBlackWhite(token, blackWhite[1], blackWhite[2], background, position, context);
775
830
  continue;
776
831
  }
777
832
  if (ARBITRARY_COLOR_CLASS.test(token)) {
@@ -991,30 +1046,71 @@ function reviewScript(code, offset, context) {
991
1046
  }
992
1047
  checkClassStrings(code.replaceAll(/^\s*import\s[^\n]*$/gm, (line) => " ".repeat(line.length)), offset, context);
993
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
+ }
994
1069
 
995
1070
  const COLOR_LITERAL = /#[\da-f]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch)\(/i;
996
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)$/;
997
- function blankComments(css) {
1072
+ function blankComments$1(css) {
998
1073
  return css.replaceAll(/\/\*[\s\S]*?\*\//g, (match) => match.replaceAll(/[^\n]/g, " "));
999
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
+ }
1000
1092
  function checkStyleDeclarations(css, offset, context) {
1093
+ const keyframes = keyframesRanges(css);
1001
1094
  for (const match of css.matchAll(/(^|[;{\s])([a-z-]+)\s*:\s*([^;{}]+)/g)) {
1002
1095
  const [, prefix, property, value] = match;
1003
1096
  if (property.startsWith("--") || !COLOR_PROPERTIES.test(property) || /var\(--/.test(value))
1004
1097
  continue;
1005
1098
  const color = COLOR_LITERAL.exec(value);
1006
- if (color) {
1007
- context.report({
1008
- rule: "hardcoded-color",
1009
- severity: "warning",
1010
- message: `\`${property}\` uses a hard-coded color (\`${color[0].replace("(", "")}\`): it ignores the theme and dark mode.`,
1011
- 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)."
1012
- }, offset + (match.index ?? 0) + prefix.length);
1013
- }
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);
1014
1110
  }
1015
1111
  }
1016
1112
  function reviewStyle(source, offset, context, { sfc = false } = {}) {
1017
- const css = blankComments(source);
1113
+ const css = blankComments$1(source);
1018
1114
  const applies = [...css.matchAll(/@apply\s+([^;]+);/g)];
1019
1115
  for (const apply of applies) {
1020
1116
  checkClassList(apply[1], offset + (apply.index ?? 0) + apply[0].indexOf(apply[1]), context);
@@ -1259,17 +1355,54 @@ function checkPropValue(prop, value, offset, component, context) {
1259
1355
  }, offset);
1260
1356
  }
1261
1357
  }
1262
- function reportUnknownProp(name, offset, component, context) {
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 } = {}) {
1263
1388
  const camel = toCamel(name);
1264
- const candidates = component.props.map((prop) => prop.name);
1265
- const near = closest(camel, candidates, { max: 2, limit: 2 });
1266
- if (isPassthroughAttribute(name) && near.length === 0)
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)
1267
1400
  return;
1268
1401
  context.report({
1269
1402
  rule: "unknown-prop",
1270
1403
  severity: "warning",
1271
- message: `${component.name} has no prop \`${name}\`${isPassthroughAttribute(name) ? "" : ": it will only be forwarded as an HTML attribute"}.`,
1272
- suggestion: near.length > 0 ? `Did you mean ${near.map((entry) => `\`${toKebab(entry)}\``).join(" or ")}? Check \`get_component_api({ names: ["${component.name}"] })\`.` : `Check the real API with \`get_component_api({ names: ["${component.name}"] })\`.`
1404
+ message: `${component.name} has no prop \`${name}\`${passthrough ? "" : ": it will only be forwarded as an HTML attribute"}.`,
1405
+ suggestion: near.length > 0 ? `Did you mean ${near.map((entry) => entry === "modelValue" ? "`v-model`" : `\`${toKebab(entry)}\``).join(" or ")}? Check \`get_component_api({ names: ["${component.name}"] })\`.` : `Check the real API with \`get_component_api({ names: ["${component.name}"] })\`.`
1273
1406
  }, offset);
1274
1407
  }
1275
1408
  function checkModel(directive, component, context) {
@@ -1320,10 +1453,16 @@ function usedSlots(element) {
1320
1453
  const templates = element.children.filter((child) => isElement(child) && child.tag === "template");
1321
1454
  return [element, ...templates].flatMap((node) => node.props.filter(isStaticSlot)).map((prop) => ({ name: directiveArg(prop) ?? "default", offset: prop.loc.start.offset }));
1322
1455
  }
1456
+ function slotMatches(slot, name) {
1457
+ if (!slot.includes("{"))
1458
+ return slot === name;
1459
+ const [prefix, suffix] = slot.split(/\{[^}]*\}/);
1460
+ return name.length > prefix.length + suffix.length && name.startsWith(prefix) && name.endsWith(suffix);
1461
+ }
1323
1462
  function checkSlots(element, component, context) {
1324
1463
  const slots = component.slots.map((slot) => slot.name);
1325
1464
  for (const { name, offset } of usedSlots(element)) {
1326
- if (slots.includes(name) || name === "default" && slots.length === 0)
1465
+ if (slots.some((slot) => slotMatches(slot, name)) || name === "default" && slots.length === 0)
1327
1466
  continue;
1328
1467
  const near = closest(name, slots, { max: 3, limit: 2 });
1329
1468
  context.report({
@@ -1371,8 +1510,26 @@ function checkMazComponent(element, component, source, context) {
1371
1510
  }
1372
1511
  checkSlots(element, component, context);
1373
1512
  }
1513
+ const SVG_SHAPES = /* @__PURE__ */ new Set(["path", "circle", "rect", "polyline", "polygon", "line", "ellipse"]);
1514
+ const ICON_MAX_VIEWBOX = 32;
1515
+ function svgDescendants(element) {
1516
+ return element.children.filter(isElement).flatMap((child) => [child, ...svgDescendants(child)]);
1517
+ }
1518
+ function isAnimated(element) {
1519
+ const classes = staticAttribute(element, "class")?.value?.content ?? "";
1520
+ return element.tag.startsWith("animate") || /\banimate-|\[animation/.test(classes) || element.props.some((prop) => prop.name.startsWith("stroke-dash"));
1521
+ }
1522
+ function isIconLikeSvg(svg) {
1523
+ const descendants = svgDescendants(svg);
1524
+ if (!descendants.some((child) => SVG_SHAPES.has(child.tag)))
1525
+ return false;
1526
+ const viewBox = staticAttribute(svg, "viewBox")?.value?.content.trim().split(/[\s,]+/).map(Number);
1527
+ if (viewBox?.length === 4 && (viewBox[2] > ICON_MAX_VIEWBOX || viewBox[3] > ICON_MAX_VIEWBOX))
1528
+ return false;
1529
+ return ![svg, ...descendants].some((node) => isAnimated(node) || node !== svg && node.props.some((prop) => prop.type === NodeTypes.DIRECTIVE));
1530
+ }
1374
1531
  function checkNativeElement(element, context) {
1375
- if (element.tag === "svg" && element.children.some((child) => isElement(child) && ["path", "circle", "rect", "polyline", "line"].includes(child.tag))) {
1532
+ if (element.tag === "svg" && isIconLikeSvg(element)) {
1376
1533
  context.report({
1377
1534
  rule: "inline-svg",
1378
1535
  severity: "warning",
@@ -1391,10 +1548,10 @@ function checkNativeElement(element, context) {
1391
1548
  }, element.loc.start.offset);
1392
1549
  }
1393
1550
  }
1394
- function checkClassBindings(element, source, context) {
1551
+ function checkClassBindings(element, source, context, inherited) {
1395
1552
  for (const prop of element.props) {
1396
1553
  if (prop.type === NodeTypes.ATTRIBUTE && prop.name === "class" && prop.value) {
1397
- checkClassList(prop.value.content, valueOffset(prop, source), context);
1554
+ checkClassList(prop.value.content, valueOffset(prop, source), context, inherited);
1398
1555
  } else if (prop.type === NodeTypes.ATTRIBUTE && prop.name === "style" && prop.value) {
1399
1556
  checkStyleDeclarations(prop.value.content, valueOffset(prop, source), context);
1400
1557
  } else if (prop.type === NodeTypes.DIRECTIVE && prop.name === "bind" && directiveArg(prop) === "class") {
@@ -1420,17 +1577,18 @@ function checkMazTag(element, mazName, source, context) {
1420
1577
  suggestion: near.length > 0 ? `Did you mean ${near.map((entry) => `\`${entry}\``).join(" or ")}?` : "Use `search_docs` or `find_icons` to find the right name."
1421
1578
  }, element.loc.start.offset);
1422
1579
  }
1423
- function walk(node, source, context) {
1580
+ function walk(node, source, context, background) {
1424
1581
  for (const child of node.children) {
1425
1582
  if (!isElement(child))
1426
1583
  continue;
1427
- const mazName = mazTagName(child.tag);
1584
+ const pascal = /[A-Z-]/.test(child.tag) ? toPascal(child.tag) : void 0;
1585
+ const mazName = pascal && context.localComponents?.has(pascal) ? mazTagName(context.localComponents.get(pascal) ?? "") : mazTagName(child.tag);
1428
1586
  if (mazName)
1429
1587
  checkMazTag(child, mazName, source, context);
1430
1588
  else
1431
1589
  checkNativeElement(child, context);
1432
- checkClassBindings(child, source, context);
1433
- walk(child, source, context);
1590
+ checkClassBindings(child, source, context, background);
1591
+ walk(child, source, context, backgroundOf(staticAttribute(child, "class")?.value?.content ?? "") ?? background);
1434
1592
  }
1435
1593
  }
1436
1594
  function reviewTemplate(root, source, context) {
@@ -1537,7 +1695,9 @@ function reviewCode(code, options) {
1537
1695
  reviewTemplate(parse(code), code, context);
1538
1696
  } else {
1539
1697
  const root = parse(code, { parseMode: "sfc" });
1540
- for (const block of blocksOf(root)) {
1698
+ const blocks = blocksOf(root);
1699
+ context.localComponents = localComponents(blocks.filter((block) => block.tag === "script").map((block) => blockContent(block).content).join("\n"));
1700
+ for (const block of blocks) {
1541
1701
  if (block.tag === "template") {
1542
1702
  reviewTemplate(block, code, context);
1543
1703
  } else if (block.tag === "script") {
@@ -1742,7 +1902,7 @@ Workflow:
1742
1902
  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").
1743
1903
  2. get_component_api(["MazBtn", ...]) BEFORE writing a template with Maz* components: use only documented props, values, events and slots.
1744
1904
  3. find_icons("<meaning>") for icons: never inline SVG nor another icon library.
1745
- 4. get_theme_tokens() BEFORE styling: 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-*.
1905
+ 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-*.
1746
1906
  5. review_code({ paths: [...] }) on the files you changed, then fix every error and warning.
1747
1907
  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).
1748
1908
 
@@ -2042,6 +2202,464 @@ const getSetupGuideTool = defineTool({
2042
2202
  run: getSetupGuide
2043
2203
  });
2044
2204
 
2205
+ function sourceOf(node, source) {
2206
+ return source.slice(node.start ?? 0, node.end ?? 0).replaceAll(/\s+/g, " ");
2207
+ }
2208
+ function keyOf(node) {
2209
+ if (node.type !== "ObjectProperty" || node.computed)
2210
+ return void 0;
2211
+ if (node.key.type === "Identifier")
2212
+ return node.key.name;
2213
+ return node.key.type === "StringLiteral" || node.key.type === "NumericLiteral" ? String(node.key.value) : void 0;
2214
+ }
2215
+ function unwrap(node) {
2216
+ return node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSNonNullExpression" || node.type === "ParenthesizedExpression" ? unwrap(node.expression) : node;
2217
+ }
2218
+ function literalValue(node, source) {
2219
+ const expression = unwrap(node);
2220
+ switch (expression.type) {
2221
+ case "StringLiteral":
2222
+ case "NumericLiteral":
2223
+ case "BooleanLiteral":
2224
+ return expression.value;
2225
+ case "NullLiteral":
2226
+ return null;
2227
+ case "TemplateLiteral":
2228
+ return expression.expressions.length === 0 ? expression.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join("") : `<expr: ${sourceOf(expression, source)}>`;
2229
+ case "ArrayExpression":
2230
+ return expression.elements.map((element) => element && element.type !== "SpreadElement" ? literalValue(element, source) : null);
2231
+ case "ObjectExpression":
2232
+ return Object.fromEntries(expression.properties.flatMap((property) => {
2233
+ const key = keyOf(property);
2234
+ return key && property.type === "ObjectProperty" ? [[key, literalValue(property.value, source)]] : [];
2235
+ }));
2236
+ default:
2237
+ return `<expr: ${sourceOf(expression, source)}>`;
2238
+ }
2239
+ }
2240
+ function isDefinePreset(node) {
2241
+ return node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "definePreset";
2242
+ }
2243
+ function findPresetCalls(node, found, name) {
2244
+ if (isDefinePreset(node)) {
2245
+ found.push({ call: node, name });
2246
+ return;
2247
+ }
2248
+ const variableName = node.type === "VariableDeclarator" && node.id.type === "Identifier" ? node.id.name : name;
2249
+ for (const [key, value] of Object.entries(node)) {
2250
+ if (key === "loc" || key === "leadingComments" || key === "trailingComments" || key === "innerComments")
2251
+ continue;
2252
+ for (const child of Array.isArray(value) ? value : [value]) {
2253
+ if (child && typeof child === "object" && typeof child.type === "string")
2254
+ findPresetCalls(child, found, variableName);
2255
+ }
2256
+ }
2257
+ }
2258
+ function readPresets(file, source) {
2259
+ let program;
2260
+ try {
2261
+ program = parse$1(source, { sourceType: "module", plugins: ["typescript"], errorRecovery: true }).program;
2262
+ } catch {
2263
+ return [];
2264
+ }
2265
+ const calls = [];
2266
+ findPresetCalls(program, calls);
2267
+ return calls.flatMap(({ call, name }) => {
2268
+ const [argument] = call.arguments;
2269
+ if (!argument || argument.type !== "ObjectExpression")
2270
+ return [];
2271
+ const options = literalValue(argument, source);
2272
+ const base = options.base;
2273
+ const overrides = options.overrides;
2274
+ return [{
2275
+ file,
2276
+ exportName: name,
2277
+ base: typeof base === "string" ? base.replace(/^<expr: (.*)>$/, "$1") : void 0,
2278
+ overrides: overrides && typeof overrides === "object" && !Array.isArray(overrides) ? overrides : {}
2279
+ }];
2280
+ });
2281
+ }
2282
+
2283
+ const BLOCK_AT_RULES = /^@(?:utility|keyframes|custom-variant)\s/;
2284
+ function blankComments(css, comments) {
2285
+ return css.replaceAll(/\/\*([\s\S]*?)\*\//g, (match, text, index) => {
2286
+ comments.push({ end: index + match.length, text: text.replaceAll(/\s*\n\s*\*?\s*/g, " ").trim() });
2287
+ return match.replaceAll(/[^\n]/g, " ");
2288
+ });
2289
+ }
2290
+ function commentBefore(css, comments, start) {
2291
+ const comment = comments.findLast((entry) => entry.end <= start);
2292
+ return comment && css.slice(comment.end, start).trim() === "" ? comment.text : void 0;
2293
+ }
2294
+ function parseCss(source) {
2295
+ const comments = [];
2296
+ const css = blankComments(source, comments);
2297
+ const result = { declarations: [], statements: [], blocks: [] };
2298
+ const stack = [];
2299
+ let start = 0;
2300
+ let parens = 0;
2301
+ let quote;
2302
+ const flush = (end) => {
2303
+ const raw = css.slice(start, end);
2304
+ const statement = raw.trim();
2305
+ const position = start + raw.indexOf(statement);
2306
+ if (statement.startsWith("@")) {
2307
+ result.statements.push(statement.replaceAll(/\s+/g, " "));
2308
+ return;
2309
+ }
2310
+ const declaration = /^(--[\w*-]+|[a-z-]+)\s*:([\s\S]*)$/.exec(statement);
2311
+ if (declaration && stack.length > 0) {
2312
+ result.declarations.push({
2313
+ property: declaration[1],
2314
+ value: declaration[2].trim().replaceAll(/\s+/g, " "),
2315
+ context: stack.map((entry) => entry.prelude),
2316
+ comment: commentBefore(css, comments, position)
2317
+ });
2318
+ }
2319
+ };
2320
+ for (let index = 0; index < css.length; index++) {
2321
+ const char = css[index];
2322
+ if (quote) {
2323
+ if (char === quote && css[index - 1] !== "\\")
2324
+ quote = void 0;
2325
+ continue;
2326
+ }
2327
+ if (char === '"' || char === "'") {
2328
+ quote = char;
2329
+ } else if (char === "(") {
2330
+ parens++;
2331
+ } else if (char === ")") {
2332
+ parens = Math.max(0, parens - 1);
2333
+ } else if (parens > 0) {
2334
+ continue;
2335
+ } else if (char === ";") {
2336
+ flush(index);
2337
+ start = index + 1;
2338
+ } else if (char === "{") {
2339
+ stack.push({ prelude: css.slice(start, index).trim().replaceAll(/\s+/g, " "), open: index });
2340
+ start = index + 1;
2341
+ } else if (char === "}") {
2342
+ flush(index);
2343
+ start = index + 1;
2344
+ const block = stack.pop();
2345
+ if (block && BLOCK_AT_RULES.test(block.prelude))
2346
+ result.blocks.push({ prelude: block.prelude, body: css.slice(block.open + 1, index).trim().replaceAll(/\s+/g, " ") });
2347
+ }
2348
+ }
2349
+ return result;
2350
+ }
2351
+ const NAMESPACES = [
2352
+ { prefix: "--color-", topic: "colors", usage: (name) => `bg-${name} text-${name} border-${name}...` },
2353
+ { prefix: "--font-weight-", topic: "typography", usage: (name) => `font-${name}` },
2354
+ { prefix: "--font-", topic: "typography", usage: (name) => `font-${name}` },
2355
+ { prefix: "--text-shadow-", topic: "shadows", usage: (name) => `text-shadow-${name}` },
2356
+ { prefix: "--text-", topic: "typography", usage: (name) => `text-${name}` },
2357
+ { prefix: "--tracking-", topic: "typography", usage: (name) => `tracking-${name}` },
2358
+ { prefix: "--leading-", topic: "typography", usage: (name) => `leading-${name}` },
2359
+ { prefix: "--animate-", topic: "typography", usage: (name) => `animate-${name}` },
2360
+ { prefix: "--ease-", topic: "typography", usage: (name) => `ease-${name}` },
2361
+ { prefix: "--duration-", topic: "typography", usage: (name) => `duration-(--duration-${name})` },
2362
+ { prefix: "--radius-", topic: "radius", usage: (name) => `rounded-${name}` },
2363
+ { prefix: "--inset-shadow-", topic: "shadows", usage: (name) => `inset-shadow-${name}` },
2364
+ { prefix: "--drop-shadow-", topic: "shadows", usage: (name) => `drop-shadow-${name}` },
2365
+ { prefix: "--shadow-", topic: "shadows", usage: (name) => `shadow-${name}` },
2366
+ { prefix: "--spacing-", topic: "spacing", usage: (name) => `p-${name} gap-${name} w-${name}...` },
2367
+ { prefix: "--breakpoint-", topic: "breakpoints", usage: (name) => `${name}:` },
2368
+ { prefix: "--container-", topic: "layout", usage: (name) => `max-w-${name} @${name}:` },
2369
+ { prefix: "--aspect-", topic: "layout", usage: (name) => `aspect-${name}` },
2370
+ { prefix: "--z-", topic: "z-index", usage: (name) => `z-${name}` }
2371
+ ];
2372
+ function tokenOf(variable, value, { inline = false, comment } = {}) {
2373
+ if (variable === "--spacing")
2374
+ return { variable, value, topic: "spacing", usage: "p-4 gap-2 w-8... (base step)", inline, comment };
2375
+ const namespace = NAMESPACES.find((entry) => variable.startsWith(entry.prefix));
2376
+ 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 };
2377
+ }
2378
+ function importPath(statement) {
2379
+ return /^@import\s+(?:url\()?\s*["']([^"']+)["']/.exec(statement)?.[1];
2380
+ }
2381
+ function themeContext(context) {
2382
+ return context.find((prelude) => /^@theme\b/.test(prelude));
2383
+ }
2384
+ function readStylesheet(file, source) {
2385
+ const { declarations, statements, blocks } = parseCss(source);
2386
+ const stylesheet = { file, imports: [], tokens: [], resets: [], variants: [], utilities: [], keyframes: [], mazVariables: [] };
2387
+ for (const statement of statements) {
2388
+ const path = importPath(statement);
2389
+ const variant = /^@custom-variant ([\w-]+) (.+)$/.exec(statement);
2390
+ if (path)
2391
+ stylesheet.imports.push(path);
2392
+ else if (variant)
2393
+ stylesheet.variants.push({ name: variant[1], definition: variant[2] });
2394
+ }
2395
+ for (const { prelude, body } of blocks) {
2396
+ const [, kind, name] = /^@([\w-]+) ([\w*-]+)/.exec(prelude) ?? [];
2397
+ if (kind === "utility")
2398
+ stylesheet.utilities.push({ name, css: body });
2399
+ else if (kind === "keyframes")
2400
+ stylesheet.keyframes.push(name);
2401
+ else if (kind === "custom-variant")
2402
+ stylesheet.variants.push({ name, definition: `{ ${body} }` });
2403
+ }
2404
+ for (const { property, value, context, comment } of declarations) {
2405
+ const theme = themeContext(context);
2406
+ if (theme && property.startsWith("--")) {
2407
+ if (value === "initial")
2408
+ stylesheet.resets.push(property);
2409
+ else if (!/^--[\w-]+--/.test(property))
2410
+ stylesheet.tokens.push(tokenOf(property, value, { inline: /\binline\b/.test(theme), comment }));
2411
+ } else if (!theme && property.startsWith("--maz-")) {
2412
+ stylesheet.mazVariables.push({ variable: property, value, selector: context.filter((prelude) => !prelude.startsWith("@layer")).join(" ") });
2413
+ }
2414
+ }
2415
+ return stylesheet;
2416
+ }
2417
+ function isTailwindStylesheet(source) {
2418
+ return /@import\s+(?:url\()?\s*["'](?:tailwindcss|maz-ui\/tailwindcss)/.test(source) || /@(?:theme|custom-variant|utility)\b/.test(source);
2419
+ }
2420
+
2421
+ const IGNORED = [
2422
+ "**/node_modules/**",
2423
+ "**/dist/**",
2424
+ "**/build/**",
2425
+ "**/coverage/**",
2426
+ "**/public/**",
2427
+ "**/.*/**",
2428
+ "**/*.min.css",
2429
+ "**/*.d.ts",
2430
+ "**/*.{spec,test}.*"
2431
+ ];
2432
+ const MAX_FILE_SIZE = 3e5;
2433
+ const PATTERNS = ["*.css", "*.ts", "*.mts", "*.cts", "*.js", "*.mjs"];
2434
+ const IGNORED_PATH = /(?:^|\/)(?:node_modules|dist|build|coverage|public|fixtures|__tests__|\.[^/]+)\/|\.min\.css$|\.d\.ts$|\.(?:spec|test)\.\w+$/;
2435
+ function projectFiles(rootDir) {
2436
+ try {
2437
+ 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));
2438
+ } catch {
2439
+ return globSync(PATTERNS.map((pattern) => `**/${pattern}`), { cwd: rootDir, ignore: IGNORED, deep: 8 }).filter((file) => !IGNORED_PATH.test(file));
2440
+ }
2441
+ }
2442
+ function readSmallFile(path) {
2443
+ try {
2444
+ return statSync(path).size <= MAX_FILE_SIZE ? readFileSync(path, "utf8") : void 0;
2445
+ } catch {
2446
+ return void 0;
2447
+ }
2448
+ }
2449
+ function readProjectTheme(rootDir) {
2450
+ if (!existsSync(join(rootDir, "package.json")))
2451
+ return { stylesheets: [], presets: [] };
2452
+ const files = projectFiles(rootDir).sort();
2453
+ const theme = { stylesheets: [], presets: [] };
2454
+ for (const file of files) {
2455
+ const source = readSmallFile(join(rootDir, file));
2456
+ if (!source)
2457
+ continue;
2458
+ if (file.endsWith(".css")) {
2459
+ if (isTailwindStylesheet(source))
2460
+ theme.stylesheets.push(readStylesheet(file, source));
2461
+ } else if (source.includes("definePreset(")) {
2462
+ theme.presets.push(...readPresets(file, source));
2463
+ }
2464
+ }
2465
+ return theme;
2466
+ }
2467
+ const FOUNDATION_USAGE = {
2468
+ "base-font-size": { topic: "typography", usage: "root font size: every `rem` and the `text-*` sizes" },
2469
+ "border-width": { topic: "typography", usage: "`border` (default width)" },
2470
+ "font-family": { topic: "typography", usage: "`font-sans` (default font)" },
2471
+ "font-mono-stack": { topic: "typography", usage: "`font-mono`" },
2472
+ "font-display-stack": { topic: "typography", usage: "`font-display`" },
2473
+ "motion-fast": { topic: "typography", usage: "`duration-(--duration-fast)`" },
2474
+ "motion-normal": { topic: "typography", usage: "`duration-(--duration-normal)` and the default transition duration" },
2475
+ "motion-slow": { topic: "typography", usage: "`duration-(--duration-slow)`" },
2476
+ "easing-out": { topic: "typography", usage: "`ease-out` and the default transition easing" },
2477
+ "easing-in": { topic: "typography", usage: "`ease-in`" },
2478
+ "easing-in-out": { topic: "typography", usage: "`ease-in-out`" },
2479
+ "disabled-opacity": { topic: "utilities", usage: "`disabled-opacity`, `disabled-state`" },
2480
+ "disabled-cursor": { topic: "utilities", usage: "`disabled-cursor`, `disabled-state`" },
2481
+ "space": { topic: "spacing", usage: "every spacing utility (`p-4` = 4 x space)" }
2482
+ };
2483
+ const COMPONENT_USAGE = {
2484
+ "btn.font-weight": "font weight of MazBtn",
2485
+ "container.bg": "`bg-container`: MazCard, MazContainer, MazDialog, MazPopover, MazDrawer...",
2486
+ "input.bg": "`bg-input`: MazInput, MazSelect, MazTextarea, MazCheckbox...",
2487
+ "input.top-label-font-weight": "font weight of the `top-label` of the inputs",
2488
+ "dialog.max-width": "max width of MazDialog",
2489
+ "dialog.min-width": "min width of MazDialog"
2490
+ };
2491
+ function code(value) {
2492
+ return `\`${value.replaceAll("`", "'").replaceAll("|", "\\|")}\``;
2493
+ }
2494
+ function shorten(value, max = 70) {
2495
+ return value.length > max ? `${value.slice(0, max - 3)}...` : value;
2496
+ }
2497
+ function display(value) {
2498
+ if (value === void 0)
2499
+ return "-";
2500
+ return code(shorten(typeof value === "object" && value !== null ? JSON.stringify(value) : String(value)));
2501
+ }
2502
+ function asRecord(value) {
2503
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2504
+ }
2505
+ const COLOR_CLASSES = {
2506
+ shadow: "`shadow-shadow/20`",
2507
+ overlay: "`bg-overlay`",
2508
+ divider: "`border-divider`"
2509
+ };
2510
+ function colorClasses(name) {
2511
+ if (COLOR_CLASSES[name])
2512
+ return COLOR_CLASSES[name];
2513
+ return name.endsWith("-foreground") ? code(`text-${name}`) : `${code(`bg-${name}`)} ${code(`text-${name}`)}`;
2514
+ }
2515
+ function presetColors(preset) {
2516
+ const colors = asRecord(preset.overrides.colors);
2517
+ const light = asRecord(colors.light);
2518
+ const dark = asRecord(colors.dark);
2519
+ const names = [.../* @__PURE__ */ new Set([...Object.keys(light), ...Object.keys(dark)])];
2520
+ if (names.length === 0)
2521
+ return [];
2522
+ return [
2523
+ "| Color | Classes | Light | Dark |",
2524
+ "| --- | --- | --- | --- |",
2525
+ ...names.map((name) => `| \`${name}\` | ${colorClasses(name)} | ${display(light[name])} | ${display(dark[name])} |`)
2526
+ ];
2527
+ }
2528
+ function presetFoundation(preset, wanted) {
2529
+ 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)} |`);
2530
+ return rows.length > 0 ? ["| Foundation | CSS variable | Used by | Value |", "| --- | --- | --- | --- |", ...rows] : [];
2531
+ }
2532
+ function presetScales(preset, wanted) {
2533
+ const scales = asRecord(preset.overrides.scales);
2534
+ const rows = [
2535
+ ...wanted("radius") ? Object.entries(asRecord(scales.rounded)).map(([key, value]) => `| ${code(`rounded-${key}`)} | ${display(value)} |`) : [],
2536
+ ...wanted("shadows") ? Object.entries(asRecord(scales.shadow)).map(([key, value]) => `| ${code(`shadow-${key}`)} | ${display(value)} |`) : []
2537
+ ];
2538
+ return rows.length > 0 ? ["| Class | Value |", "| --- | --- |", ...rows] : [];
2539
+ }
2540
+ function flatten(value, path) {
2541
+ const record = asRecord(value);
2542
+ const keys = Object.keys(record);
2543
+ if (keys.length === 0 || keys.every((key) => key === "light" || key === "dark"))
2544
+ return [[path, value]];
2545
+ return keys.flatMap((key) => flatten(record[key], path ? `${path}.${key}` : key));
2546
+ }
2547
+ function presetComponents(preset) {
2548
+ const rows = flatten(asRecord(preset.overrides.components), "").filter(([path]) => path).map(([path, value]) => `| \`components.${path}\` | ${COMPONENT_USAGE[path] ?? "-"} | ${display(value)} |`);
2549
+ return rows.length > 0 ? ["| Component token | Used by | Value |", "| --- | --- | --- |", ...rows] : [];
2550
+ }
2551
+ function renderPreset(preset, wanted, filtered) {
2552
+ const name = typeof preset.overrides.name === "string" ? ` \`${preset.overrides.name}\`` : "";
2553
+ const origin = [preset.exportName && `\`${preset.exportName}\``, preset.base && `base \`${preset.base}\``].filter(Boolean).join(", ");
2554
+ const tables = [
2555
+ wanted("colors") ? presetColors(preset) : [],
2556
+ presetFoundation(preset, wanted),
2557
+ presetScales(preset, wanted),
2558
+ wanted("colors") ? presetComponents(preset) : []
2559
+ ].filter((table) => table.length > 0);
2560
+ if (filtered && tables.length === 0)
2561
+ return void 0;
2562
+ return [
2563
+ `### Preset${name} - \`${preset.file}\`${origin ? ` (${origin})` : ""}`,
2564
+ "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.",
2565
+ ...tables.map((table) => table.join("\n"))
2566
+ ].join("\n\n");
2567
+ }
2568
+ function mazUiThemeVariables(tailwind) {
2569
+ const variables = /* @__PURE__ */ new Map();
2570
+ const named = (entries, toVariable) => {
2571
+ for (const entry of entries) variables.set(toVariable(entry.name), entry.value);
2572
+ };
2573
+ for (const color of tailwind.colors) variables.set(`--color-${color.name}`, `var(${color.variable})`);
2574
+ named(tailwind.fonts, (name) => `--${name}`);
2575
+ named(tailwind.radius, (name) => name.replace(/^rounded-/, "--radius-"));
2576
+ named(tailwind.shadows, (name) => `--${name}`);
2577
+ named(tailwind.breakpoints, (name) => `--breakpoint-${name.replace(/:$/, "")}`);
2578
+ named(tailwind.easings, (name) => `--${name}`);
2579
+ named(tailwind.zIndex, (name) => `--${name}`);
2580
+ named(tailwind.durations, (name) => /--duration-[\w-]+/.exec(name)?.[0] ?? name);
2581
+ return variables;
2582
+ }
2583
+ function tokenNote(token, mazUi) {
2584
+ const original = mazUi.get(token.variable);
2585
+ const notes = [
2586
+ token.comment,
2587
+ original && original !== token.value ? `Redefines the maz-ui token (maz-ui: ${code(shorten(original, 50))}): this class no longer follows the preset.` : void 0
2588
+ ];
2589
+ return notes.filter(Boolean).join(" ").replaceAll("|", "\\|") || "-";
2590
+ }
2591
+ function renderStylesheet(stylesheet, mazUi, wanted, animated) {
2592
+ const sections = [];
2593
+ const problems = stylesheetProblems(stylesheet);
2594
+ if (problems.length > 0)
2595
+ sections.push(problems.map((problem) => `> ${problem}`).join("\n>\n"));
2596
+ const tokens = stylesheet.tokens.filter((token) => wanted(token.topic));
2597
+ if (tokens.length > 0) {
2598
+ sections.push([
2599
+ `#### Project tokens (\`@theme\`)`,
2600
+ "",
2601
+ "| Classes | Variable | Value | Note |",
2602
+ "| --- | --- | --- | --- |",
2603
+ ...tokens.map((token) => `| ${code(token.usage)} | \`${token.variable}\` | ${code(shorten(token.value, 60))} | ${tokenNote(token, mazUi)} |`)
2604
+ ].join("\n"));
2605
+ }
2606
+ if (stylesheet.resets.length > 0 && wanted("colors"))
2607
+ sections.push(`Reset namespaces: ${stylesheet.resets.map((reset) => code(reset)).join(", ")}: the default Tailwind tokens of these namespaces do not exist.`);
2608
+ if (stylesheet.variants.length > 0 && (wanted("variants") || wanted("breakpoints")))
2609
+ sections.push(["#### Custom variants", "", "| Variant | Definition |", "| --- | --- |", ...stylesheet.variants.map((variant) => `| \`${variant.name}:\` | ${code(shorten(variant.definition))} |`)].join("\n"));
2610
+ if (stylesheet.utilities.length > 0 && wanted("utilities"))
2611
+ sections.push(["#### Custom utilities", "", "| Class | CSS |", "| --- | --- |", ...stylesheet.utilities.map((utility) => `| \`${utility.name}\` | ${code(shorten(utility.css))} |`)].join("\n"));
2612
+ const keyframes = stylesheet.keyframes.filter((name) => !animated.has(name));
2613
+ if (keyframes.length > 0 && wanted("typography"))
2614
+ 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.`);
2615
+ if (stylesheet.mazVariables.length > 0 && (wanted("layout") || wanted("css-variables"))) {
2616
+ sections.push([
2617
+ "#### maz-ui variables set in CSS",
2618
+ "",
2619
+ "| Variable | Value | Selector |",
2620
+ "| --- | --- | --- |",
2621
+ ...stylesheet.mazVariables.map((variable) => `| \`${variable.variable}\` | ${code(shorten(variable.value, 50))} | ${code(variable.selector)} |`)
2622
+ ].join("\n"));
2623
+ }
2624
+ return sections;
2625
+ }
2626
+ function renderStylesheetSection(stylesheet, mazUi, topics, animated) {
2627
+ const sections = renderStylesheet(stylesheet, mazUi, (topic) => !topics?.length || topics.includes(topic), animated);
2628
+ if (topics?.length && sections.length === 0)
2629
+ return void 0;
2630
+ const imports = stylesheet.imports.length > 0 ? [`Imports: ${stylesheet.imports.map((path) => code(path)).join(", ")}`] : [];
2631
+ return [`### Tailwind \`${stylesheet.file}\``, ...imports, ...sections].join("\n\n");
2632
+ }
2633
+ function stylesheetProblems(stylesheet) {
2634
+ if (!stylesheet.imports.includes("tailwindcss"))
2635
+ return [];
2636
+ const mazUiImports = stylesheet.imports.filter((path) => path.startsWith("maz-ui/tailwindcss"));
2637
+ const problems = [];
2638
+ if (mazUiImports.length === 0)
2639
+ 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';`.");
2640
+ else if (!mazUiImports.includes("maz-ui/tailwindcss/utilities.css"))
2641
+ problems.push("`maz-ui/tailwindcss/utilities.css` is not imported: `flex-center`, `padded-container`, `disabled-state`... do not exist.");
2642
+ return problems;
2643
+ }
2644
+ const HOW_IT_WORKS = `How your theme combines with maz-ui:
2645
+
2646
+ 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.
2647
+ 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\`.
2648
+ 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.`;
2649
+ function renderProjectTheme(theme, tailwind, topics) {
2650
+ if (theme.stylesheets.length === 0 && theme.presets.length === 0)
2651
+ return void 0;
2652
+ const wanted = (topic) => !topics?.length || topics.includes(topic);
2653
+ const mazUi = mazUiThemeVariables(tailwind);
2654
+ const animated = new Set(theme.stylesheets.flatMap((stylesheet) => stylesheet.tokens.filter((token) => token.variable.startsWith("--animate-")).map((token) => token.value.split(/\s/)[0])));
2655
+ return [
2656
+ "## Your project theme",
2657
+ HOW_IT_WORKS,
2658
+ ...theme.presets.map((preset) => renderPreset(preset, wanted, !!topics?.length)),
2659
+ ...theme.stylesheets.map((stylesheet) => renderStylesheetSection(stylesheet, mazUi, topics, animated))
2660
+ ].filter(Boolean).join("\n\n");
2661
+ }
2662
+
2045
2663
  const THEME_TOPICS = {
2046
2664
  "setup": "Setup",
2047
2665
  "colors": "Colors",
@@ -2057,28 +2675,32 @@ const THEME_TOPICS = {
2057
2675
  "transitions": "Vue transitions",
2058
2676
  "css-variables": "CSS variables in custom CSS"
2059
2677
  };
2060
- function getThemeTokens(context, { topics }) {
2678
+ function getThemeTokens(context, { topics, project = true }) {
2061
2679
  const reference = context.knowledge.docsById.get("reference/tailwind-tokens");
2062
2680
  if (!reference)
2063
2681
  throw new ToolInputError('This maz-ui version does not ship the Tailwind reference. Read `get_doc({ id: "guide/tailwind" })`.');
2064
2682
  const markdown = stripFrontmatter(context.knowledge.readDoc(reference.id));
2065
- const footer = '\n\nTo extend the theme (your own colors, fonts, animations, variants, container width): `get_doc({ id: "guide/tailwind" })`. Brand colors and radius belong to a preset: `get_doc({ id: "ecosystem/themes", sections: ["Creating Custom Themes"] })`.';
2066
- if (!topics?.length)
2067
- return `${markdown.trim()}${footer}`;
2068
- const parts = topics.map((topic) => {
2683
+ const projectTheme = project ? renderProjectTheme(readProjectTheme(context.cwd), context.knowledge.manifest.tailwind, topics) : void 0;
2684
+ 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"] })`.';
2685
+ const mazUiTheme = topics?.length ? `# maz-ui ${context.knowledge.version} Tailwind theme
2686
+
2687
+ ${topics.map((topic) => {
2069
2688
  const section = findSection(markdown, THEME_TOPICS[topic]);
2070
2689
  return section ? sectionContent(markdown, section) : "";
2071
- });
2072
- return `# maz-ui ${context.knowledge.version} Tailwind theme
2690
+ }).filter(Boolean).join("\n\n")}` : markdown.trim();
2691
+ return projectTheme ? `${projectTheme}
2692
+
2693
+ ---
2073
2694
 
2074
- ${parts.filter(Boolean).join("\n\n")}${footer}`;
2695
+ ${mazUiTheme}${footer}` : `${mazUiTheme}${footer}`;
2075
2696
  }
2076
2697
  const getThemeTokensTool = defineTool({
2077
2698
  name: "get_theme_tokens",
2078
2699
  title: "maz-ui Tailwind theme tokens",
2079
- description: "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.",
2700
+ 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.",
2080
2701
  inputSchema: {
2081
- topics: z.array(z.enum(Object.keys(THEME_TOPICS))).optional().describe("Only these topics (default: everything)")
2702
+ topics: z.array(z.enum(Object.keys(THEME_TOPICS))).optional().describe("Only these topics (default: everything)"),
2703
+ project: z.boolean().optional().describe("Include the theme of the project: preset, `@theme` tokens, variants, utilities (default: true)")
2082
2704
  },
2083
2705
  run: getThemeTokens
2084
2706
  });
@@ -2145,7 +2767,7 @@ async function runTool(context, tool, args) {
2145
2767
  function createMazUiMcpServer(options = {}) {
2146
2768
  const context = createContext(options);
2147
2769
  const server = new McpServer(
2148
- { name: "maz-ui", title: "maz-ui", version },
2770
+ { name: "maz-ui", title: "maz-ui", version: readOwnVersion() },
2149
2771
  { instructions: buildInstructions(context) }
2150
2772
  );
2151
2773
  for (const tool of TOOLS) {
@@ -2172,7 +2794,7 @@ async function startStdioServer(options = {}) {
2172
2794
  return server;
2173
2795
  }
2174
2796
 
2175
- const HELP = `maz-ui-mcp ${version}: maz-ui knowledge for AI coding agents
2797
+ const HELP = `maz-ui-mcp: maz-ui knowledge for AI coding agents
2176
2798
 
2177
2799
  Usage:
2178
2800
  maz-ui-mcp [serve] [--docs <dir>] Start the MCP server (stdio)
@@ -2309,7 +2931,7 @@ async function runCli(argv = process.argv.slice(2)) {
2309
2931
  return 0;
2310
2932
  }
2311
2933
  if (values.version) {
2312
- console.log(version);
2934
+ console.log(readOwnVersion());
2313
2935
  return 0;
2314
2936
  }
2315
2937
  switch (command) {