@sebgroup/green-core 2.21.0-rc.20260113130744922 → 2.21.0-rc.20260113143213999

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.
@@ -18,19 +18,39 @@ function setupToolHandlers(server) {
18
18
  tools: [
19
19
  {
20
20
  name: "search_components",
21
- description: "Search for Green Design System components by name, description, or functionality. Use this when you don't know the exact component name or want to discover available components.",
21
+ description: "Search for Green Design System components by name, description, or functionality. Use this when you don't know the exact component name or want to discover available components. Supports multi-term searches and regex patterns.",
22
22
  inputSchema: {
23
23
  type: "object",
24
24
  properties: {
25
25
  query: {
26
26
  type: "string",
27
- description: "Component name or search term (e.g., 'button', 'dropdown', 'form input', 'arrow icon')"
27
+ description: "Component name or search term. Can be multiple terms separated by spaces/commas (e.g., 'button input radio'). Use regex format (e.g., '^gds-button') if useRegex is true."
28
28
  },
29
29
  category: {
30
30
  type: "string",
31
31
  enum: ["component", "icon", "all"],
32
32
  description: "Filter by type. Default: 'all'",
33
33
  default: "all"
34
+ },
35
+ splitTerms: {
36
+ type: "boolean",
37
+ description: "Split query on spaces and commas to search for multiple terms. Default: true",
38
+ default: true
39
+ },
40
+ matchAll: {
41
+ type: "boolean",
42
+ description: "When splitTerms is true, require ALL terms to match (AND logic). Default: false (OR logic)",
43
+ default: false
44
+ },
45
+ useRegex: {
46
+ type: "boolean",
47
+ description: "Treat query as a regular expression pattern. Default: false",
48
+ default: false
49
+ },
50
+ maxResults: {
51
+ type: "number",
52
+ description: "Maximum number of results to return. Default: 20",
53
+ default: 20
34
54
  }
35
55
  },
36
56
  required: ["query"]
@@ -155,19 +175,93 @@ function setupToolHandlers(server) {
155
175
  });
156
176
  }
157
177
  async function handleSearchComponents(input) {
158
- const { query, category = "all" } = input;
178
+ const {
179
+ query,
180
+ category = "all",
181
+ splitTerms = true,
182
+ matchAll = false,
183
+ useRegex = false,
184
+ maxResults = 20
185
+ } = input;
159
186
  const results = [];
160
- const queryLower = query.toLowerCase();
161
187
  try {
188
+ let searchTerms;
189
+ let regexPattern = null;
190
+ if (useRegex) {
191
+ try {
192
+ if (query.length > 100) {
193
+ throw new Error("Regex pattern too long (max 100 characters)");
194
+ }
195
+ regexPattern = new RegExp(query, "i");
196
+ searchTerms = [query];
197
+ } catch (error) {
198
+ throw new Error(
199
+ `Invalid regex pattern: ${error instanceof Error ? error.message : "unknown error"}`
200
+ );
201
+ }
202
+ } else if (splitTerms) {
203
+ searchTerms = query.toLowerCase().split(/[\s,]+/).filter((term) => term.length > 0);
204
+ } else {
205
+ searchTerms = [query.toLowerCase()];
206
+ }
207
+ if (searchTerms.length === 0) {
208
+ throw new Error("Query cannot be empty");
209
+ }
162
210
  const loadComponents = category === "component" || category === "all";
163
211
  const loadIcons = category === "icon" || category === "all";
164
212
  const [componentsIndex, iconsIndex] = await Promise.all([
165
213
  loadComponents ? loadComponentsIndex() : Promise.resolve(null),
166
214
  loadIcons ? loadIconsIndex() : Promise.resolve(null)
167
215
  ]);
216
+ const checkMatches = (item) => {
217
+ const tagName = item.tagName.toLowerCase();
218
+ const name = item.name.toLowerCase();
219
+ const className = item.className.toLowerCase();
220
+ const description = item.description?.toLowerCase() || "";
221
+ if (regexPattern) {
222
+ const matches2 = regexPattern.test(tagName) || regexPattern.test(name) || regexPattern.test(className) || regexPattern.test(description);
223
+ if (matches2) {
224
+ if (regexPattern.test(tagName)) {
225
+ if (tagName === query.toLowerCase()) return { matches: true, tier: 1, matchedTerms: 1 };
226
+ if (tagName.startsWith(query.toLowerCase())) return { matches: true, tier: 2, matchedTerms: 1 };
227
+ return { matches: true, tier: 3, matchedTerms: 1 };
228
+ }
229
+ return { matches: true, tier: 4, matchedTerms: 1 };
230
+ }
231
+ return { matches: false, tier: 0, matchedTerms: 0 };
232
+ }
233
+ let matchedTerms = 0;
234
+ let bestTier = 999;
235
+ for (const term of searchTerms) {
236
+ let termMatched = false;
237
+ let termTier = 999;
238
+ if (tagName === `gds-${term}` || tagName === term) {
239
+ termMatched = true;
240
+ termTier = Math.min(termTier, 1);
241
+ } else if (tagName.startsWith(term) || tagName.startsWith(`gds-${term}`)) {
242
+ termMatched = true;
243
+ termTier = Math.min(termTier, 2);
244
+ } else if (tagName.includes(term)) {
245
+ termMatched = true;
246
+ termTier = Math.min(termTier, 3);
247
+ } else if (name.includes(term) || className.includes(term)) {
248
+ termMatched = true;
249
+ termTier = Math.min(termTier, 4);
250
+ } else if (description.includes(term)) {
251
+ termMatched = true;
252
+ termTier = Math.min(termTier, 5);
253
+ }
254
+ if (termMatched) {
255
+ matchedTerms++;
256
+ bestTier = Math.min(bestTier, termTier);
257
+ }
258
+ }
259
+ const matches = matchAll ? matchedTerms === searchTerms.length : matchedTerms > 0;
260
+ return { matches, tier: matches ? bestTier : 999, matchedTerms };
261
+ };
168
262
  if (componentsIndex) {
169
263
  for (const component of componentsIndex.components) {
170
- const matches = component.tagName.toLowerCase().includes(queryLower) || component.name.toLowerCase().includes(queryLower) || component.className.toLowerCase().includes(queryLower) || component.description?.toLowerCase().includes(queryLower);
264
+ const { matches, tier, matchedTerms } = checkMatches(component);
171
265
  if (matches) {
172
266
  const shortName = component.tagName.replace(/^gds-/, "");
173
267
  const resourceUris = {};
@@ -185,23 +279,21 @@ async function handleSearchComponents(input) {
185
279
  description: component.description,
186
280
  category: "component",
187
281
  availableDocs: component.files,
188
- resourceUris
282
+ resourceUris,
283
+ tier,
284
+ matchedTerms
189
285
  });
190
286
  }
191
287
  }
192
288
  }
193
289
  if (iconsIndex) {
194
290
  for (const icon of iconsIndex.icons) {
195
- const matches = icon.tagName.toLowerCase().includes(queryLower) || icon.name.toLowerCase().includes(queryLower) || icon.className.toLowerCase().includes(queryLower) || icon.description?.toLowerCase().includes(queryLower);
291
+ const { matches, tier, matchedTerms } = checkMatches(icon);
196
292
  if (matches) {
197
293
  const shortName = icon.tagName.replace(/^gds-/, "");
198
294
  const resourceUris = {};
199
295
  for (const docType of icon.files) {
200
- resourceUris[docType] = buildResourceUri(
201
- "icons",
202
- shortName,
203
- docType
204
- );
296
+ resourceUris[docType] = buildResourceUri("icons", shortName, docType);
205
297
  }
206
298
  results.push({
207
299
  name: icon.name,
@@ -210,19 +302,23 @@ async function handleSearchComponents(input) {
210
302
  description: icon.description,
211
303
  category: "icon",
212
304
  availableDocs: icon.files,
213
- resourceUris
305
+ resourceUris,
306
+ tier,
307
+ matchedTerms
214
308
  });
215
309
  }
216
310
  }
217
311
  }
218
312
  results.sort((a, b) => {
219
- const aExact = a.tagName.toLowerCase() === queryLower;
220
- const bExact = b.tagName.toLowerCase() === queryLower;
221
- if (aExact && !bExact) return -1;
222
- if (!aExact && bExact) return 1;
313
+ if (a.tier !== b.tier) return a.tier - b.tier;
314
+ if (splitTerms && a.matchedTerms !== b.matchedTerms)
315
+ return b.matchedTerms - a.matchedTerms;
223
316
  return a.tagName.localeCompare(b.tagName);
224
317
  });
225
- const limitedResults = results.slice(0, 20);
318
+ const limitedResults = results.slice(0, maxResults).map((r) => {
319
+ const { tier, matchedTerms, ...result } = r;
320
+ return result;
321
+ });
226
322
  return {
227
323
  content: [
228
324
  {
@@ -96,6 +96,10 @@ export interface SearchResult {
96
96
  export interface SearchComponentsInput {
97
97
  query: string;
98
98
  category?: Category;
99
+ splitTerms?: boolean;
100
+ matchAll?: boolean;
101
+ useRegex?: boolean;
102
+ maxResults?: number;
99
103
  }
100
104
  export interface GetComponentDocsInput {
101
105
  componentName: string;