@pure-ds/core 0.3.19 → 0.4.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.
@@ -1,571 +1,582 @@
1
- /**
2
- * PDSQuery - Smart query engine for the Pure Design System
3
- *
4
- * Interprets natural language questions and maps them to PDS runtime data
5
- * structures (tokens, components, utilities, patterns) using keyword matching,
6
- * intent detection, and semantic scoring.
7
- *
8
- * @example
9
- * const query = new PDSQuery(PDS);
10
- * const results = await query.search("what is the focus border color on inputs?");
11
- * // Returns array of scored results with text, value, icon, category
12
- */
13
-
14
- export class PDSQuery {
15
- constructor(pds) {
16
- this.pds = pds;
17
-
18
- // Keyword dictionaries for intent detection
19
- this.intents = {
20
- color: ['color', 'colours', 'shade', 'tint', 'hue', 'foreground', 'background', 'text', 'fill', 'bg', 'fg'],
21
- spacing: ['spacing', 'space', 'gap', 'padding', 'margin', 'distance', 'rhythm'],
22
- typography: ['font', 'text', 'type', 'typography', 'heading', 'body', 'size', 'weight', 'family'],
23
- border: ['border', 'outline', 'stroke', 'edge', 'frame'],
24
- radius: ['radius', 'rounded', 'corner', 'curve', 'round'],
25
- shadow: ['shadow', 'elevation', 'depth', 'glow', 'drop-shadow'],
26
- component: ['component', 'element', 'widget'],
27
- utility: ['utility', 'class', 'helper', 'css'],
28
- layout: ['layout', 'container', 'grid', 'flex', 'group', 'arrange', 'organize'],
29
- pattern: ['pattern', 'example', 'template', 'structure'],
30
- interaction: ['hover', 'focus', 'active', 'disabled', 'pressed', 'selected', 'checked'],
31
- };
32
-
33
- // Entity/element keywords
34
- this.entities = {
35
- button: ['button', 'btn', 'cta'],
36
- input: ['input', 'field', 'textbox', 'text-field', 'form-control'],
37
- card: ['card', 'panel'],
38
- badge: ['badge', 'pill', 'tag', 'chip'],
39
- surface: ['surface', 'background', 'layer', 'container'],
40
- icon: ['icon', 'svg', 'glyph', 'symbol'],
41
- link: ['link', 'anchor', 'hyperlink'],
42
- nav: ['nav', 'navigation', 'menu'],
43
- modal: ['modal', 'dialog', 'popup', 'overlay'],
44
- drawer: ['drawer', 'sidebar', 'panel'],
45
- tab: ['tab', 'tabstrip'],
46
- toast: ['toast', 'notification', 'alert', 'message'],
47
- };
48
-
49
- // Question patterns
50
- this.questionWords = ['what', 'which', 'how', 'where', 'when', 'show', 'find', 'get', 'give', 'tell'];
51
- }
52
-
53
- /**
54
- * Main search entry point
55
- * @param {string} query - Natural language question
56
- * @returns {Promise<Array>} Array of results with text, value, icon, category, score
57
- */
58
- async search(query) {
59
- if (!query || query.length < 2) return [];
60
-
61
- const normalized = query.toLowerCase().trim();
62
- const tokens = this.tokenize(normalized);
63
-
64
- // Detect intent and entities from query
65
- const context = this.analyzeQuery(tokens, normalized);
66
-
67
- // Generate results from multiple strategies
68
- const results = [];
69
-
70
- // Strategy 1: Direct token/color queries
71
- if (context.intents.has('color')) {
72
- results.push(...this.queryColors(context, normalized));
73
- }
74
-
75
- // Strategy 2: Utility class queries
76
- if (context.intents.has('utility') || context.intents.has('border') ||
77
- context.intents.has('layout') || normalized.includes('class')) {
78
- results.push(...this.queryUtilities(context, normalized));
79
- }
80
-
81
- // Strategy 3: Component queries
82
- if (context.intents.has('component') || context.entities.size > 0) {
83
- results.push(...this.queryComponents(context, normalized));
84
- }
85
-
86
- // Strategy 4: Pattern/layout queries
87
- if (context.intents.has('layout') || context.intents.has('pattern')) {
88
- results.push(...this.queryPatterns(context, normalized));
89
- }
90
-
91
- // Strategy 5: Typography queries
92
- if (context.intents.has('typography')) {
93
- results.push(...this.queryTypography(context, normalized));
94
- }
95
-
96
- // Strategy 6: Spacing queries
97
- if (context.intents.has('spacing')) {
98
- results.push(...this.querySpacing(context, normalized));
99
- }
100
-
101
- // Deduplicate by value, keeping highest score
102
- const seen = new Map();
103
- for (const result of results) {
104
- const key = result.value;
105
- if (!seen.has(key) || seen.get(key).score < result.score) {
106
- seen.set(key, result);
107
- }
108
- }
109
-
110
- // Sort by score descending, limit to top 10
111
- return Array.from(seen.values())
112
- .sort((a, b) => b.score - a.score)
113
- .slice(0, 10);
114
- }
115
-
116
- /**
117
- * Tokenize and normalize query string
118
- */
119
- tokenize(text) {
120
- return text.toLowerCase()
121
- .replace(/[?!.]/g, '')
122
- .split(/\s+/)
123
- .filter(t => t.length > 0);
124
- }
125
-
126
- /**
127
- * Analyze query to extract intents and entities
128
- */
129
- analyzeQuery(tokens, fullText) {
130
- const context = {
131
- intents: new Set(),
132
- entities: new Set(),
133
- modifiers: new Set(),
134
- isQuestion: false,
135
- tokens,
136
- fullText
137
- };
138
-
139
- // Check if it's a question
140
- context.isQuestion = this.questionWords.some(qw => tokens.includes(qw));
141
-
142
- // Match intents
143
- for (const [intent, keywords] of Object.entries(this.intents)) {
144
- if (keywords.some(kw => tokens.includes(kw) || fullText.includes(kw))) {
145
- context.intents.add(intent);
146
- }
147
- }
148
-
149
- // Match entities
150
- for (const [entity, keywords] of Object.entries(this.entities)) {
151
- if (keywords.some(kw => tokens.includes(kw) || fullText.includes(kw))) {
152
- context.entities.add(entity);
153
- }
154
- }
155
-
156
- // Extract interaction modifiers
157
- if (tokens.includes('hover') || fullText.includes('hover')) context.modifiers.add('hover');
158
- if (tokens.includes('focus') || fullText.includes('focus')) context.modifiers.add('focus');
159
- if (tokens.includes('active') || fullText.includes('active')) context.modifiers.add('active');
160
- if (tokens.includes('disabled') || fullText.includes('disabled')) context.modifiers.add('disabled');
161
-
162
- return context;
163
- }
164
-
165
- /**
166
- * Query color tokens and surfaces
167
- */
168
- queryColors(context, query) {
169
- const results = [];
170
- const compiled = this.pds.compiled;
171
-
172
- if (!compiled?.tokens?.colors) return results;
173
-
174
- const colors = compiled.tokens.colors;
175
- const entities = Array.from(context.entities);
176
- const modifiers = Array.from(context.modifiers);
177
-
178
- // Specific color + element queries: "focus border color on inputs"
179
- if (modifiers.includes('focus') && context.intents.has('border') && entities.includes('input')) {
180
- results.push({
181
- text: 'Focus border color: var(--color-primary-500)',
182
- value: '--color-primary-500',
183
- icon: 'palette',
184
- category: 'Color Token',
185
- score: 100,
186
- cssVar: 'var(--color-primary-500)',
187
- description: 'Primary color used for focus states on form inputs'
188
- });
189
- }
190
-
191
- // Foreground on surface queries
192
- if ((query.includes('foreground') || query.includes('text')) &&
193
- (query.includes('surface') || context.entities.has('surface'))) {
194
- results.push({
195
- text: 'Text on surface: var(--surface-text)',
196
- value: '--surface-text',
197
- icon: 'palette',
198
- category: 'Surface Token',
199
- score: 95,
200
- cssVar: 'var(--surface-text)',
201
- description: 'Default text color for current surface'
202
- });
203
- results.push({
204
- text: 'Secondary text: var(--surface-text-secondary)',
205
- value: '--surface-text-secondary',
206
- icon: 'palette',
207
- category: 'Surface Token',
208
- score: 90,
209
- cssVar: 'var(--surface-text-secondary)',
210
- description: 'Secondary/muted text on surface'
211
- });
212
- }
213
-
214
- // Generic color scale queries
215
- if (query.includes('primary') || query.includes('accent') || query.includes('secondary')) {
216
- const scale = query.includes('primary') ? 'primary' :
217
- query.includes('accent') ? 'accent' : 'secondary';
218
-
219
- for (const shade of [500, 600, 700]) {
220
- const varName = `--color-${scale}-${shade}`;
221
- results.push({
222
- text: `${scale.charAt(0).toUpperCase() + scale.slice(1)} ${shade}: var(${varName})`,
223
- value: varName,
224
- icon: 'palette',
225
- category: 'Color Scale',
226
- score: 80 - (shade - 500) / 100,
227
- cssVar: `var(${varName})`,
228
- description: `${scale} color scale shade ${shade}`
229
- });
230
- }
231
- }
232
-
233
- // Button color queries
234
- if (entities.includes('button') && context.intents.has('color')) {
235
- const modifier = modifiers[0];
236
- if (modifier) {
237
- results.push({
238
- text: `Button ${modifier} fill: var(--${modifier === 'hover' ? 'primary' : 'primary'}-fill-${modifier})`,
239
- value: `--primary-fill-${modifier}`,
240
- icon: 'palette',
241
- category: 'Interactive Token',
242
- score: 92,
243
- description: `Button background color in ${modifier} state`
244
- });
245
- } else {
246
- results.push({
247
- text: 'Button fill: var(--primary-fill)',
248
- value: '--primary-fill',
249
- icon: 'palette',
250
- category: 'Interactive Token',
251
- score: 88,
252
- description: 'Default button background color'
253
- });
254
- }
255
- }
256
-
257
- return results;
258
- }
259
-
260
- /**
261
- * Query utility classes
262
- */
263
- queryUtilities(context, query) {
264
- const results = [];
265
- const ontology = this.pds.ontology;
266
-
267
- if (!ontology?.utilities) return results;
268
-
269
- const utilities = ontology.utilities;
270
-
271
- // Border utilities
272
- if (context.intents.has('border')) {
273
- const borderUtils = utilities.filter(u =>
274
- u.includes('border') || u.includes('outline')
275
- );
276
-
277
- borderUtils.forEach(util => {
278
- let score = 80;
279
- if (query.includes('gradient') && util.includes('gradient')) score = 95;
280
- if (query.includes('glow') && util.includes('glow')) score = 95;
281
-
282
- results.push({
283
- text: `${util} - Border utility class`,
284
- value: util,
285
- icon: 'code',
286
- category: 'Utility Class',
287
- score,
288
- code: `<div class="${util}">...</div>`,
289
- description: this.describeUtility(util)
290
- });
291
- });
292
- }
293
-
294
- // Layout utilities
295
- if (context.intents.has('layout')) {
296
- const layoutUtils = utilities.filter(u =>
297
- u.includes('flex') || u.includes('grid') || u.includes('items-') ||
298
- u.includes('justify-') || u.includes('gap-')
299
- );
300
-
301
- layoutUtils.forEach(util => {
302
- results.push({
303
- text: `${util} - Layout utility`,
304
- value: util,
305
- icon: 'layout',
306
- category: 'Utility Class',
307
- score: 85,
308
- code: `<div class="${util}">...</div>`,
309
- description: this.describeUtility(util)
310
- });
311
- });
312
- }
313
-
314
- // Button group utilities
315
- if (query.includes('group') && context.entities.has('button')) {
316
- results.push({
317
- text: '.btn-group - Group buttons together',
318
- value: '.btn-group',
319
- icon: 'code',
320
- category: 'Utility Class',
321
- score: 90,
322
- code: `<div class="btn-group">\n <button class="btn-primary">One</button>\n <button class="btn-primary">Two</button>\n</div>`,
323
- description: 'Container for grouped buttons with connected styling'
324
- });
325
- }
326
-
327
- return results;
328
- }
329
-
330
- /**
331
- * Query components
332
- */
333
- queryComponents(context, query) {
334
- const results = [];
335
- const ontology = this.pds.ontology;
336
-
337
- if (!ontology?.components && !ontology?.primitives) return results;
338
-
339
- // Search custom components
340
- if (ontology.components) {
341
- ontology.components.forEach(comp => {
342
- const matchScore = this.scoreMatch(query, comp.name + ' ' + comp.id);
343
- if (matchScore > 50) {
344
- results.push({
345
- text: `<${comp.id}> - ${comp.name}`,
346
- value: comp.id,
347
- icon: 'brackets-curly',
348
- category: 'Web Component',
349
- score: matchScore,
350
- code: `<${comp.id}></${comp.id}>`,
351
- description: comp.description || `${comp.name} web component`
352
- });
353
- }
354
- });
355
- }
356
-
357
- // Search primitives (native HTML elements with PDS styling)
358
- if (ontology.primitives) {
359
- ontology.primitives.forEach(prim => {
360
- const matchScore = this.scoreMatch(query, prim.name + ' ' + prim.id);
361
- if (matchScore > 50) {
362
- const selector = prim.selectors?.[0] || prim.id;
363
- results.push({
364
- text: `${selector} - ${prim.name}`,
365
- value: prim.id,
366
- icon: 'tag',
367
- category: 'Primitive',
368
- score: matchScore - 5,
369
- code: this.generatePrimitiveExample(prim),
370
- description: prim.description || `${prim.name} primitive element`
371
- });
372
- }
373
- });
374
- }
375
-
376
- // Icon-specific queries
377
- if (query.includes('icon') && (query.includes('only') || query.includes('button'))) {
378
- results.push({
379
- text: 'Icon-only button: <button class="btn-icon">',
380
- value: 'btn-icon',
381
- icon: 'star',
382
- category: 'Pattern',
383
- score: 95,
384
- code: `<button class="btn-icon btn-primary">\n <pds-icon icon="heart"></pds-icon>\n</button>`,
385
- description: 'Button with only an icon, no text label'
386
- });
387
- }
388
-
389
- return results;
390
- }
391
-
392
- /**
393
- * Query layout patterns
394
- */
395
- queryPatterns(context, query) {
396
- const results = [];
397
- const ontology = this.pds.ontology;
398
-
399
- if (!ontology?.layoutPatterns) return results;
400
-
401
- ontology.layoutPatterns.forEach(pattern => {
402
- const matchScore = this.scoreMatch(query, pattern.name + ' ' + pattern.id + ' ' + (pattern.description || ''));
403
- if (matchScore > 50) {
404
- const selector = pattern.selectors?.[0] || `.${pattern.id}`;
405
- results.push({
406
- text: `${pattern.name} - ${pattern.description || 'Layout pattern'}`,
407
- value: pattern.id,
408
- icon: 'layout',
409
- category: 'Layout Pattern',
410
- score: matchScore,
411
- code: `<div class="${selector.replace('.', '')}">\n <!-- content -->\n</div>`,
412
- description: pattern.description || pattern.name
413
- });
414
- }
415
- });
416
-
417
- // Container queries
418
- if (query.includes('container') || query.includes('group')) {
419
- results.push({
420
- text: 'Card - Container for grouping content',
421
- value: 'card',
422
- icon: 'layout',
423
- category: 'Primitive',
424
- score: 88,
425
- code: `<article class="card">\n <header>\n <h3>Title</h3>\n </header>\n <p>Content...</p>\n</article>`,
426
- description: 'Card container with optional header, body, and footer'
427
- });
428
-
429
- results.push({
430
- text: 'Section - Semantic container for grouping',
431
- value: 'section',
432
- icon: 'layout',
433
- category: 'Pattern',
434
- score: 85,
435
- code: `<section>\n <h2>Section Title</h2>\n <!-- content -->\n</section>`,
436
- description: 'Semantic section element for content grouping'
437
- });
438
- }
439
-
440
- return results;
441
- }
442
-
443
- /**
444
- * Query typography tokens
445
- */
446
- queryTypography(context, query) {
447
- const results = [];
448
- const compiled = this.pds.compiled;
449
-
450
- if (!compiled?.tokens?.typography) return results;
451
-
452
- const typo = compiled.tokens.typography;
453
-
454
- if (query.includes('heading') || query.includes('title')) {
455
- results.push({
456
- text: 'Heading font: var(--font-family-heading)',
457
- value: '--font-family-heading',
458
- icon: 'text-aa',
459
- category: 'Typography Token',
460
- score: 85,
461
- cssVar: 'var(--font-family-heading)',
462
- description: 'Font family for headings'
463
- });
464
- }
465
-
466
- if (query.includes('body') || query.includes('text')) {
467
- results.push({
468
- text: 'Body font: var(--font-family-body)',
469
- value: '--font-family-body',
470
- icon: 'text-aa',
471
- category: 'Typography Token',
472
- score: 85,
473
- cssVar: 'var(--font-family-body)',
474
- description: 'Font family for body text'
475
- });
476
- }
477
-
478
- return results;
479
- }
480
-
481
- /**
482
- * Query spacing tokens
483
- */
484
- querySpacing(context, query) {
485
- const results = [];
486
- const compiled = this.pds.compiled;
487
-
488
- if (!compiled?.tokens?.spacing) return results;
489
-
490
- const spacing = compiled.tokens.spacing;
491
-
492
- // Show common spacing values
493
- for (const [key, value] of Object.entries(spacing)) {
494
- if (['2', '4', '6', '8'].includes(key)) {
495
- results.push({
496
- text: `Spacing ${key}: var(--spacing-${key})`,
497
- value: `--spacing-${key}`,
498
- icon: 'ruler',
499
- category: 'Spacing Token',
500
- score: 75,
501
- cssVar: `var(--spacing-${key})`,
502
- description: `Spacing value: ${value}`
503
- });
504
- }
505
- }
506
-
507
- return results;
508
- }
509
-
510
- /**
511
- * Calculate match score between query and target text
512
- */
513
- scoreMatch(query, target) {
514
- const queryLower = query.toLowerCase();
515
- const targetLower = target.toLowerCase();
516
-
517
- let score = 0;
518
-
519
- // Exact match
520
- if (queryLower === targetLower) return 100;
521
-
522
- // Contains full query
523
- if (targetLower.includes(queryLower)) score += 80;
524
-
525
- // Word overlap
526
- const queryWords = this.tokenize(queryLower);
527
- const targetWords = this.tokenize(targetLower);
528
- const overlap = queryWords.filter(w => targetWords.includes(w)).length;
529
- score += (overlap / queryWords.length) * 40;
530
-
531
- // Starts with
532
- if (targetLower.startsWith(queryLower)) score += 20;
533
-
534
- return Math.min(100, score);
535
- }
536
-
537
- /**
538
- * Generate example code for a primitive
539
- */
540
- generatePrimitiveExample(primitive) {
541
- const selector = primitive.selectors?.[0] || primitive.id;
542
-
543
- if (selector.includes('button') || primitive.id === 'button') {
544
- return '<button class="btn-primary">Click me</button>';
545
- }
546
- if (selector.includes('card') || primitive.id === 'card') {
547
- return '<article class="card">\n <h3>Title</h3>\n <p>Content</p>\n</article>';
548
- }
549
- if (selector.includes('badge') || primitive.id === 'badge') {
550
- return '<span class="badge">New</span>';
551
- }
552
-
553
- return `<${selector}>Content</${selector}>`;
554
- }
555
-
556
- /**
557
- * Describe utility class purpose
558
- */
559
- describeUtility(utilClass) {
560
- if (utilClass.includes('border-gradient')) return 'Apply animated gradient border effect';
561
- if (utilClass.includes('border-glow')) return 'Apply glowing border effect';
562
- if (utilClass.includes('flex')) return 'Flexbox container utility';
563
- if (utilClass.includes('grid')) return 'Grid container utility';
564
- if (utilClass.includes('gap-')) return 'Set gap between flex/grid children';
565
- if (utilClass.includes('items-')) return 'Align items in flex container';
566
- if (utilClass.includes('justify-')) return 'Justify content in flex container';
567
- if (utilClass === '.btn-group') return 'Group buttons with connected styling';
568
-
569
- return 'Utility class for styling';
570
- }
571
- }
1
+ /**
2
+ * PDSQuery - Smart query engine for the Pure Design System
3
+ *
4
+ * Interprets natural language questions and maps them to PDS runtime data
5
+ * structures (tokens, components, utilities, patterns) using keyword matching,
6
+ * intent detection, and semantic scoring.
7
+ *
8
+ * @example
9
+ * const query = new PDSQuery(PDS);
10
+ * const results = await query.search("what is the focus border color on inputs?");
11
+ * // Returns array of scored results with text, value, icon, category
12
+ */
13
+
14
+ export class PDSQuery {
15
+ constructor(pds) {
16
+ this.pds = pds;
17
+
18
+ // Keyword dictionaries for intent detection
19
+ this.intents = {
20
+ color: ['color', 'colours', 'shade', 'tint', 'hue', 'foreground', 'background', 'text', 'fill', 'bg', 'fg'],
21
+ spacing: ['spacing', 'space', 'gap', 'padding', 'margin', 'distance', 'rhythm'],
22
+ typography: ['font', 'text', 'type', 'typography', 'heading', 'body', 'size', 'weight', 'family'],
23
+ border: ['border', 'outline', 'stroke', 'edge', 'frame'],
24
+ radius: ['radius', 'rounded', 'corner', 'curve', 'round'],
25
+ shadow: ['shadow', 'elevation', 'depth', 'glow', 'drop-shadow'],
26
+ component: ['component', 'element', 'widget'],
27
+ utility: ['utility', 'class', 'helper', 'css'],
28
+ layout: ['layout', 'container', 'grid', 'flex', 'group', 'arrange', 'organize'],
29
+ pattern: ['pattern', 'example', 'template', 'structure'],
30
+ interaction: ['hover', 'focus', 'active', 'disabled', 'pressed', 'selected', 'checked'],
31
+ };
32
+
33
+ // Entity/element keywords
34
+ this.entities = {
35
+ button: ['button', 'btn', 'cta'],
36
+ input: ['input', 'field', 'textbox', 'text-field', 'form-control'],
37
+ card: ['card', 'panel'],
38
+ badge: ['badge', 'pill', 'tag', 'chip'],
39
+ surface: ['surface', 'background', 'layer', 'container'],
40
+ icon: ['icon', 'svg', 'glyph', 'symbol'],
41
+ link: ['link', 'anchor', 'hyperlink'],
42
+ nav: ['nav', 'navigation', 'menu'],
43
+ modal: ['modal', 'dialog', 'popup', 'overlay'],
44
+ drawer: ['drawer', 'sidebar', 'panel'],
45
+ tab: ['tab', 'tabstrip'],
46
+ toast: ['toast', 'notification', 'alert', 'message'],
47
+ };
48
+
49
+ // Question patterns
50
+ this.questionWords = ['what', 'which', 'how', 'where', 'when', 'show', 'find', 'get', 'give', 'tell'];
51
+ }
52
+
53
+ /**
54
+ * Main search entry point
55
+ * @param {string} query - Natural language question
56
+ * @returns {Promise<Array>} Array of results with text, value, icon, category, score
57
+ */
58
+ async search(query) {
59
+ if (!query || query.length < 2) return [];
60
+
61
+ const normalized = query.toLowerCase().trim();
62
+ const tokens = this.tokenize(normalized);
63
+
64
+ // Detect intent and entities from query
65
+ const context = this.analyzeQuery(tokens, normalized);
66
+
67
+ // Generate results from multiple strategies
68
+ const results = [];
69
+
70
+ // Strategy 1: Direct token/color queries
71
+ if (context.intents.has('color')) {
72
+ results.push(...this.queryColors(context, normalized));
73
+ }
74
+
75
+ // Strategy 2: Utility class queries
76
+ if (context.intents.has('utility') || context.intents.has('border') ||
77
+ context.intents.has('layout') || normalized.includes('class')) {
78
+ results.push(...this.queryUtilities(context, normalized));
79
+ }
80
+
81
+ // Strategy 3: Component queries
82
+ if (context.intents.has('component') || context.entities.size > 0) {
83
+ results.push(...this.queryComponents(context, normalized));
84
+ }
85
+
86
+ // Strategy 4: Pattern/layout queries
87
+ if (context.intents.has('layout') || context.intents.has('pattern')) {
88
+ results.push(...this.queryPatterns(context, normalized));
89
+ }
90
+
91
+ // Strategy 5: Typography queries
92
+ if (context.intents.has('typography')) {
93
+ results.push(...this.queryTypography(context, normalized));
94
+ }
95
+
96
+ // Strategy 6: Spacing queries
97
+ if (context.intents.has('spacing')) {
98
+ results.push(...this.querySpacing(context, normalized));
99
+ }
100
+
101
+ // Deduplicate by value, keeping highest score
102
+ const seen = new Map();
103
+ for (const result of results) {
104
+ const key = result.value;
105
+ if (!seen.has(key) || seen.get(key).score < result.score) {
106
+ seen.set(key, result);
107
+ }
108
+ }
109
+
110
+ // Sort by score descending, limit to top 10
111
+ return Array.from(seen.values())
112
+ .sort((a, b) => b.score - a.score)
113
+ .slice(0, 10);
114
+ }
115
+
116
+ /**
117
+ * Tokenize and normalize query string
118
+ */
119
+ tokenize(text) {
120
+ return text.toLowerCase()
121
+ .replace(/[?!.]/g, '')
122
+ .split(/\s+/)
123
+ .filter(t => t.length > 0);
124
+ }
125
+
126
+ /**
127
+ * Analyze query to extract intents and entities
128
+ */
129
+ analyzeQuery(tokens, fullText) {
130
+ const context = {
131
+ intents: new Set(),
132
+ entities: new Set(),
133
+ modifiers: new Set(),
134
+ isQuestion: false,
135
+ tokens,
136
+ fullText
137
+ };
138
+
139
+ // Check if it's a question
140
+ context.isQuestion = this.questionWords.some(qw => tokens.includes(qw));
141
+
142
+ // Match intents
143
+ for (const [intent, keywords] of Object.entries(this.intents)) {
144
+ if (keywords.some(kw => tokens.includes(kw) || fullText.includes(kw))) {
145
+ context.intents.add(intent);
146
+ }
147
+ }
148
+
149
+ // Match entities
150
+ for (const [entity, keywords] of Object.entries(this.entities)) {
151
+ if (keywords.some(kw => tokens.includes(kw) || fullText.includes(kw))) {
152
+ context.entities.add(entity);
153
+ }
154
+ }
155
+
156
+ // Extract interaction modifiers
157
+ if (tokens.includes('hover') || fullText.includes('hover')) context.modifiers.add('hover');
158
+ if (tokens.includes('focus') || fullText.includes('focus')) context.modifiers.add('focus');
159
+ if (tokens.includes('active') || fullText.includes('active')) context.modifiers.add('active');
160
+ if (tokens.includes('disabled') || fullText.includes('disabled')) context.modifiers.add('disabled');
161
+
162
+ return context;
163
+ }
164
+
165
+ /**
166
+ * Query color tokens and surfaces
167
+ */
168
+ queryColors(context, query) {
169
+ const results = [];
170
+ const compiled = this.pds.compiled;
171
+
172
+ if (!compiled?.tokens?.colors) return results;
173
+
174
+ const colors = compiled.tokens.colors;
175
+ const entities = Array.from(context.entities);
176
+ const modifiers = Array.from(context.modifiers);
177
+
178
+ // Specific color + element queries: "focus border color on inputs"
179
+ if (modifiers.includes('focus') && context.intents.has('border') && entities.includes('input')) {
180
+ results.push({
181
+ text: 'Focus border color: var(--color-primary-500)',
182
+ value: '--color-primary-500',
183
+ icon: 'palette',
184
+ category: 'Color Token',
185
+ score: 100,
186
+ cssVar: 'var(--color-primary-500)',
187
+ description: 'Primary color used for focus states on form inputs'
188
+ });
189
+ }
190
+
191
+ // Foreground on surface queries
192
+ if ((query.includes('foreground') || query.includes('text')) &&
193
+ (query.includes('surface') || context.entities.has('surface'))) {
194
+ results.push({
195
+ text: 'Text on surface: var(--surface-text)',
196
+ value: '--surface-text',
197
+ icon: 'palette',
198
+ category: 'Surface Token',
199
+ score: 95,
200
+ cssVar: 'var(--surface-text)',
201
+ description: 'Default text color for current surface'
202
+ });
203
+ results.push({
204
+ text: 'Secondary text: var(--surface-text-secondary)',
205
+ value: '--surface-text-secondary',
206
+ icon: 'palette',
207
+ category: 'Surface Token',
208
+ score: 90,
209
+ cssVar: 'var(--surface-text-secondary)',
210
+ description: 'Secondary/muted text on surface'
211
+ });
212
+ }
213
+
214
+ // Generic color scale queries
215
+ if (query.includes('primary') || query.includes('accent') || query.includes('secondary')) {
216
+ const scale = query.includes('primary') ? 'primary' :
217
+ query.includes('accent') ? 'accent' : 'secondary';
218
+
219
+ for (const shade of [500, 600, 700]) {
220
+ const varName = `--color-${scale}-${shade}`;
221
+ results.push({
222
+ text: `${scale.charAt(0).toUpperCase() + scale.slice(1)} ${shade}: var(${varName})`,
223
+ value: varName,
224
+ icon: 'palette',
225
+ category: 'Color Scale',
226
+ score: 80 - (shade - 500) / 100,
227
+ cssVar: `var(${varName})`,
228
+ description: `${scale} color scale shade ${shade}`
229
+ });
230
+ }
231
+ }
232
+
233
+ // Button color queries
234
+ if (entities.includes('button') && context.intents.has('color')) {
235
+ const modifier = modifiers[0];
236
+ if (modifier) {
237
+ results.push({
238
+ text: `Button ${modifier} fill: var(--${modifier === 'hover' ? 'primary' : 'primary'}-fill-${modifier})`,
239
+ value: `--primary-fill-${modifier}`,
240
+ icon: 'palette',
241
+ category: 'Interactive Token',
242
+ score: 92,
243
+ description: `Button background color in ${modifier} state`
244
+ });
245
+ } else {
246
+ results.push({
247
+ text: 'Button fill: var(--primary-fill)',
248
+ value: '--primary-fill',
249
+ icon: 'palette',
250
+ category: 'Interactive Token',
251
+ score: 88,
252
+ description: 'Default button background color'
253
+ });
254
+ }
255
+ }
256
+
257
+ return results;
258
+ }
259
+
260
+ /**
261
+ * Query utility classes
262
+ */
263
+ queryUtilities(context, query) {
264
+ const results = [];
265
+ const ontology = this.pds.ontology;
266
+
267
+ if (!ontology?.utilities) return results;
268
+
269
+ // Flatten utilities object into array of class names
270
+ const utilitiesObj = ontology.utilities;
271
+ const utilities = [];
272
+ for (const category of Object.values(utilitiesObj)) {
273
+ if (typeof category === 'object') {
274
+ for (const value of Object.values(category)) {
275
+ if (Array.isArray(value)) {
276
+ utilities.push(...value);
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ // Border utilities
283
+ if (context.intents.has('border')) {
284
+ const borderUtils = utilities.filter(u =>
285
+ u.includes('border') || u.includes('outline')
286
+ );
287
+
288
+ borderUtils.forEach(util => {
289
+ let score = 80;
290
+ if (query.includes('gradient') && util.includes('gradient')) score = 95;
291
+ if (query.includes('glow') && util.includes('glow')) score = 95;
292
+
293
+ results.push({
294
+ text: `${util} - Border utility class`,
295
+ value: util,
296
+ icon: 'code',
297
+ category: 'Utility Class',
298
+ score,
299
+ code: `<div class="${util}">...</div>`,
300
+ description: this.describeUtility(util)
301
+ });
302
+ });
303
+ }
304
+
305
+ // Layout utilities
306
+ if (context.intents.has('layout')) {
307
+ const layoutUtils = utilities.filter(u =>
308
+ u.includes('flex') || u.includes('grid') || u.includes('items-') ||
309
+ u.includes('justify-') || u.includes('gap-')
310
+ );
311
+
312
+ layoutUtils.forEach(util => {
313
+ results.push({
314
+ text: `${util} - Layout utility`,
315
+ value: util,
316
+ icon: 'layout',
317
+ category: 'Utility Class',
318
+ score: 85,
319
+ code: `<div class="${util}">...</div>`,
320
+ description: this.describeUtility(util)
321
+ });
322
+ });
323
+ }
324
+
325
+ // Button group utilities
326
+ if (query.includes('group') && context.entities.has('button')) {
327
+ results.push({
328
+ text: '.btn-group - Group buttons together',
329
+ value: '.btn-group',
330
+ icon: 'code',
331
+ category: 'Utility Class',
332
+ score: 90,
333
+ code: `<div class="btn-group">\n <button class="btn-primary">One</button>\n <button class="btn-primary">Two</button>\n</div>`,
334
+ description: 'Container for grouped buttons with connected styling'
335
+ });
336
+ }
337
+
338
+ return results;
339
+ }
340
+
341
+ /**
342
+ * Query components
343
+ */
344
+ queryComponents(context, query) {
345
+ const results = [];
346
+ const ontology = this.pds.ontology;
347
+
348
+ if (!ontology?.components && !ontology?.primitives) return results;
349
+
350
+ // Search custom components
351
+ if (ontology.components) {
352
+ ontology.components.forEach(comp => {
353
+ const matchScore = this.scoreMatch(query, comp.name + ' ' + comp.id);
354
+ if (matchScore > 50) {
355
+ results.push({
356
+ text: `<${comp.id}> - ${comp.name}`,
357
+ value: comp.id,
358
+ icon: 'brackets-curly',
359
+ category: 'Web Component',
360
+ score: matchScore,
361
+ code: `<${comp.id}></${comp.id}>`,
362
+ description: comp.description || `${comp.name} web component`
363
+ });
364
+ }
365
+ });
366
+ }
367
+
368
+ // Search primitives (native HTML elements with PDS styling)
369
+ if (ontology.primitives) {
370
+ ontology.primitives.forEach(prim => {
371
+ const matchScore = this.scoreMatch(query, prim.name + ' ' + prim.id);
372
+ if (matchScore > 50) {
373
+ const selector = prim.selectors?.[0] || prim.id;
374
+ results.push({
375
+ text: `${selector} - ${prim.name}`,
376
+ value: prim.id,
377
+ icon: 'tag',
378
+ category: 'Primitive',
379
+ score: matchScore - 5,
380
+ code: this.generatePrimitiveExample(prim),
381
+ description: prim.description || `${prim.name} primitive element`
382
+ });
383
+ }
384
+ });
385
+ }
386
+
387
+ // Icon-specific queries
388
+ if (query.includes('icon') && (query.includes('only') || query.includes('button'))) {
389
+ results.push({
390
+ text: 'Icon-only button: <button class="btn-icon">',
391
+ value: 'btn-icon',
392
+ icon: 'star',
393
+ category: 'Pattern',
394
+ score: 95,
395
+ code: `<button class="btn-icon btn-primary">\n <pds-icon icon="heart"></pds-icon>\n</button>`,
396
+ description: 'Button with only an icon, no text label'
397
+ });
398
+ }
399
+
400
+ return results;
401
+ }
402
+
403
+ /**
404
+ * Query layout patterns
405
+ */
406
+ queryPatterns(context, query) {
407
+ const results = [];
408
+ const ontology = this.pds.ontology;
409
+
410
+ if (!ontology?.layoutPatterns) return results;
411
+
412
+ ontology.layoutPatterns.forEach(pattern => {
413
+ const matchScore = this.scoreMatch(query, pattern.name + ' ' + pattern.id + ' ' + (pattern.description || ''));
414
+ if (matchScore > 50) {
415
+ const selector = pattern.selectors?.[0] || `.${pattern.id}`;
416
+ results.push({
417
+ text: `${pattern.name} - ${pattern.description || 'Layout pattern'}`,
418
+ value: pattern.id,
419
+ icon: 'layout',
420
+ category: 'Layout Pattern',
421
+ score: matchScore,
422
+ code: `<div class="${selector.replace('.', '')}">\n <!-- content -->\n</div>`,
423
+ description: pattern.description || pattern.name
424
+ });
425
+ }
426
+ });
427
+
428
+ // Container queries
429
+ if (query.includes('container') || query.includes('group')) {
430
+ results.push({
431
+ text: 'Card - Container for grouping content',
432
+ value: 'card',
433
+ icon: 'layout',
434
+ category: 'Primitive',
435
+ score: 88,
436
+ code: `<article class="card">\n <header>\n <h3>Title</h3>\n </header>\n <p>Content...</p>\n</article>`,
437
+ description: 'Card container with optional header, body, and footer'
438
+ });
439
+
440
+ results.push({
441
+ text: 'Section - Semantic container for grouping',
442
+ value: 'section',
443
+ icon: 'layout',
444
+ category: 'Pattern',
445
+ score: 85,
446
+ code: `<section>\n <h2>Section Title</h2>\n <!-- content -->\n</section>`,
447
+ description: 'Semantic section element for content grouping'
448
+ });
449
+ }
450
+
451
+ return results;
452
+ }
453
+
454
+ /**
455
+ * Query typography tokens
456
+ */
457
+ queryTypography(context, query) {
458
+ const results = [];
459
+ const compiled = this.pds.compiled;
460
+
461
+ if (!compiled?.tokens?.typography) return results;
462
+
463
+ const typo = compiled.tokens.typography;
464
+
465
+ if (query.includes('heading') || query.includes('title')) {
466
+ results.push({
467
+ text: 'Heading font: var(--font-family-heading)',
468
+ value: '--font-family-heading',
469
+ icon: 'text-aa',
470
+ category: 'Typography Token',
471
+ score: 85,
472
+ cssVar: 'var(--font-family-heading)',
473
+ description: 'Font family for headings'
474
+ });
475
+ }
476
+
477
+ if (query.includes('body') || query.includes('text')) {
478
+ results.push({
479
+ text: 'Body font: var(--font-family-body)',
480
+ value: '--font-family-body',
481
+ icon: 'text-aa',
482
+ category: 'Typography Token',
483
+ score: 85,
484
+ cssVar: 'var(--font-family-body)',
485
+ description: 'Font family for body text'
486
+ });
487
+ }
488
+
489
+ return results;
490
+ }
491
+
492
+ /**
493
+ * Query spacing tokens
494
+ */
495
+ querySpacing(context, query) {
496
+ const results = [];
497
+ const compiled = this.pds.compiled;
498
+
499
+ if (!compiled?.tokens?.spacing) return results;
500
+
501
+ const spacing = compiled.tokens.spacing;
502
+
503
+ // Show common spacing values
504
+ for (const [key, value] of Object.entries(spacing)) {
505
+ if (['2', '4', '6', '8'].includes(key)) {
506
+ results.push({
507
+ text: `Spacing ${key}: var(--spacing-${key})`,
508
+ value: `--spacing-${key}`,
509
+ icon: 'ruler',
510
+ category: 'Spacing Token',
511
+ score: 75,
512
+ cssVar: `var(--spacing-${key})`,
513
+ description: `Spacing value: ${value}`
514
+ });
515
+ }
516
+ }
517
+
518
+ return results;
519
+ }
520
+
521
+ /**
522
+ * Calculate match score between query and target text
523
+ */
524
+ scoreMatch(query, target) {
525
+ const queryLower = query.toLowerCase();
526
+ const targetLower = target.toLowerCase();
527
+
528
+ let score = 0;
529
+
530
+ // Exact match
531
+ if (queryLower === targetLower) return 100;
532
+
533
+ // Contains full query
534
+ if (targetLower.includes(queryLower)) score += 80;
535
+
536
+ // Word overlap
537
+ const queryWords = this.tokenize(queryLower);
538
+ const targetWords = this.tokenize(targetLower);
539
+ const overlap = queryWords.filter(w => targetWords.includes(w)).length;
540
+ score += (overlap / queryWords.length) * 40;
541
+
542
+ // Starts with
543
+ if (targetLower.startsWith(queryLower)) score += 20;
544
+
545
+ return Math.min(100, score);
546
+ }
547
+
548
+ /**
549
+ * Generate example code for a primitive
550
+ */
551
+ generatePrimitiveExample(primitive) {
552
+ const selector = primitive.selectors?.[0] || primitive.id;
553
+
554
+ if (selector.includes('button') || primitive.id === 'button') {
555
+ return '<button class="btn-primary">Click me</button>';
556
+ }
557
+ if (selector.includes('card') || primitive.id === 'card') {
558
+ return '<article class="card">\n <h3>Title</h3>\n <p>Content</p>\n</article>';
559
+ }
560
+ if (selector.includes('badge') || primitive.id === 'badge') {
561
+ return '<span class="badge">New</span>';
562
+ }
563
+
564
+ return `<${selector}>Content</${selector}>`;
565
+ }
566
+
567
+ /**
568
+ * Describe utility class purpose
569
+ */
570
+ describeUtility(utilClass) {
571
+ if (utilClass.includes('border-gradient')) return 'Apply animated gradient border effect';
572
+ if (utilClass.includes('border-glow')) return 'Apply glowing border effect';
573
+ if (utilClass.includes('flex')) return 'Flexbox container utility';
574
+ if (utilClass.includes('grid')) return 'Grid container utility';
575
+ if (utilClass.includes('gap-')) return 'Set gap between flex/grid children';
576
+ if (utilClass.includes('items-')) return 'Align items in flex container';
577
+ if (utilClass.includes('justify-')) return 'Justify content in flex container';
578
+ if (utilClass === '.btn-group') return 'Group buttons with connected styling';
579
+
580
+ return 'Utility class for styling';
581
+ }
582
+ }