@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,256 +1,803 @@
1
- // Pure Design System Ontology (PDS)
2
- // This file is the single source-of-truth metadata for primitives, components, tokens, themes and enhancements.
3
-
4
- export const ontology = {
5
- meta: { name: "Pure Design System Ontology", version: "0.1" },
6
-
7
- tokens: {
8
- colors: ["primary", "secondary", "accent", "success", "warning", "danger", "info"],
9
- spacing: ["xs", "sm", "md", "lg", "xl"],
10
- typography: ["heading", "body", "mono"],
11
- themes: ["light", "dark"],
12
- },
13
-
14
-
15
- primitives: [
16
- { id: "badge", name: "Badge / Pill", selectors: [".badge", ".pill", ".tag", ".chip"] },
17
- { id: "card", name: "Card", selectors: [".card", ".card-basic", ".card-elevated", ".card-outlined", ".card-interactive"] },
18
- { id: "surface", name: "Surface", selectors: [".surface", ".surface-base", ".surface-raised", ".surface-overlay", ".surface-subtle", ".surface-elevated", ".surface-sunken"] },
19
- { id: "alert", name: "Alert", selectors: [".alert", ".alert-info", ".alert-success", ".alert-warning", ".alert-danger"] },
20
- { id: "dialog", name: "Dialog", selectors: ["dialog", ".dialog"] },
21
- { id: "table", name: "Table", selectors: ["table", ".table-responsive", ".data-table"] },
22
- { id: "button", name: "Button", selectors: ["button", "[class^='btn-']", ".icon-only"] },
23
- { id: "fieldset", name: "Fieldset Group", selectors: ["fieldset[role='group']", "fieldset[role='radiogroup']"] },
24
- { id: "label-field", name: "Label+Input", selectors: ["label"] },
25
- { id: "accordion", name: "Accordion", selectors: [".accordion", ".accordion-item", "details"] },
26
- { id: "icon", name: "Icon", selectors: ["pds-icon", ".icon", ".icon-*"] },
27
- { id: "figure", name: "Figure/Media", selectors: ["figure", "figure.media"] },
28
- { id: "gallery", name: "Gallery", selectors: [".gallery", ".gallery-grid"] },
29
- ],
30
-
31
- components: [
32
- { id: "pds-tabstrip", name: "Tab Strip", selectors: ["pds-tabstrip"] },
33
- { id: "pds-drawer", name: "Drawer", selectors: ["pds-drawer"] },
34
- { id: "pds-upload", name: "Upload", selectors: ["pds-upload"] },
35
- ],
36
-
37
- // Layout utilities - patterns for structuring content
38
- layoutPatterns: [
39
- { id: "grid", name: "Grid Container", selectors: [".grid", ".demo-grid"], description: "CSS Grid layout container" },
40
- { id: "grid-auto", name: "Auto-fit Grid", selectors: [".grid-auto-sm", ".grid-auto-md", ".grid-auto-lg", ".grid-auto-xl"], description: "Responsive auto-fit grid" },
41
- { id: "grid-cols", name: "Grid Columns", selectors: [".grid-cols-1", ".grid-cols-2", ".grid-cols-3", ".grid-cols-4", ".grid-cols-6"], description: "Fixed column grid" },
42
- { id: "flex", name: "Flex Container", selectors: [".flex", ".flex-wrap"], description: "Flexbox layout container" },
43
- { id: "container", name: "Container", selectors: [".container"], description: "Centered max-width container" },
44
- { id: "media-grid", name: "Media Grid", selectors: [".media-grid"], description: "Grid for media elements" },
45
- ],
46
-
47
- utilities: [
48
- ".btn-group",
49
- ".demo-grid",
50
- ".color-scale",
51
- ".gap-*",
52
- ".items-*",
53
- ".justify-*",
54
- ".border-gradient",
55
- ".border-gradient-primary",
56
- ".border-gradient-accent",
57
- ".border-gradient-secondary",
58
- ".border-gradient-soft",
59
- ".border-gradient-medium",
60
- ".border-gradient-strong",
61
- ".border-glow",
62
- ".border-glow-sm",
63
- ".border-glow-lg",
64
- ".border-gradient-glow",
65
- ".border-glow-*"
66
- ],
67
-
68
- styles: {
69
- typography: ["headings", "body", "code"],
70
- icons: { source: "svg", sets: ["core", "brand"] },
71
- interactive: ["focus", "hover", "active"],
72
- },
73
- };
74
-
75
- // Safe matches with try/catch for invalid selectors or environments without .matches
76
- function tryMatches(el, selector) {
77
- if (!el || !selector) return false;
78
- try {
79
- return el.matches(selector);
80
- } catch (e) {
81
- return false;
82
- }
83
- }
84
-
85
- function safeClosest(el, selector) {
86
- if (!el || !selector || !el.closest) return null;
87
- try {
88
- return el.closest(selector);
89
- } catch (e) {
90
- return null;
91
- }
92
- }
93
-
94
- // Find component for an element using the ontology
95
- export function findComponentForElement(startEl, { maxDepth = 5 } = {}) {
96
- if (!startEl) return null;
97
- if (startEl.closest && startEl.closest('.showcase-toc')) return null;
98
-
99
- let current = startEl;
100
- let depth = 0;
101
-
102
- while (current && depth < maxDepth) {
103
- depth++;
104
-
105
- // never traverse past the showcase
106
- if (current.tagName === 'DS-SHOWCASE') return null;
107
-
108
- // skip the section wrapper and continue climbing
109
- if (current.classList && current.classList.contains('showcase-section')) {
110
- current = current.parentElement;
111
- continue;
112
- }
113
-
114
- // 1) progressive enhancements
115
- for (const sel of PDS.ontology.enhancements) {
116
- if (tryMatches(current, sel)) {
117
- return { element: current, componentType: 'enhanced-component', displayName: sel };
118
- }
119
- }
120
-
121
- // 2) Fieldset role groups
122
- if (current.tagName === 'FIELDSET') {
123
- const role = current.getAttribute('role');
124
- if (role === 'group' || role === 'radiogroup') {
125
- return { element: current, componentType: 'form-group', displayName: role === 'radiogroup' ? 'radio group' : 'form group' };
126
- }
127
- }
128
-
129
- // 3) label+input
130
- if (current.tagName === 'LABEL') {
131
- if (current.querySelector && current.querySelector('input,select,textarea')) {
132
- return { element: current, componentType: 'form-control', displayName: 'label with input' };
133
- }
134
- }
135
- const labelAncestor = current.closest ? current.closest('label') : null;
136
- if (labelAncestor && labelAncestor.querySelector && labelAncestor.querySelector('input,select,textarea')) {
137
- return { element: labelAncestor, componentType: 'form-control', displayName: 'label with input' };
138
- }
139
-
140
- // 4) primitives
141
- for (const prim of PDS.ontology.primitives) {
142
- // handle each selector safely, support wildcard class prefix like .icon-*
143
- for (const sel of prim.selectors || []) {
144
- const s = String(sel || '').trim();
145
-
146
- // Wildcard class prefix handling (e.g., .icon-*)
147
- if (s.includes('*')) {
148
- // Only support simple class wildcard like .prefix-*
149
- if (s.startsWith('.')) {
150
- const prefix = s.slice(1).replace(/\*/g, '');
151
- if (current.classList && Array.from(current.classList).some((c) => c.startsWith(prefix))) {
152
- return { element: current, componentType: 'pds-primitive', displayName: prim.name || prim.id };
153
- }
154
- // Also try to find an ancestor with such a class (but do not use closest with wildcard)
155
- let ancestor = current.parentElement;
156
- let levels = 0;
157
- while (ancestor && levels < maxDepth) {
158
- if (ancestor.classList && Array.from(ancestor.classList).some((c) => c.startsWith(prefix)) && ancestor.tagName !== 'DS-SHOWCASE') {
159
- return { element: ancestor, componentType: 'pds-primitive', displayName: prim.name || prim.id };
160
- }
161
- ancestor = ancestor.parentElement;
162
- levels++;
163
- }
164
- continue;
165
- }
166
- // unsupported wildcard pattern - skip
167
- continue;
168
- }
169
-
170
- // Normal selector: try matches, then safeClosest
171
- if (tryMatches(current, s)) {
172
- return { element: current, componentType: 'pds-primitive', displayName: prim.name || prim.id };
173
- }
174
- const ancestor = safeClosest(current, s);
175
- if (ancestor && ancestor.tagName !== 'DS-SHOWCASE') {
176
- return { element: ancestor, componentType: 'pds-primitive', displayName: prim.name || prim.id };
177
- }
178
- }
179
-
180
- // class prefix fallback for selectors that are like .icon-* written differently
181
- if (current.classList) {
182
- const clsList = Array.from(current.classList);
183
- for (const s of prim.selectors || []) {
184
- if (typeof s === 'string' && s.includes('*') && s.startsWith('.')) {
185
- const prefix = s.slice(1).replace(/\*/g, '');
186
- if (clsList.some((c) => c.startsWith(prefix))) {
187
- return { element: current, componentType: 'pds-primitive', displayName: prim.name || prim.id };
188
- }
189
- }
190
- }
191
- }
192
- }
193
-
194
- // 4.5) layout patterns - check before going higher in tree
195
- for (const layout of PDS.ontology.layoutPatterns || []) {
196
- for (const sel of layout.selectors || []) {
197
- const s = String(sel || '').trim();
198
-
199
- // Wildcard handling for gap-*, items-*, etc.
200
- if (s.includes('*')) {
201
- if (s.startsWith('.')) {
202
- const prefix = s.slice(1).replace(/\*/g, '');
203
- if (current.classList && Array.from(current.classList).some((c) => c.startsWith(prefix))) {
204
- return { element: current, componentType: 'layout-pattern', displayName: layout.name || layout.id };
205
- }
206
- }
207
- continue;
208
- }
209
-
210
- // Normal selector
211
- if (tryMatches(current, s)) {
212
- return { element: current, componentType: 'layout-pattern', displayName: layout.name || layout.id };
213
- }
214
- const ancestor = safeClosest(current, s);
215
- if (ancestor && ancestor.tagName !== 'DS-SHOWCASE') {
216
- return { element: ancestor, componentType: 'layout-pattern', displayName: layout.name || layout.id };
217
- }
218
- }
219
- }
220
-
221
- // 5) web components
222
- if (current.tagName && current.tagName.includes('-')) {
223
- return { element: current, componentType: 'web-component', displayName: current.tagName.toLowerCase() };
224
- }
225
-
226
- // 6) button/icon
227
- if (current.tagName === 'BUTTON') {
228
- const hasIcon = current.querySelector && current.querySelector('pds-icon');
229
- return { element: current, componentType: 'button', displayName: hasIcon ? 'button with icon' : 'button' };
230
- }
231
- if (tryMatches(current, 'pds-icon') || (current.closest && current.closest('pds-icon'))) {
232
- const el = tryMatches(current, 'pds-icon') ? current : current.closest('pds-icon');
233
- return { element: el, componentType: 'icon', displayName: `pds-icon (${el.getAttribute && el.getAttribute('icon') || 'unknown'})` };
234
- }
235
-
236
- // 7) nav dropdown
237
- if (tryMatches(current, 'nav[data-dropdown]') || (current.closest && current.closest('nav[data-dropdown]'))) {
238
- const el = tryMatches(current, 'nav[data-dropdown]') ? current : current.closest('nav[data-dropdown]');
239
- return { element: el, componentType: 'navigation', displayName: 'dropdown menu' };
240
- }
241
-
242
- // climb
243
- current = current.parentElement;
244
- }
245
-
246
- return null;
247
- }
248
-
249
- export function getAllSelectors() {
250
- const s = [];
251
- for (const p of PDS.ontology.primitives) s.push(...(p.selectors || []));
252
- for (const c of PDS.ontology.components) s.push(...(c.selectors || []));
253
- return Array.from(new Set(s));
254
- }
255
-
256
- export default ontology;
1
+ // Pure Design System Ontology (PDS)
2
+ // This file is the single source-of-truth metadata for primitives, components, tokens, themes and enhancements.
3
+ // Used by PDS.query() for searching and correlating concepts.
4
+
5
+ export const ontology = {
6
+ meta: {
7
+ name: "Pure Design System Ontology",
8
+ version: "1.0.0",
9
+ description: "Complete metadata registry for PDS primitives, components, utilities, and tokens"
10
+ },
11
+
12
+ // ═══════════════════════════════════════════════════════════════════════════
13
+ // DESIGN TOKENS
14
+ // ═══════════════════════════════════════════════════════════════════════════
15
+ tokens: {
16
+ colors: {
17
+ semantic: ["primary", "secondary", "accent", "success", "warning", "danger", "info"],
18
+ neutral: ["gray"],
19
+ shades: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950],
20
+ surface: ["base", "subtle", "elevated", "sunken", "overlay", "inverse", "translucent"],
21
+ text: ["default", "muted", "subtle", "inverse", "primary", "success", "warning", "danger", "info"]
22
+ },
23
+ spacing: {
24
+ scale: ["1", "2", "3", "4", "5", "6", "8", "10", "12", "16", "20", "24"],
25
+ semantic: ["xs", "sm", "md", "lg", "xl"]
26
+ },
27
+ typography: {
28
+ families: ["heading", "body", "mono"],
29
+ sizes: ["xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl"],
30
+ weights: ["light", "normal", "medium", "semibold", "bold"]
31
+ },
32
+ radius: {
33
+ scale: ["none", "sm", "base", "md", "lg", "xl", "2xl", "full"]
34
+ },
35
+ shadows: {
36
+ scale: ["none", "sm", "base", "md", "lg", "xl", "inner"]
37
+ },
38
+ themes: ["light", "dark"],
39
+ breakpoints: {
40
+ sm: 640,
41
+ md: 768,
42
+ lg: 1024,
43
+ xl: 1280
44
+ }
45
+ },
46
+
47
+ // ═══════════════════════════════════════════════════════════════════════════
48
+ // PRIMITIVES (Single-class styled components)
49
+ // ═══════════════════════════════════════════════════════════════════════════
50
+ primitives: [
51
+ {
52
+ id: "badge",
53
+ name: "Badge / Pill",
54
+ description: "Inline status indicators and labels",
55
+ selectors: [".badge", ".badge-primary", ".badge-secondary", ".badge-success", ".badge-info", ".badge-warning", ".badge-danger", ".badge-outline", ".badge-sm", ".badge-lg", ".pill", ".tag", ".chip"],
56
+ tags: ["status", "label", "indicator", "inline"],
57
+ category: "feedback"
58
+ },
59
+ {
60
+ id: "card",
61
+ name: "Card",
62
+ description: "Content container with padding, border-radius, and optional shadow",
63
+ selectors: [".card", ".card-basic", ".card-elevated", ".card-outlined", ".card-interactive"],
64
+ tags: ["container", "content", "grouping"],
65
+ category: "container"
66
+ },
67
+ {
68
+ id: "surface",
69
+ name: "Surface",
70
+ description: "Smart surface classes with automatic text/background color handling",
71
+ selectors: [".surface-base", ".surface-subtle", ".surface-elevated", ".surface-sunken", ".surface-overlay", ".surface-inverse", ".surface-translucent", ".surface-translucent-25", ".surface-translucent-50", ".surface-translucent-75", ".surface-primary", ".surface-secondary", ".surface-success", ".surface-warning", ".surface-danger", ".surface-info"],
72
+ tags: ["background", "theming", "color", "container"],
73
+ category: "theming"
74
+ },
75
+ {
76
+ id: "alert",
77
+ name: "Alert",
78
+ description: "Contextual feedback messages",
79
+ selectors: [".alert", ".alert-info", ".alert-success", ".alert-warning", ".alert-danger", ".alert-error", ".alert-dismissible", ".semantic-message"],
80
+ tags: ["feedback", "message", "notification", "status"],
81
+ category: "feedback"
82
+ },
83
+ {
84
+ id: "dialog",
85
+ name: "Dialog",
86
+ description: "Modal dialog element",
87
+ selectors: ["dialog", ".dialog"],
88
+ tags: ["modal", "overlay", "popup"],
89
+ category: "overlay"
90
+ },
91
+ {
92
+ id: "divider",
93
+ name: "Divider",
94
+ description: "Horizontal rule with optional label",
95
+ selectors: ["hr", "hr[data-content]"],
96
+ tags: ["separator", "line", "content-divider"],
97
+ category: "layout"
98
+ },
99
+ {
100
+ id: "table",
101
+ name: "Table",
102
+ description: "Data tables with responsive and styling variants",
103
+ selectors: ["table", ".table-responsive", ".table-striped", ".table-bordered", ".table-compact", ".data-table"],
104
+ tags: ["data", "grid", "tabular", "responsive"],
105
+ category: "data"
106
+ },
107
+ {
108
+ id: "button",
109
+ name: "Button",
110
+ description: "Interactive button element with variants",
111
+ selectors: ["button", ".btn-primary", ".btn-secondary", ".btn-outline", ".btn-sm", ".btn-xs", ".btn-lg", ".btn-working", ".icon-only"],
112
+ tags: ["interactive", "action", "cta", "form"],
113
+ category: "action"
114
+ },
115
+ {
116
+ id: "fieldset",
117
+ name: "Fieldset Group",
118
+ description: "Form field grouping for radio/checkbox groups",
119
+ selectors: ["fieldset[role='group']", "fieldset[role='radiogroup']", "fieldset.buttons"],
120
+ tags: ["form", "grouping", "radio", "checkbox"],
121
+ category: "form"
122
+ },
123
+ {
124
+ id: "label-field",
125
+ name: "Label+Input",
126
+ description: "Semantic label wrapping form input",
127
+ selectors: ["label", "label:has(input)", "label:has(select)", "label:has(textarea)"],
128
+ tags: ["form", "input", "accessibility"],
129
+ category: "form"
130
+ },
131
+ {
132
+ id: "accordion",
133
+ name: "Accordion",
134
+ description: "Collapsible content sections",
135
+ selectors: [".accordion", ".accordion-item", "details", "details > summary"],
136
+ tags: ["expandable", "collapsible", "disclosure"],
137
+ category: "disclosure"
138
+ },
139
+ {
140
+ id: "icon",
141
+ name: "Icon",
142
+ description: "SVG icon element with size and color variants",
143
+ selectors: ["pds-icon", ".icon-xs", ".icon-sm", ".icon-md", ".icon-lg", ".icon-xl", ".icon-primary", ".icon-secondary", ".icon-accent", ".icon-success", ".icon-warning", ".icon-danger", ".icon-info", ".icon-muted", ".icon-subtle", ".icon-text", ".icon-text-start", ".icon-text-end"],
144
+ tags: ["graphic", "symbol", "visual"],
145
+ category: "media"
146
+ },
147
+ {
148
+ id: "figure",
149
+ name: "Figure/Media",
150
+ description: "Figure element for images with captions",
151
+ selectors: ["figure", "figure.media", "figcaption"],
152
+ tags: ["image", "media", "caption"],
153
+ category: "media"
154
+ },
155
+ {
156
+ id: "gallery",
157
+ name: "Gallery",
158
+ description: "Image gallery grid",
159
+ selectors: [".gallery", ".gallery-grid", ".img-gallery"],
160
+ tags: ["images", "grid", "collection"],
161
+ category: "media"
162
+ },
163
+ {
164
+ id: "form",
165
+ name: "Form Container",
166
+ description: "Form styling and layout",
167
+ selectors: ["form", ".form-container", ".form-actions", ".field-description"],
168
+ tags: ["form", "input", "submission"],
169
+ category: "form"
170
+ },
171
+ {
172
+ id: "navigation",
173
+ name: "Navigation",
174
+ description: "Navigation elements and menus",
175
+ selectors: ["nav", "nav[data-dropdown]", "menu", "nav menu li"],
176
+ tags: ["menu", "links", "routing"],
177
+ category: "navigation"
178
+ }
179
+ ],
180
+
181
+ // ═══════════════════════════════════════════════════════════════════════════
182
+ // WEB COMPONENTS
183
+ // ═══════════════════════════════════════════════════════════════════════════
184
+ components: [
185
+ {
186
+ id: "pds-tabstrip",
187
+ name: "Tab Strip",
188
+ description: "Tabbed interface component",
189
+ selectors: ["pds-tabstrip"],
190
+ tags: ["tabs", "navigation", "panels"],
191
+ category: "navigation"
192
+ },
193
+ {
194
+ id: "pds-drawer",
195
+ name: "Drawer",
196
+ description: "Slide-out panel overlay",
197
+ selectors: ["pds-drawer"],
198
+ tags: ["panel", "overlay", "sidebar"],
199
+ category: "overlay"
200
+ },
201
+ {
202
+ id: "pds-upload",
203
+ name: "Upload",
204
+ description: "File upload component with drag-and-drop",
205
+ selectors: ["pds-upload"],
206
+ tags: ["file", "upload", "drag-drop", "form"],
207
+ category: "form"
208
+ },
209
+ {
210
+ id: "pds-icon",
211
+ name: "Icon",
212
+ description: "SVG icon web component",
213
+ selectors: ["pds-icon"],
214
+ tags: ["icon", "graphic", "svg"],
215
+ category: "media"
216
+ },
217
+ {
218
+ id: "pds-toaster",
219
+ name: "Toaster",
220
+ description: "Toast notification container",
221
+ selectors: ["pds-toaster"],
222
+ tags: ["notification", "toast", "feedback"],
223
+ category: "feedback"
224
+ },
225
+ {
226
+ id: "pds-jsonform",
227
+ name: "JSON Form",
228
+ description: "Auto-generated form from JSON Schema",
229
+ selectors: ["pds-jsonform"],
230
+ tags: ["form", "schema", "auto-generate"],
231
+ category: "form"
232
+ },
233
+ {
234
+ id: "pds-splitpanel",
235
+ name: "Split Panel",
236
+ description: "Resizable split pane layout",
237
+ selectors: ["pds-splitpanel"],
238
+ tags: ["layout", "resize", "panels"],
239
+ category: "layout"
240
+ },
241
+ {
242
+ id: "pds-scrollrow",
243
+ name: "Scroll Row",
244
+ description: "Horizontal scrolling row with snap points",
245
+ selectors: ["pds-scrollrow"],
246
+ tags: ["scroll", "horizontal", "carousel"],
247
+ category: "layout"
248
+ },
249
+ {
250
+ id: "pds-richtext",
251
+ name: "Rich Text",
252
+ description: "Rich text editor component",
253
+ selectors: ["pds-richtext"],
254
+ tags: ["editor", "wysiwyg", "text"],
255
+ category: "form"
256
+ },
257
+ {
258
+ id: "pds-calendar",
259
+ name: "Calendar",
260
+ description: "Date picker calendar component",
261
+ selectors: ["pds-calendar"],
262
+ tags: ["date", "picker", "calendar"],
263
+ category: "form"
264
+ }
265
+ ],
266
+
267
+ // ═══════════════════════════════════════════════════════════════════════════
268
+ // LAYOUT PATTERNS
269
+ // ═══════════════════════════════════════════════════════════════════════════
270
+ layoutPatterns: [
271
+ {
272
+ id: "container",
273
+ name: "Container",
274
+ description: "Centered max-width wrapper with padding",
275
+ selectors: [".container"],
276
+ tags: ["wrapper", "centered", "max-width", "page"],
277
+ category: "structure"
278
+ },
279
+ {
280
+ id: "grid",
281
+ name: "Grid",
282
+ description: "CSS Grid layout container",
283
+ selectors: [".grid"],
284
+ tags: ["layout", "columns", "css-grid"],
285
+ category: "layout"
286
+ },
287
+ {
288
+ id: "grid-cols",
289
+ name: "Grid Columns",
290
+ description: "Fixed column count grids",
291
+ selectors: [".grid-cols-1", ".grid-cols-2", ".grid-cols-3", ".grid-cols-4", ".grid-cols-6"],
292
+ tags: ["columns", "fixed", "grid"],
293
+ category: "layout"
294
+ },
295
+ {
296
+ id: "grid-auto",
297
+ name: "Auto-fit Grid",
298
+ description: "Responsive auto-fit grid with minimum widths",
299
+ selectors: [".grid-auto-sm", ".grid-auto-md", ".grid-auto-lg", ".grid-auto-xl"],
300
+ tags: ["responsive", "auto-fit", "fluid"],
301
+ category: "layout"
302
+ },
303
+ {
304
+ id: "flex",
305
+ name: "Flex Container",
306
+ description: "Flexbox layout with direction and wrap modifiers",
307
+ selectors: [".flex", ".flex-wrap", ".flex-col", ".flex-row"],
308
+ tags: ["flexbox", "layout", "alignment"],
309
+ category: "layout"
310
+ },
311
+ {
312
+ id: "grow",
313
+ name: "Flex Grow",
314
+ description: "Fill remaining flex space",
315
+ selectors: [".grow"],
316
+ tags: ["flex", "expand", "fill"],
317
+ category: "layout"
318
+ },
319
+ {
320
+ id: "stack",
321
+ name: "Stack",
322
+ description: "Vertical flex layout with predefined gaps",
323
+ selectors: [".stack-sm", ".stack-md", ".stack-lg", ".stack-xl"],
324
+ tags: ["vertical", "spacing", "column"],
325
+ category: "layout"
326
+ },
327
+ {
328
+ id: "gap",
329
+ name: "Gap",
330
+ description: "Spacing between flex/grid children",
331
+ selectors: [".gap-0", ".gap-xs", ".gap-sm", ".gap-md", ".gap-lg", ".gap-xl"],
332
+ tags: ["spacing", "margin", "gutters"],
333
+ category: "spacing"
334
+ },
335
+ {
336
+ id: "items",
337
+ name: "Items Alignment",
338
+ description: "Cross-axis alignment for flex/grid",
339
+ selectors: [".items-start", ".items-center", ".items-end", ".items-stretch", ".items-baseline"],
340
+ tags: ["alignment", "vertical", "cross-axis"],
341
+ category: "alignment"
342
+ },
343
+ {
344
+ id: "justify",
345
+ name: "Justify Content",
346
+ description: "Main-axis alignment for flex/grid",
347
+ selectors: [".justify-start", ".justify-center", ".justify-end", ".justify-between", ".justify-around", ".justify-evenly"],
348
+ tags: ["alignment", "horizontal", "main-axis"],
349
+ category: "alignment"
350
+ },
351
+ {
352
+ id: "max-width",
353
+ name: "Max-Width",
354
+ description: "Content width constraints",
355
+ selectors: [".max-w-sm", ".max-w-md", ".max-w-lg", ".max-w-xl"],
356
+ tags: ["width", "constraint", "readable"],
357
+ category: "sizing"
358
+ },
359
+ {
360
+ id: "section",
361
+ name: "Section Spacing",
362
+ description: "Vertical padding for content sections",
363
+ selectors: [".section", ".section-lg"],
364
+ tags: ["spacing", "vertical", "padding"],
365
+ category: "spacing"
366
+ },
367
+ {
368
+ id: "mobile-stack",
369
+ name: "Mobile Stack",
370
+ description: "Stack on mobile, row on desktop",
371
+ selectors: [".mobile-stack"],
372
+ tags: ["responsive", "mobile", "breakpoint"],
373
+ category: "responsive"
374
+ }
375
+ ],
376
+
377
+ // ═══════════════════════════════════════════════════════════════════════════
378
+ // UTILITIES (Low-level single-purpose classes)
379
+ // ═══════════════════════════════════════════════════════════════════════════
380
+ utilities: {
381
+ text: {
382
+ alignment: [".text-left", ".text-center", ".text-right"],
383
+ color: [".text-muted", ".text-primary", ".text-success", ".text-warning", ".text-danger", ".text-info"],
384
+ overflow: [".truncate"]
385
+ },
386
+ backdrop: {
387
+ base: [".backdrop"],
388
+ variants: [".backdrop-light", ".backdrop-dark"],
389
+ blur: [".backdrop-blur-sm", ".backdrop-blur-md", ".backdrop-blur-lg"]
390
+ },
391
+ shadow: {
392
+ scale: [".shadow-sm", ".shadow-base", ".shadow-md", ".shadow-lg", ".shadow-xl", ".shadow-inner", ".shadow-none"]
393
+ },
394
+ border: {
395
+ gradient: [".border-gradient", ".border-gradient-primary", ".border-gradient-accent", ".border-gradient-secondary", ".border-gradient-soft", ".border-gradient-medium", ".border-gradient-strong"],
396
+ glow: [".border-glow", ".border-glow-sm", ".border-glow-lg", ".border-glow-primary", ".border-glow-accent", ".border-glow-success", ".border-glow-warning", ".border-glow-danger"],
397
+ combined: [".border-gradient-glow"]
398
+ },
399
+ media: {
400
+ image: [".img-gallery", ".img-rounded-sm", ".img-rounded-md", ".img-rounded-lg", ".img-rounded-xl", ".img-rounded-full", ".img-inline"],
401
+ video: [".video-responsive"],
402
+ figure: [".figure-responsive"]
403
+ },
404
+ effects: {
405
+ glass: [".liquid-glass"]
406
+ }
407
+ },
408
+
409
+ // ═══════════════════════════════════════════════════════════════════════════
410
+ // RESPONSIVE UTILITIES (Breakpoint-prefixed)
411
+ // ═══════════════════════════════════════════════════════════════════════════
412
+ responsive: {
413
+ prefixes: ["sm", "md", "lg"],
414
+ utilities: {
415
+ grid: [":grid-cols-2", ":grid-cols-3", ":grid-cols-4"],
416
+ flex: [":flex-row"],
417
+ text: [":text-sm", ":text-lg", ":text-xl"],
418
+ spacing: [":p-6", ":p-8", ":p-12", ":gap-6", ":gap-8", ":gap-12"],
419
+ width: [":w-1/2", ":w-1/3", ":w-1/4"],
420
+ display: [":hidden", ":block"]
421
+ }
422
+ },
423
+
424
+ // ═══════════════════════════════════════════════════════════════════════════
425
+ // ENHANCEMENTS (Progressive enhancement selectors)
426
+ // ═══════════════════════════════════════════════════════════════════════════
427
+ enhancements: [
428
+ {
429
+ id: "dropdown",
430
+ selector: "nav[data-dropdown]",
431
+ description: "Dropdown menu from nav element",
432
+ tags: ["menu", "interactive", "navigation"]
433
+ },
434
+ {
435
+ id: "toggle",
436
+ selector: "label[data-toggle]",
437
+ description: "Toggle switch from checkbox",
438
+ tags: ["switch", "boolean", "form"]
439
+ },
440
+ {
441
+ id: "range",
442
+ selector: 'input[type="range"]',
443
+ description: "Enhanced range slider with output",
444
+ tags: ["slider", "input", "form"]
445
+ },
446
+ {
447
+ id: "required",
448
+ selector: "form [required]",
449
+ description: "Required field asterisk indicator",
450
+ tags: ["validation", "form", "accessibility"]
451
+ },
452
+ {
453
+ id: "open-group",
454
+ selector: "fieldset[role=group][data-open]",
455
+ description: "Editable checkbox/radio group",
456
+ tags: ["form", "dynamic", "editable"]
457
+ },
458
+ {
459
+ id: "working-button",
460
+ selector: "button.btn-working, a.btn-working",
461
+ description: "Button with loading spinner",
462
+ tags: ["loading", "async", "feedback"]
463
+ },
464
+ {
465
+ id: "labeled-divider",
466
+ selector: "hr[data-content]",
467
+ description: "Horizontal rule with centered label",
468
+ tags: ["divider", "separator", "text"]
469
+ }
470
+ ],
471
+
472
+ // ═══════════════════════════════════════════════════════════════════════════
473
+ // SEMANTIC CATEGORIES (For correlation and search)
474
+ // ═══════════════════════════════════════════════════════════════════════════
475
+ categories: {
476
+ feedback: {
477
+ description: "User feedback and status indicators",
478
+ primitives: ["alert", "badge"],
479
+ components: ["pds-toaster"]
480
+ },
481
+ form: {
482
+ description: "Form inputs and controls",
483
+ primitives: ["button", "fieldset", "label-field", "form"],
484
+ components: ["pds-upload", "pds-jsonform", "pds-richtext", "pds-calendar"]
485
+ },
486
+ layout: {
487
+ description: "Page structure and content arrangement",
488
+ patterns: ["container", "grid", "flex", "stack", "section"],
489
+ components: ["pds-splitpanel", "pds-scrollrow"]
490
+ },
491
+ navigation: {
492
+ description: "Navigation and routing",
493
+ primitives: ["navigation"],
494
+ components: ["pds-tabstrip", "pds-drawer"]
495
+ },
496
+ media: {
497
+ description: "Images, icons, and visual content",
498
+ primitives: ["icon", "figure", "gallery"],
499
+ components: ["pds-icon"]
500
+ },
501
+ overlay: {
502
+ description: "Modal and overlay content",
503
+ primitives: ["dialog"],
504
+ components: ["pds-drawer"]
505
+ },
506
+ data: {
507
+ description: "Data display and tables",
508
+ primitives: ["table"]
509
+ },
510
+ theming: {
511
+ description: "Colors, surfaces, and visual theming",
512
+ primitives: ["surface"]
513
+ }
514
+ },
515
+
516
+ // ═══════════════════════════════════════════════════════════════════════════
517
+ // STYLE METADATA
518
+ // ═══════════════════════════════════════════════════════════════════════════
519
+ styles: {
520
+ typography: ["headings", "body", "code", "links"],
521
+ icons: { source: "svg", sets: ["core", "brand"] },
522
+ interactive: ["focus", "hover", "active", "disabled"],
523
+ states: ["success", "warning", "danger", "info", "muted"]
524
+ }
525
+ };
526
+
527
+ // ═══════════════════════════════════════════════════════════════════════════
528
+ // HELPER FUNCTIONS
529
+ // ═══════════════════════════════════════════════════════════════════════════
530
+
531
+ // Safe matches with try/catch for invalid selectors or environments without .matches
532
+ function tryMatches(el, selector) {
533
+ if (!el || !selector) return false;
534
+ try {
535
+ return el.matches(selector);
536
+ } catch (e) {
537
+ return false;
538
+ }
539
+ }
540
+
541
+ function safeClosest(el, selector) {
542
+ if (!el || !selector || !el.closest) return null;
543
+ try {
544
+ return el.closest(selector);
545
+ } catch (e) {
546
+ return null;
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Find component for an element using the ontology
552
+ * @param {HTMLElement} startEl - Starting element to search from
553
+ * @param {Object} options - Search options
554
+ * @param {number} options.maxDepth - Maximum depth to traverse (default: 5)
555
+ * @returns {Object|null} Component info or null
556
+ */
557
+ export function findComponentForElement(startEl, { maxDepth = 5 } = {}) {
558
+ if (!startEl) return null;
559
+ if (startEl.closest && startEl.closest('.showcase-toc')) return null;
560
+
561
+ let current = startEl;
562
+ let depth = 0;
563
+
564
+ while (current && depth < maxDepth) {
565
+ depth++;
566
+
567
+ // never traverse past the showcase
568
+ if (current.tagName === 'DS-SHOWCASE') return null;
569
+
570
+ // skip the section wrapper and continue climbing
571
+ if (current.classList && current.classList.contains('showcase-section')) {
572
+ current = current.parentElement;
573
+ continue;
574
+ }
575
+
576
+ // 1) progressive enhancements
577
+ for (const enh of PDS.ontology.enhancements) {
578
+ const sel = enh.selector || enh;
579
+ if (tryMatches(current, sel)) {
580
+ return { element: current, componentType: 'enhanced-component', displayName: enh.description || sel, id: enh.id };
581
+ }
582
+ }
583
+
584
+ // 2) Fieldset role groups
585
+ if (current.tagName === 'FIELDSET') {
586
+ const role = current.getAttribute('role');
587
+ if (role === 'group' || role === 'radiogroup') {
588
+ return { element: current, componentType: 'form-group', displayName: role === 'radiogroup' ? 'radio group' : 'form group' };
589
+ }
590
+ }
591
+
592
+ // 3) label+input
593
+ if (current.tagName === 'LABEL') {
594
+ if (current.querySelector && current.querySelector('input,select,textarea')) {
595
+ return { element: current, componentType: 'form-control', displayName: 'label with input' };
596
+ }
597
+ }
598
+ const labelAncestor = current.closest ? current.closest('label') : null;
599
+ if (labelAncestor && labelAncestor.querySelector && labelAncestor.querySelector('input,select,textarea')) {
600
+ return { element: labelAncestor, componentType: 'form-control', displayName: 'label with input' };
601
+ }
602
+
603
+ // 4) primitives
604
+ for (const prim of PDS.ontology.primitives) {
605
+ // handle each selector safely, support wildcard class prefix like .icon-*
606
+ for (const sel of prim.selectors || []) {
607
+ const s = String(sel || '').trim();
608
+
609
+ // Wildcard class prefix handling (e.g., .icon-*)
610
+ if (s.includes('*')) {
611
+ // Only support simple class wildcard like .prefix-*
612
+ if (s.startsWith('.')) {
613
+ const prefix = s.slice(1).replace(/\*/g, '');
614
+ if (current.classList && Array.from(current.classList).some((c) => c.startsWith(prefix))) {
615
+ return { element: current, componentType: 'pds-primitive', displayName: prim.name || prim.id, id: prim.id, tags: prim.tags };
616
+ }
617
+ // Also try to find an ancestor with such a class (but do not use closest with wildcard)
618
+ let ancestor = current.parentElement;
619
+ let levels = 0;
620
+ while (ancestor && levels < maxDepth) {
621
+ if (ancestor.classList && Array.from(ancestor.classList).some((c) => c.startsWith(prefix)) && ancestor.tagName !== 'DS-SHOWCASE') {
622
+ return { element: ancestor, componentType: 'pds-primitive', displayName: prim.name || prim.id, id: prim.id, tags: prim.tags };
623
+ }
624
+ ancestor = ancestor.parentElement;
625
+ levels++;
626
+ }
627
+ continue;
628
+ }
629
+ // unsupported wildcard pattern - skip
630
+ continue;
631
+ }
632
+
633
+ // Normal selector: try matches, then safeClosest
634
+ if (tryMatches(current, s)) {
635
+ return { element: current, componentType: 'pds-primitive', displayName: prim.name || prim.id, id: prim.id, tags: prim.tags };
636
+ }
637
+ const ancestor = safeClosest(current, s);
638
+ if (ancestor && ancestor.tagName !== 'DS-SHOWCASE') {
639
+ return { element: ancestor, componentType: 'pds-primitive', displayName: prim.name || prim.id, id: prim.id, tags: prim.tags };
640
+ }
641
+ }
642
+
643
+ // class prefix fallback for selectors that are like .icon-* written differently
644
+ if (current.classList) {
645
+ const clsList = Array.from(current.classList);
646
+ for (const s of prim.selectors || []) {
647
+ if (typeof s === 'string' && s.includes('*') && s.startsWith('.')) {
648
+ const prefix = s.slice(1).replace(/\*/g, '');
649
+ if (clsList.some((c) => c.startsWith(prefix))) {
650
+ return { element: current, componentType: 'pds-primitive', displayName: prim.name || prim.id, id: prim.id, tags: prim.tags };
651
+ }
652
+ }
653
+ }
654
+ }
655
+ }
656
+
657
+ // 4.5) layout patterns - check before going higher in tree
658
+ for (const layout of PDS.ontology.layoutPatterns || []) {
659
+ for (const sel of layout.selectors || []) {
660
+ const s = String(sel || '').trim();
661
+
662
+ // Wildcard handling for gap-*, items-*, etc.
663
+ if (s.includes('*')) {
664
+ if (s.startsWith('.')) {
665
+ const prefix = s.slice(1).replace(/\*/g, '');
666
+ if (current.classList && Array.from(current.classList).some((c) => c.startsWith(prefix))) {
667
+ return { element: current, componentType: 'layout-pattern', displayName: layout.name || layout.id, id: layout.id, tags: layout.tags };
668
+ }
669
+ }
670
+ continue;
671
+ }
672
+
673
+ // Normal selector
674
+ if (tryMatches(current, s)) {
675
+ return { element: current, componentType: 'layout-pattern', displayName: layout.name || layout.id, id: layout.id, tags: layout.tags };
676
+ }
677
+ const ancestor = safeClosest(current, s);
678
+ if (ancestor && ancestor.tagName !== 'DS-SHOWCASE') {
679
+ return { element: ancestor, componentType: 'layout-pattern', displayName: layout.name || layout.id, id: layout.id, tags: layout.tags };
680
+ }
681
+ }
682
+ }
683
+
684
+ // 5) web components
685
+ if (current.tagName && current.tagName.includes('-')) {
686
+ const tagName = current.tagName.toLowerCase();
687
+ const comp = PDS.ontology.components.find(c => c.selectors.includes(tagName));
688
+ return {
689
+ element: current,
690
+ componentType: 'web-component',
691
+ displayName: comp?.name || tagName,
692
+ id: comp?.id || tagName,
693
+ tags: comp?.tags
694
+ };
695
+ }
696
+
697
+ // 6) button/icon
698
+ if (current.tagName === 'BUTTON') {
699
+ const hasIcon = current.querySelector && current.querySelector('pds-icon');
700
+ return { element: current, componentType: 'button', displayName: hasIcon ? 'button with icon' : 'button', id: 'button' };
701
+ }
702
+ if (tryMatches(current, 'pds-icon') || (current.closest && current.closest('pds-icon'))) {
703
+ const el = tryMatches(current, 'pds-icon') ? current : current.closest('pds-icon');
704
+ return { element: el, componentType: 'icon', displayName: `pds-icon (${el.getAttribute && el.getAttribute('icon') || 'unknown'})`, id: 'pds-icon' };
705
+ }
706
+
707
+ // 7) nav dropdown
708
+ if (tryMatches(current, 'nav[data-dropdown]') || (current.closest && current.closest('nav[data-dropdown]'))) {
709
+ const el = tryMatches(current, 'nav[data-dropdown]') ? current : current.closest('nav[data-dropdown]');
710
+ return { element: el, componentType: 'navigation', displayName: 'dropdown menu', id: 'dropdown' };
711
+ }
712
+
713
+ // climb
714
+ current = current.parentElement;
715
+ }
716
+
717
+ return null;
718
+ }
719
+
720
+ /**
721
+ * Get all CSS selectors from the ontology
722
+ * @returns {string[]} Array of all selectors
723
+ */
724
+ export function getAllSelectors() {
725
+ const s = [];
726
+ for (const p of PDS.ontology.primitives) s.push(...(p.selectors || []));
727
+ for (const c of PDS.ontology.components) s.push(...(c.selectors || []));
728
+ for (const l of PDS.ontology.layoutPatterns || []) s.push(...(l.selectors || []));
729
+ return Array.from(new Set(s));
730
+ }
731
+
732
+ /**
733
+ * Search the ontology by tag, name, or category
734
+ * @param {string} query - Search term
735
+ * @param {Object} options - Search options
736
+ * @returns {Object[]} Matching items
737
+ */
738
+ export function searchOntology(query, options = {}) {
739
+ const q = query.toLowerCase();
740
+ const results = [];
741
+
742
+ const searchIn = (items, type) => {
743
+ for (const item of items) {
744
+ const matches =
745
+ item.id?.toLowerCase().includes(q) ||
746
+ item.name?.toLowerCase().includes(q) ||
747
+ item.description?.toLowerCase().includes(q) ||
748
+ item.tags?.some(t => t.toLowerCase().includes(q)) ||
749
+ item.category?.toLowerCase().includes(q) ||
750
+ item.selectors?.some(s => s.toLowerCase().includes(q));
751
+
752
+ if (matches) {
753
+ results.push({ ...item, type });
754
+ }
755
+ }
756
+ };
757
+
758
+ if (!options.type || options.type === 'primitive') {
759
+ searchIn(ontology.primitives, 'primitive');
760
+ }
761
+ if (!options.type || options.type === 'component') {
762
+ searchIn(ontology.components, 'component');
763
+ }
764
+ if (!options.type || options.type === 'layout') {
765
+ searchIn(ontology.layoutPatterns, 'layout');
766
+ }
767
+ if (!options.type || options.type === 'enhancement') {
768
+ searchIn(ontology.enhancements, 'enhancement');
769
+ }
770
+
771
+ return results;
772
+ }
773
+
774
+ /**
775
+ * Get items by category
776
+ * @param {string} category - Category name
777
+ * @returns {Object} Items grouped by type
778
+ */
779
+ export function getByCategory(category) {
780
+ const cat = category.toLowerCase();
781
+ return {
782
+ primitives: ontology.primitives.filter(p => p.category === cat),
783
+ components: ontology.components.filter(c => c.category === cat),
784
+ layouts: ontology.layoutPatterns.filter(l => l.category === cat)
785
+ };
786
+ }
787
+
788
+ /**
789
+ * Get all available tags
790
+ * @returns {string[]} Sorted unique tags
791
+ */
792
+ export function getAllTags() {
793
+ const tags = new Set();
794
+
795
+ ontology.primitives.forEach(p => p.tags?.forEach(t => tags.add(t)));
796
+ ontology.components.forEach(c => c.tags?.forEach(t => tags.add(t)));
797
+ ontology.layoutPatterns.forEach(l => l.tags?.forEach(t => tags.add(t)));
798
+ ontology.enhancements.forEach(e => e.tags?.forEach(t => tags.add(t)));
799
+
800
+ return Array.from(tags).sort();
801
+ }
802
+
803
+ export default ontology;