@testgorilla/tgo-ui 8.12.0 → 8.13.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/mcp/server.mjs CHANGED
@@ -21634,7 +21634,7 @@ function registerListStoriesWithDesignLinksTool(server, store) {
21634
21634
  function registerGetIconForFigmaNameTool(server, resolver) {
21635
21635
  server.tool(
21636
21636
  "get_icon_for_figma_name",
21637
- 'Resolve a Figma icon name (as exported by Figma Dev Mode) to a Canopy `<ui-icon>` name + svg path. Returns null when no match is found at any level. The `match` field tells you how the resolution happened: "exact" (after case/punctuation normalization), "alias" (manual entry in mcp/icon-aliases.json), or "fuzzy" (substring match \u2014 verify visually before relying on it).',
21637
+ 'Resolve a Figma icon name (as exported by Figma Dev Mode, e.g. "Assessment/Timer") to a Canopy `<ui-icon>` name + svg path. The Figma "Category/" prefix is stripped automatically. Returns null when no match is found at any level. The `match` field tells you how the resolution happened: "mapped" (curated entry in mcp/icon-figma-map.json), "exact" (after prefix/case/punctuation normalization), "alias" (generic synonym in the same file), or "fuzzy" (substring match). The `confident` boolean is true for exact/alias and false for fuzzy \u2014 treat a non-confident result as "needs a human mapping decision" and verify visually before relying on it.',
21638
21638
  {
21639
21639
  figmaIconName: external_exports.string().describe('Icon name from Figma (e.g., "checkmark", "Arrow-down", "Eye Open"). Case- and punctuation-insensitive.')
21640
21640
  },
@@ -21647,6 +21647,41 @@ function registerGetIconForFigmaNameTool(server, resolver) {
21647
21647
  );
21648
21648
  }
21649
21649
 
21650
+ // src/icons/icon-report.ts
21651
+ function buildIconNameReport(resolver, names) {
21652
+ const seen = /* @__PURE__ */ new Set();
21653
+ const unique = names.filter((n) => {
21654
+ const key = n.trim().toLowerCase();
21655
+ if (!key || seen.has(key)) return false;
21656
+ seen.add(key);
21657
+ return true;
21658
+ });
21659
+ const verdicts = unique.map((n) => resolver.classify(n));
21660
+ return {
21661
+ total: verdicts.length,
21662
+ confident: verdicts.filter((v) => v.status === "confident").length,
21663
+ knownGaps: verdicts.filter((v) => v.status === "known-gap").length,
21664
+ review: verdicts.filter((v) => v.status === "review")
21665
+ };
21666
+ }
21667
+
21668
+ // src/tools/check-icon-names.ts
21669
+ function registerCheckIconNamesTool(server, resolver) {
21670
+ server.tool(
21671
+ "check_icon_names",
21672
+ 'Batch-audit icon names and return only the ones that need a human mapping decision. Input is a list of names \u2014 Figma export names (e.g. "Assessment/Timer") and/or free-form / natural-language icon words (e.g. "trash", "gear"). The response has counts for `confident` (safe to use) and `knownGaps` (intentionally no code icon \u2014 already decided), plus a `review` array of the actionable names: low-confidence fuzzy guesses and outright misses, each with a `reason`. Use this to flag unresolved icons for review rather than rendering a wrong guess. To resolve a single name to a concrete `<ui-icon>`, use get_icon_for_figma_name instead.',
21673
+ {
21674
+ names: external_exports.array(external_exports.string()).describe("Icon names to check (Figma export names and/or natural-language icon words).")
21675
+ },
21676
+ async ({ names }) => {
21677
+ const report = buildIconNameReport(resolver, names);
21678
+ return {
21679
+ content: [{ type: "text", text: JSON.stringify(report, null, 2) }]
21680
+ };
21681
+ }
21682
+ );
21683
+ }
21684
+
21650
21685
  // src/tools/get-design-tokens.ts
21651
21686
  function handleGetDesignTokens(tokens, params) {
21652
21687
  if (!params.category) return tokens;
@@ -21708,41 +21743,113 @@ var IconResolver = class {
21708
21743
  byNormalized = /* @__PURE__ */ new Map();
21709
21744
  aliasMap;
21710
21745
  unmapped;
21746
+ figmaMap;
21711
21747
  icons;
21712
- constructor(icons, aliases = {}, unmapped = []) {
21748
+ constructor(icons, aliases = {}, unmapped = [], figmaMap = {}) {
21713
21749
  this.icons = icons;
21714
21750
  for (const icon of icons) {
21715
21751
  this.byNormalized.set(normalize(icon.name), icon);
21716
21752
  }
21753
+ for (const icon of icons) {
21754
+ const base = normalize(stripVariantSuffix(icon.name));
21755
+ if (!this.byNormalized.has(base)) {
21756
+ this.byNormalized.set(base, icon);
21757
+ }
21758
+ }
21717
21759
  this.aliasMap = /* @__PURE__ */ new Map();
21718
21760
  for (const [from, to] of Object.entries(aliases)) {
21719
21761
  this.aliasMap.set(normalize(from), to);
21720
21762
  }
21721
21763
  this.unmapped = new Set(unmapped.map(normalize));
21764
+ this.figmaMap = /* @__PURE__ */ new Map();
21765
+ for (const [figmaName, entry] of Object.entries(figmaMap)) {
21766
+ this.figmaMap.set(normalize(figmaName), entry);
21767
+ }
21722
21768
  }
21723
21769
  resolve(figmaName) {
21724
- const normalized = normalize(figmaName);
21770
+ const curated = this.figmaMap.get(normalize(figmaName));
21771
+ if (curated) {
21772
+ if (curated.status === "mapped" && curated.canopy) {
21773
+ const target = this.byNormalized.get(normalize(curated.canopy));
21774
+ if (target) {
21775
+ return { canopyIconName: target.name, svgPath: target.svgPath, match: "mapped", confident: true };
21776
+ }
21777
+ }
21778
+ return null;
21779
+ }
21780
+ const normalized = normalize(stripCategoryPrefix(figmaName));
21725
21781
  if (!normalized) return null;
21726
21782
  const exact = this.byNormalized.get(normalized);
21727
21783
  if (exact) {
21728
- return { canopyIconName: exact.name, svgPath: exact.svgPath, match: "exact" };
21784
+ return { canopyIconName: exact.name, svgPath: exact.svgPath, match: "exact", confident: true };
21729
21785
  }
21730
21786
  const aliasTarget = this.aliasMap.get(normalized);
21731
21787
  if (aliasTarget) {
21732
21788
  const target = this.byNormalized.get(normalize(aliasTarget));
21733
21789
  if (target) {
21734
- return { canopyIconName: target.name, svgPath: target.svgPath, match: "alias" };
21790
+ return { canopyIconName: target.name, svgPath: target.svgPath, match: "alias", confident: true };
21735
21791
  }
21736
21792
  }
21737
21793
  if (this.unmapped.has(normalized)) return null;
21738
21794
  if (normalized.length >= MIN_FUZZY_QUERY_LENGTH) {
21739
21795
  const best = this.bestFuzzyMatch(normalized);
21740
21796
  if (best) {
21741
- return { canopyIconName: best.name, svgPath: best.svgPath, match: "fuzzy" };
21797
+ return { canopyIconName: best.name, svgPath: best.svgPath, match: "fuzzy", confident: false };
21742
21798
  }
21743
21799
  }
21744
21800
  return null;
21745
21801
  }
21802
+ /**
21803
+ * Classify a name for Layer-1 reporting: is it safe to use, does it need a
21804
+ * human mapping decision, or is it an already-decided gap? Used by
21805
+ * `check_icon_names` to surface only the names worth a human's attention —
21806
+ * works for Figma export names AND free-form / natural-language icon words.
21807
+ */
21808
+ classify(figmaName) {
21809
+ const curated = this.figmaMap.get(normalize(figmaName));
21810
+ if (curated) {
21811
+ if (curated.status === "mapped" && curated.canopy) {
21812
+ const target = this.byNormalized.get(normalize(curated.canopy));
21813
+ if (target) {
21814
+ return {
21815
+ name: figmaName,
21816
+ status: "confident",
21817
+ match: "mapped",
21818
+ canopyIconName: target.name,
21819
+ svgPath: target.svgPath,
21820
+ reason: "curated map"
21821
+ };
21822
+ }
21823
+ }
21824
+ const reason = curated.status === "figma-only" ? "Figma-only by design (no code icon)" : curated.status === "add-to-canopy" ? "not built in Canopy yet" : "curated entry has no resolvable target";
21825
+ return { name: figmaName, status: "known-gap", canopyIconName: null, svgPath: null, reason };
21826
+ }
21827
+ const r = this.resolve(figmaName);
21828
+ if (r && r.confident) {
21829
+ return {
21830
+ name: figmaName,
21831
+ status: "confident",
21832
+ match: r.match,
21833
+ canopyIconName: r.canopyIconName,
21834
+ svgPath: r.svgPath,
21835
+ reason: r.match
21836
+ };
21837
+ }
21838
+ if (r && r.match === "fuzzy") {
21839
+ return {
21840
+ name: figmaName,
21841
+ status: "review",
21842
+ match: "fuzzy",
21843
+ canopyIconName: r.canopyIconName,
21844
+ svgPath: r.svgPath,
21845
+ reason: `low-confidence guess (${r.canopyIconName}) \u2014 verify or add a mapping`
21846
+ };
21847
+ }
21848
+ if (this.unmapped.has(normalize(stripCategoryPrefix(figmaName)))) {
21849
+ return { name: figmaName, status: "known-gap", canopyIconName: null, svgPath: null, reason: "no Canopy equivalent (unmapped)" };
21850
+ }
21851
+ return { name: figmaName, status: "review", canopyIconName: null, svgPath: null, reason: "no match found \u2014 needs a mapping or a new icon" };
21852
+ }
21746
21853
  /**
21747
21854
  * Pick the best fuzzy candidate rather than the first one in iteration order.
21748
21855
  * Among icons whose normalized name contains (or is contained by) the query,
@@ -21773,6 +21880,13 @@ function compareFuzzyRank(a, b) {
21773
21880
  function stripSvgExtension(name) {
21774
21881
  return name.toLowerCase().endsWith(".svg") ? name.slice(0, -".svg".length) : name;
21775
21882
  }
21883
+ function stripCategoryPrefix(name) {
21884
+ const slash = name.lastIndexOf("/");
21885
+ return slash >= 0 ? name.slice(slash + 1) : name;
21886
+ }
21887
+ function stripVariantSuffix(name) {
21888
+ return name.replace(/-(filled|in-line)$/i, "");
21889
+ }
21776
21890
  function normalize(name) {
21777
21891
  return stripSvgExtension(name).toLowerCase().replace(/[^a-z0-9]/g, "");
21778
21892
  }
@@ -21796,6 +21910,12 @@ var CatalogSchema = external_exports.object({
21796
21910
  icons: external_exports.array(external_exports.object({ name: external_exports.string(), svgPath: external_exports.string() })).optional().default([]),
21797
21911
  iconAliases: external_exports.record(external_exports.string()).optional().default({}),
21798
21912
  iconUnmapped: external_exports.array(external_exports.string()).optional().default([]),
21913
+ iconFigmaMap: external_exports.record(external_exports.object({
21914
+ canopy: external_exports.string().nullable(),
21915
+ node: external_exports.string().optional(),
21916
+ status: external_exports.enum(["mapped", "figma-only", "add-to-canopy"]),
21917
+ note: external_exports.string().optional()
21918
+ })).optional().default({}),
21799
21919
  tokens: external_exports.array(external_exports.object({ name: external_exports.string(), value: external_exports.string(), category: external_exports.string() })).optional().default([]),
21800
21920
  // Foundations additions (Phase G.1.d). Same default-to-empty pattern.
21801
21921
  typography: external_exports.array(external_exports.object({
@@ -21830,8 +21950,9 @@ function createStaticMcpServer(catalog) {
21830
21950
  registerGetComponentTypesTool(server, store);
21831
21951
  registerGetQueryGuideTool(server, store);
21832
21952
  registerListStoriesWithDesignLinksTool(server, store);
21833
- const iconResolver = new IconResolver(catalog.icons ?? [], catalog.iconAliases ?? {}, catalog.iconUnmapped ?? []);
21953
+ const iconResolver = new IconResolver(catalog.icons ?? [], catalog.iconAliases ?? {}, catalog.iconUnmapped ?? [], catalog.iconFigmaMap ?? {});
21834
21954
  registerGetIconForFigmaNameTool(server, iconResolver);
21955
+ registerCheckIconNamesTool(server, iconResolver);
21835
21956
  registerGetDesignTokensTool(server, catalog.tokens ?? []);
21836
21957
  registerListTypographyClassesTool(server, catalog.typography ?? []);
21837
21958
  registerListColorPalettesTool(server, catalog.colorPalettes ?? []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testgorilla/tgo-ui",
3
- "version": "8.12.0",
3
+ "version": "8.13.0",
4
4
  "license": "proprietary-license",
5
5
  "lint-staged": {
6
6
  "{projects,components}/**/*.ts": [