@pure-ds/storybook 0.4.4 → 0.4.5

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,9 +1,6 @@
1
1
  import { addons, types } from '@storybook/manager-api';
2
2
  import { ADDON_ID, TOOL_ID } from './constants.js';
3
3
  import { Tool } from './Tool.js';
4
- import { SearchTool } from './SearchTool.js';
5
-
6
- const SEARCH_TOOL_ID = `${ADDON_ID}/search`;
7
4
 
8
5
  addons.register(ADDON_ID, () => {
9
6
  // Register PDS Configurator button
@@ -13,12 +10,4 @@ addons.register(ADDON_ID, () => {
13
10
  match: ({ viewMode, tabId }) => !tabId && (viewMode === 'story' || viewMode === 'docs'),
14
11
  render: Tool
15
12
  });
16
-
17
- // Register Quick Search button
18
- addons.add(SEARCH_TOOL_ID, {
19
- type: types.TOOL,
20
- title: 'Quick Search',
21
- match: ({ viewMode, tabId }) => !tabId && (viewMode === 'story' || viewMode === 'docs'),
22
- render: SearchTool
23
- });
24
13
  });
@@ -40,6 +40,8 @@ const config = {
40
40
  staticDirs: [
41
41
  { from: join(pdsAssetsPath, 'pds'), to: 'pds' },
42
42
  { from: pdsAssetsPath, to: 'assets' },
43
+ // Serve pds-reference.json for ontology search in manager
44
+ { from: join(currentDirname, '../dist'), to: 'pds-data' },
43
45
  // Add user's public folder if it exists
44
46
  ...(fs.existsSync(resolve(process.cwd(), 'public')) ? [{ from: resolve(process.cwd(), 'public'), to: '/' }] : [])
45
47
  ],
@@ -0,0 +1,550 @@
1
+ // .storybook/manager.js
2
+ // PDS Ontology-aware sidebar filtering for Storybook
3
+ // Uses experimental_setFilter API to enrich search with PDS ontology concepts
4
+ // ALL DATA IS LOADED FROM pds-reference.json (Single Source of Truth from pds-ontology.js)
5
+ import { addons } from '@storybook/manager-api';
6
+
7
+ const ADDON_ID = 'pds-ontology-filter';
8
+ const FILTER_ID = 'pds-ontology';
9
+
10
+ // ═══════════════════════════════════════════════════════════════════════════
11
+ // PDS ONTOLOGY - Loaded from SSoT (pds-reference.json)
12
+ // No hardcoded data here - everything comes from the ontology!
13
+ // ═══════════════════════════════════════════════════════════════════════════
14
+
15
+ /** @type {Object|null} Loaded reference data */
16
+ let referenceData = null;
17
+
18
+ /** @type {Map<string, Set<string>>} Term to related terms (from searchRelations + derived) */
19
+ let relationIndex = new Map();
20
+
21
+ /** @type {Map<string, Set<string>>} Category to member IDs/tags */
22
+ let categoryIndex = new Map();
23
+
24
+ /** @type {Map<string, Set<string>>} Tag to items that have that tag */
25
+ let tagIndex = new Map();
26
+
27
+ /**
28
+ * Load ontology data from pds-reference.json
29
+ * This file is generated by build-pds-reference.mjs from the SSoT (pds-ontology.js)
30
+ */
31
+ async function loadOntology() {
32
+ if (referenceData) return referenceData;
33
+
34
+ try {
35
+ const response = await fetch('/pds-data/pds-reference.json');
36
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
37
+ referenceData = await response.json();
38
+ buildIndices();
39
+ console.log('%c🎨 PDS Ontology loaded from SSoT', 'color: #029cfd; font-weight: bold', {
40
+ searchRelations: Object.keys(referenceData.ontologyData?.searchRelations || {}).length,
41
+ primitives: referenceData.ontologyData?.primitives?.length || 0,
42
+ components: referenceData.ontologyData?.components?.length || 0,
43
+ enhancements: referenceData.ontologyData?.enhancements?.length || 0
44
+ });
45
+ return referenceData;
46
+ } catch (err) {
47
+ console.warn('PDS Ontology: Failed to load pds-reference.json', err);
48
+ referenceData = { components: {}, ontologyData: {}, enhancements: [], tokens: {} };
49
+ return referenceData;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Build search indices entirely from loaded ontology data
55
+ * NO HARDCODED DATA - everything derives from pds-ontology.js via pds-reference.json
56
+ */
57
+ function buildIndices() {
58
+ if (!referenceData) return;
59
+
60
+ const od = referenceData.ontologyData || {};
61
+
62
+ // ═══════════════════════════════════════════════════════════════════════════
63
+ // 1. Build relationIndex from searchRelations (SSoT)
64
+ // ═══════════════════════════════════════════════════════════════════════════
65
+ const searchRelations = od.searchRelations || {};
66
+
67
+ // Bidirectional: if "text" → ["typography", "truncate"], then also "typography" → ["text"]
68
+ Object.entries(searchRelations).forEach(([key, related]) => {
69
+ const keyLower = key.toLowerCase();
70
+ if (!relationIndex.has(keyLower)) relationIndex.set(keyLower, new Set());
71
+
72
+ (related || []).forEach(r => {
73
+ const rLower = String(r).toLowerCase();
74
+ relationIndex.get(keyLower).add(rLower);
75
+
76
+ // Bidirectional mapping
77
+ if (!relationIndex.has(rLower)) relationIndex.set(rLower, new Set());
78
+ relationIndex.get(rLower).add(keyLower);
79
+ });
80
+ });
81
+
82
+ // ═══════════════════════════════════════════════════════════════════════════
83
+ // 2. Build categoryIndex from categories and items
84
+ // ═══════════════════════════════════════════════════════════════════════════
85
+ const categories = od.categories || {};
86
+
87
+ // Map declared categories to their member IDs
88
+ Object.entries(categories).forEach(([cat, info]) => {
89
+ const catLower = cat.toLowerCase();
90
+ if (!categoryIndex.has(catLower)) categoryIndex.set(catLower, new Set());
91
+
92
+ (info.primitives || []).forEach(id => categoryIndex.get(catLower).add(id.toLowerCase()));
93
+ (info.components || []).forEach(id => categoryIndex.get(catLower).add(id.toLowerCase()));
94
+ (info.patterns || []).forEach(id => categoryIndex.get(catLower).add(id.toLowerCase()));
95
+ });
96
+
97
+ // Also index items by their declared category
98
+ (od.primitives || []).forEach(p => {
99
+ const cat = (p.category || 'other').toLowerCase();
100
+ if (!categoryIndex.has(cat)) categoryIndex.set(cat, new Set());
101
+ categoryIndex.get(cat).add(p.id?.toLowerCase());
102
+ (p.tags || []).forEach(t => categoryIndex.get(cat).add(t.toLowerCase()));
103
+ });
104
+
105
+ (od.components || []).forEach(c => {
106
+ const cat = (c.category || 'other').toLowerCase();
107
+ if (!categoryIndex.has(cat)) categoryIndex.set(cat, new Set());
108
+ categoryIndex.get(cat).add(c.id?.toLowerCase());
109
+ (c.tags || []).forEach(t => categoryIndex.get(cat).add(t.toLowerCase()));
110
+ });
111
+
112
+ (od.layoutPatterns || []).forEach(l => {
113
+ const cat = (l.category || 'layout').toLowerCase();
114
+ if (!categoryIndex.has(cat)) categoryIndex.set(cat, new Set());
115
+ categoryIndex.get(cat).add(l.id?.toLowerCase());
116
+ (l.tags || []).forEach(t => categoryIndex.get(cat).add(t.toLowerCase()));
117
+ // Also add selector-derived terms (e.g., ".grid" → "grid")
118
+ (l.selectors || []).forEach(s => {
119
+ const cleaned = s.replace(/[.#\[\]]/g, '').toLowerCase();
120
+ if (cleaned) categoryIndex.get(cat).add(cleaned);
121
+ });
122
+ });
123
+
124
+ // ═══════════════════════════════════════════════════════════════════════════
125
+ // 3. Build tagIndex from all item tags
126
+ // Maps tags to the specific item IDs that have that tag (NOT the type!)
127
+ // ═══════════════════════════════════════════════════════════════════════════
128
+ const indexItemTags = (items) => {
129
+ (items || []).forEach(item => {
130
+ const itemId = item.id?.toLowerCase();
131
+ if (!itemId) return;
132
+
133
+ (item.tags || []).forEach(tag => {
134
+ const tagLower = tag.toLowerCase();
135
+ if (!tagIndex.has(tagLower)) tagIndex.set(tagLower, new Set());
136
+ tagIndex.get(tagLower).add(itemId);
137
+
138
+ // Also add tag→id relations (but NOT the generic type like 'primitive')
139
+ if (!relationIndex.has(tagLower)) relationIndex.set(tagLower, new Set());
140
+ relationIndex.get(tagLower).add(itemId);
141
+ });
142
+ });
143
+ };
144
+
145
+ indexItemTags(od.primitives);
146
+ indexItemTags(od.components);
147
+ indexItemTags(od.layoutPatterns);
148
+ indexItemTags(od.enhancements);
149
+
150
+ // ═══════════════════════════════════════════════════════════════════════════
151
+ // 4. Index utilities (text, backdrop, border, etc.)
152
+ // ═══════════════════════════════════════════════════════════════════════════
153
+ const utilities = od.utilities || {};
154
+ Object.entries(utilities).forEach(([group, value]) => {
155
+ const groupLower = group.toLowerCase();
156
+ if (!categoryIndex.has('utility')) categoryIndex.set('utility', new Set());
157
+ categoryIndex.get('utility').add(groupLower);
158
+
159
+ // If value is an object with arrays, index the class names
160
+ if (typeof value === 'object' && value !== null) {
161
+ Object.entries(value).forEach(([subgroup, classes]) => {
162
+ if (Array.isArray(classes)) {
163
+ classes.forEach(cls => {
164
+ const cleaned = cls.replace(/[.#\[\]]/g, '').toLowerCase();
165
+ if (!relationIndex.has(groupLower)) relationIndex.set(groupLower, new Set());
166
+ relationIndex.get(groupLower).add(cleaned);
167
+ // Bidirectional
168
+ if (!relationIndex.has(cleaned)) relationIndex.set(cleaned, new Set());
169
+ relationIndex.get(cleaned).add(groupLower);
170
+ });
171
+ }
172
+ });
173
+ }
174
+ });
175
+
176
+ // ═══════════════════════════════════════════════════════════════════════════
177
+ // 5. Index components from the components object (with pdsTags)
178
+ // ═══════════════════════════════════════════════════════════════════════════
179
+ Object.values(referenceData.components || {}).forEach(comp => {
180
+ const tag = comp.tag?.toLowerCase();
181
+ const name = comp.displayName?.toLowerCase();
182
+ const cat = (comp.ontology?.category || comp.category || 'other').toLowerCase();
183
+
184
+ if (!categoryIndex.has(cat)) categoryIndex.set(cat, new Set());
185
+ if (tag) categoryIndex.get(cat).add(tag);
186
+ if (name) categoryIndex.get(cat).add(name);
187
+
188
+ // Index component tags
189
+ (comp.pdsTags || comp.ontology?.tags || []).forEach(t => {
190
+ const tagLower = t.toLowerCase();
191
+ if (!tagIndex.has(tagLower)) tagIndex.set(tagLower, new Set());
192
+ tagIndex.get(tagLower).add(tag);
193
+
194
+ if (!relationIndex.has(tagLower)) relationIndex.set(tagLower, new Set());
195
+ relationIndex.get(tagLower).add(tag);
196
+ if (tag) {
197
+ if (!relationIndex.has(tag)) relationIndex.set(tag, new Set());
198
+ relationIndex.get(tag).add(tagLower);
199
+ }
200
+ });
201
+ });
202
+
203
+ console.log('%c📚 Ontology indices built', 'color: #10b981', {
204
+ relations: relationIndex.size,
205
+ categories: categoryIndex.size,
206
+ tags: tagIndex.size
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Expand a search query using ontology indices
212
+ * @param {string} query - User's search term
213
+ * @returns {string[]} - Array of related tags to search for
214
+ */
215
+ function expandQuery(query) {
216
+ const q = query.toLowerCase().trim();
217
+ if (!q) return [];
218
+
219
+ const terms = new Set([q]);
220
+
221
+ // 1. Check searchRelations (primary SSoT for relationships)
222
+ if (relationIndex.has(q)) {
223
+ relationIndex.get(q).forEach(t => terms.add(t));
224
+ }
225
+
226
+ // 2. Check if query matches a category - expand to all category members
227
+ if (categoryIndex.has(q)) {
228
+ categoryIndex.get(q).forEach(t => terms.add(t));
229
+ }
230
+
231
+ // 3. Check if query is a tag - expand to items with that tag
232
+ if (tagIndex.has(q)) {
233
+ tagIndex.get(q).forEach(t => terms.add(t));
234
+ }
235
+
236
+ // 4. Partial matches: only for longer queries (3+ chars) to avoid over-matching
237
+ // Only add the matched key - NOT its relations (that would cause transitive pollution)
238
+ if (q.length >= 3) {
239
+ relationIndex.forEach((related, key) => {
240
+ // Only match if query is a prefix/suffix of key or vice versa
241
+ const isPrefix = key.startsWith(q) || q.startsWith(key);
242
+ const isSuffix = key.endsWith(q) || q.endsWith(key);
243
+ if (isPrefix || isSuffix) {
244
+ terms.add(key);
245
+ // Don't add related terms here - that causes transitive expansion pollution
246
+ }
247
+ });
248
+ }
249
+
250
+ // 5. Component tag variations (handle pds- prefix)
251
+ // Only add the component's tag/name - NOT all its pdsTags (which are too generic)
252
+ if (referenceData?.components) {
253
+ Object.values(referenceData.components).forEach(comp => {
254
+ const tag = comp.tag?.toLowerCase();
255
+ const name = comp.displayName?.toLowerCase();
256
+ const tagWithoutPrefix = tag?.replace('pds-', '');
257
+
258
+ // Match if query equals or closely matches the component
259
+ if (tag === q || name === q || tagWithoutPrefix === q ||
260
+ tag?.includes(q) || name?.includes(q)) {
261
+ terms.add(tag);
262
+ terms.add(tagWithoutPrefix);
263
+ terms.add(name);
264
+ // Don't add pdsTags - they're too generic and cause over-matching
265
+ }
266
+ });
267
+ }
268
+
269
+ // 6. Enhancement selectors
270
+ (referenceData?.enhancements || []).forEach(e => {
271
+ const sel = e.selector?.toLowerCase();
272
+ const id = e.id?.toLowerCase();
273
+ if (sel?.includes(q) || id?.includes(q)) {
274
+ terms.add(sel);
275
+ terms.add(id);
276
+ }
277
+ });
278
+
279
+ return Array.from(terms).filter(Boolean);
280
+ }
281
+
282
+ /**
283
+ * Check if a story item matches the search query using ontology relations
284
+ * @param {Object} item - Storybook sidebar item
285
+ * @param {string[]} expandedTerms - Expanded search terms
286
+ * @returns {boolean}
287
+ */
288
+ function matchesOntology(item, expandedTerms) {
289
+ if (!expandedTerms.length) return true;
290
+
291
+ // Build searchable text from item properties
292
+ const searchableText = [
293
+ item.name,
294
+ item.title,
295
+ item.id,
296
+ item.importPath
297
+ ].filter(Boolean).join(' ').toLowerCase();
298
+
299
+ // Match if ANY expanded term is found in the searchable text
300
+ return expandedTerms.some(term => searchableText.includes(term));
301
+ }
302
+
303
+ // ═══════════════════════════════════════════════════════════════════════════
304
+ // STORYBOOK INTEGRATION
305
+ // ═══════════════════════════════════════════════════════════════════════════
306
+
307
+ addons.register(ADDON_ID, (api) => {
308
+ let currentQuery = '';
309
+
310
+ const updateIndicator = (active, terms, noMatches = false) => {
311
+ let indicator = document.getElementById('pds-ontology-indicator');
312
+ if (!indicator) {
313
+ indicator = document.createElement('div');
314
+ indicator.id = 'pds-ontology-indicator';
315
+ indicator.innerHTML = `
316
+ <style>
317
+ #pds-ontology-indicator {
318
+ position: fixed;
319
+ bottom: 8px;
320
+ left: 8px;
321
+ background: var(--sb-bar-background, #1d1d1d);
322
+ border: 1px solid var(--sb-outline, #383838);
323
+ border-radius: 6px;
324
+ padding: 6px 10px;
325
+ font-size: 11px;
326
+ color: var(--sb-barTextColor, #999);
327
+ z-index: 100;
328
+ max-width: 280px;
329
+ display: none;
330
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
331
+ pointer-events: none;
332
+ }
333
+ #pds-ontology-indicator.active { display: block; }
334
+ #pds-ontology-indicator .title {
335
+ font-weight: bold;
336
+ color: var(--sb-primary, #029cfd);
337
+ margin-bottom: 4px;
338
+ }
339
+ #pds-ontology-indicator .terms {
340
+ display: flex;
341
+ flex-wrap: wrap;
342
+ gap: 4px;
343
+ }
344
+ #pds-ontology-indicator .term {
345
+ background: var(--sb-secondary, #444);
346
+ padding: 2px 6px;
347
+ border-radius: 3px;
348
+ font-family: monospace;
349
+ font-size: 10px;
350
+ }
351
+ #pds-ontology-indicator .term.original {
352
+ background: var(--sb-primary, #029cfd);
353
+ color: white;
354
+ }
355
+ #pds-ontology-indicator .no-matches {
356
+ color: #f59e0b;
357
+ font-style: italic;
358
+ }
359
+
360
+ /* PDS Ontology Search: Override Storybook's inline display styles */
361
+ #storybook-explorer-menu > div:last-child { display: none !important; }
362
+ #storybook-explorer-menu > div:first-child { display: block !important; }
363
+ [id^="downshift-"][role="listbox"] { display: none !important; }
364
+
365
+ /* Hide empty storybook message */
366
+ .css-1w747cv,
367
+ #storybook-explorer-tree > div > div > div > div > div { display: none !important; }
368
+
369
+ /* Show actual story items */
370
+ #storybook-explorer-tree .sidebar-subheading,
371
+ #storybook-explorer-tree .sidebar-item,
372
+ #storybook-explorer-tree [data-nodetype] { display: flex !important; }
373
+
374
+ /* No results message */
375
+ #storybook-explorer-tree:not(:has([data-nodetype="story"], [data-nodetype="component"], [data-nodetype="document"]))::after {
376
+ content: "No matching stories. Try: text, form, layout, modal";
377
+ display: block;
378
+ padding: 1rem;
379
+ color: var(--sb-barTextColor, #999);
380
+ font-style: italic;
381
+ text-align: center;
382
+ font-size: 12px;
383
+ }
384
+
385
+ /* Auto-expand filtered nodes */
386
+ body.pds-filter-active #storybook-explorer-tree [aria-expanded="false"] + div,
387
+ body.pds-filter-active #storybook-explorer-tree [data-nodetype="story"],
388
+ body.pds-filter-active #storybook-explorer-tree [data-nodetype="document"] { display: flex !important; }
389
+
390
+ body.pds-filter-active #storybook-explorer-tree button[aria-expanded="false"] .css-5ba62h svg,
391
+ body.pds-filter-active #storybook-explorer-tree button[aria-expanded="false"] .css-1yuef8z svg {
392
+ transform: rotate(90deg);
393
+ }
394
+ </style>
395
+ <div class="title">🔍 PDS Ontology Search</div>
396
+ <div class="terms"></div>
397
+ <div class="no-matches" style="display: none;">No matches. Try: text, form, layout, modal</div>
398
+ `;
399
+ document.body.appendChild(indicator);
400
+ }
401
+
402
+ indicator.classList.toggle('active', active);
403
+ document.body.classList.toggle('pds-filter-active', active);
404
+
405
+ const termsEl = indicator.querySelector('.terms');
406
+ const noMatchesEl = indicator.querySelector('.no-matches');
407
+
408
+ if (active && noMatches) {
409
+ termsEl.style.display = 'none';
410
+ noMatchesEl.style.display = 'block';
411
+ } else if (active && terms.length) {
412
+ termsEl.style.display = 'flex';
413
+ noMatchesEl.style.display = 'none';
414
+ const originalTerms = currentQuery.split(/\s+/);
415
+ termsEl.innerHTML = terms.slice(0, 20).map(t =>
416
+ `<span class="term${originalTerms.includes(t) ? ' original' : ''}">${t}</span>`
417
+ ).join('');
418
+ } else {
419
+ termsEl.style.display = 'none';
420
+ noMatchesEl.style.display = 'none';
421
+ }
422
+ };
423
+
424
+ const applyFilter = (query) => {
425
+ currentQuery = (query || '').trim().toLowerCase();
426
+
427
+ if (!currentQuery) {
428
+ api.experimental_setFilter(FILTER_ID, () => true);
429
+ updateIndicator(false, [], false);
430
+ return;
431
+ }
432
+
433
+ const expandedTerms = expandQuery(currentQuery);
434
+
435
+ api.experimental_setFilter(FILTER_ID, (item) => matchesOntology(item, expandedTerms));
436
+
437
+ setTimeout(() => {
438
+ const tree = document.getElementById('storybook-explorer-tree');
439
+ const hasVisibleItems = tree && tree.querySelector('[data-highlightable="true"]');
440
+ updateIndicator(true, expandedTerms, !hasVisibleItems);
441
+
442
+ document.querySelectorAll('#storybook-explorer-menu ol').forEach(ol => {
443
+ ol.style.display = 'none';
444
+ });
445
+ }, 100);
446
+ };
447
+
448
+ const hookSearch = () => {
449
+ const searchField = document.getElementById('storybook-explorer-searchfield');
450
+ if (!searchField) {
451
+ requestAnimationFrame(hookSearch);
452
+ return;
453
+ }
454
+
455
+ if (searchField.dataset.pdsHooked) return;
456
+ searchField.dataset.pdsHooked = 'true';
457
+
458
+ console.log('%c🎨 PDS Ontology Search Active', 'color: #029cfd; font-weight: bold; font-size: 14px');
459
+ console.log('%cSearch expands via ontology relations. Try: "text", "form", "modal"', 'color: #999');
460
+
461
+ let debounceTimer;
462
+ searchField.addEventListener('input', (e) => {
463
+ clearTimeout(debounceTimer);
464
+ debounceTimer = setTimeout(() => applyFilter(e.target.value), 100);
465
+ });
466
+
467
+ searchField.addEventListener('keydown', (e) => {
468
+ if (e.key === 'Escape') setTimeout(() => applyFilter(''), 10);
469
+ });
470
+
471
+ searchField.addEventListener('search', () => {
472
+ if (!searchField.value) applyFilter('');
473
+ });
474
+
475
+ // Watch for the clear button click (the X button in the combobox)
476
+ // The button is a sibling or nearby element that clears the input
477
+ const searchContainer = searchField.closest('[role="search"]') || searchField.parentElement?.parentElement;
478
+ if (searchContainer) {
479
+ searchContainer.addEventListener('click', (e) => {
480
+ // Check if clicked element is a button (clear button) or inside one
481
+ const button = e.target.closest('button');
482
+ if (button && button !== searchField) {
483
+ // Small delay to let the clear action complete
484
+ setTimeout(() => {
485
+ if (!searchField.value) {
486
+ applyFilter('');
487
+ }
488
+ }, 50);
489
+ }
490
+ });
491
+ }
492
+
493
+ // Also poll for value changes as a fallback (handles programmatic clears)
494
+ let lastValue = searchField.value;
495
+ const checkValue = () => {
496
+ if (searchField.value !== lastValue) {
497
+ lastValue = searchField.value;
498
+ clearTimeout(debounceTimer);
499
+ debounceTimer = setTimeout(() => applyFilter(searchField.value), 100);
500
+ }
501
+ requestAnimationFrame(checkValue);
502
+ };
503
+ checkValue();
504
+ };
505
+
506
+ applyFilter('');
507
+
508
+ const initializeOntologySearch = async () => {
509
+ await loadOntology();
510
+ hookSearch();
511
+ };
512
+
513
+ if (document.readyState === 'complete') {
514
+ setTimeout(initializeOntologySearch, 500);
515
+ } else {
516
+ window.addEventListener('load', () => setTimeout(initializeOntologySearch, 500));
517
+ }
518
+
519
+ const mutationObserver = new MutationObserver(() => {
520
+ const searchField = document.getElementById('storybook-explorer-searchfield');
521
+ if (searchField && !searchField.dataset.pdsHooked) hookSearch();
522
+ });
523
+ mutationObserver.observe(document.body, { childList: true, subtree: true });
524
+
525
+ // Debug API
526
+ window.pdsOntology = {
527
+ search: (query) => {
528
+ const field = document.getElementById('storybook-explorer-searchfield');
529
+ if (field) {
530
+ field.value = query;
531
+ field.dispatchEvent(new Event('input', { bubbles: true }));
532
+ }
533
+ applyFilter(query);
534
+ },
535
+ expand: expandQuery,
536
+ clear: () => {
537
+ const field = document.getElementById('storybook-explorer-searchfield');
538
+ if (field) {
539
+ field.value = '';
540
+ field.dispatchEvent(new Event('input', { bubbles: true }));
541
+ }
542
+ applyFilter('');
543
+ },
544
+ data: () => referenceData,
545
+ relations: () => Object.fromEntries([...relationIndex].map(([k, v]) => [k, [...v]])),
546
+ categories: () => Object.fromEntries([...categoryIndex].map(([k, v]) => [k, [...v]])),
547
+ tags: () => Object.fromEntries([...tagIndex].map(([k, v]) => [k, [...v]])),
548
+ currentQuery: () => currentQuery
549
+ };
550
+ });