@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.
- package/.storybook/addons/pds-configurator/register.js +0 -11
- package/.storybook/main.js +2 -0
- package/.storybook/manager.js +550 -0
- package/dist/pds-reference.json +1747 -50
- package/package.json +2 -2
- package/public/assets/js/app.js +138 -138
- package/public/assets/js/pds.js +1 -1
- package/scripts/build-pds-reference.mjs +63 -8
- package/src/js/pds-core/pds-ontology.js +178 -1
- package/stories/layout/LayoutSystem.stories.js +1 -1
- package/stories/primitives/Alerts.stories.js +2 -2
- package/stories/reference/reference-catalog.js +32 -5
- package/stories/utils/PdsAsk.stories.js +2 -2
- package/.storybook/addons/pds-configurator/SearchTool.js +0 -44
|
@@ -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
|
});
|
package/.storybook/main.js
CHANGED
|
@@ -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
|
+
});
|