@vanduo-oss/framework 1.5.0 → 1.6.0
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/CHANGELOG.md +27 -0
- package/README.md +33 -6
- package/css/components/expanding-cards.css +94 -25
- package/css/components/navbar.css +11 -0
- package/css/components/popover.css +97 -0
- package/css/components/theme-customizer.css +53 -0
- package/css/core/colors-fib-base.css +524 -0
- package/css/core/colors-palette.css +786 -0
- package/css/core/colors.css +54 -46
- package/css/core/tokens.css +19 -19
- package/css/vanduo.css +8 -1
- package/dist/build-info.json +3 -3
- package/dist/vanduo-core.css +1588 -86
- package/dist/vanduo-core.css.map +1 -1
- package/dist/vanduo-core.min.css +2 -2
- package/dist/vanduo-core.min.css.map +1 -1
- package/dist/vanduo.cjs.js +394 -9
- package/dist/vanduo.cjs.js.map +3 -3
- package/dist/vanduo.cjs.min.js +16 -10
- package/dist/vanduo.cjs.min.js.map +4 -4
- package/dist/vanduo.css +1588 -86
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +394 -9
- package/dist/vanduo.esm.js.map +3 -3
- package/dist/vanduo.esm.min.js +16 -10
- package/dist/vanduo.esm.min.js.map +4 -4
- package/dist/vanduo.js +394 -9
- package/dist/vanduo.js.map +3 -3
- package/dist/vanduo.min.css +2 -2
- package/dist/vanduo.min.css.map +1 -1
- package/dist/vanduo.min.js +16 -10
- package/dist/vanduo.min.js.map +4 -4
- package/js/components/doc-search.js +9 -4
- package/js/components/expanding-cards.js +15 -3
- package/js/components/popover.js +295 -0
- package/js/components/search.js +108 -0
- package/js/components/theme-customizer.js +54 -1
- package/js/index.js +2 -0
- package/js/utils/helpers.js +12 -4
- package/package.json +7 -4
|
@@ -717,12 +717,17 @@
|
|
|
717
717
|
}
|
|
718
718
|
|
|
719
719
|
/**
|
|
720
|
-
* Escape HTML entities
|
|
720
|
+
* Escape HTML entities. Escapes quotes too, so output is safe in the
|
|
721
|
+
* attribute contexts this module uses it in (data-category="…", class="…").
|
|
721
722
|
*/
|
|
722
723
|
function escapeHtml(text) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
724
|
+
if (text === null || text === undefined) return '';
|
|
725
|
+
return String(text)
|
|
726
|
+
.replace(/&/g, '&')
|
|
727
|
+
.replace(/</g, '<')
|
|
728
|
+
.replace(/>/g, '>')
|
|
729
|
+
.replace(/"/g, '"')
|
|
730
|
+
.replace(/'/g, ''');
|
|
726
731
|
}
|
|
727
732
|
|
|
728
733
|
/**
|
|
@@ -45,7 +45,14 @@
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
const onKeydown = function (e) {
|
|
48
|
-
if (
|
|
48
|
+
if (
|
|
49
|
+
e.key !== 'ArrowLeft' &&
|
|
50
|
+
e.key !== 'ArrowRight' &&
|
|
51
|
+
e.key !== 'ArrowUp' &&
|
|
52
|
+
e.key !== 'ArrowDown' &&
|
|
53
|
+
e.key !== 'Home' &&
|
|
54
|
+
e.key !== 'End'
|
|
55
|
+
) {
|
|
49
56
|
return;
|
|
50
57
|
}
|
|
51
58
|
const cards = getCards().filter(function (c) {
|
|
@@ -61,10 +68,15 @@
|
|
|
61
68
|
}
|
|
62
69
|
if (idx < 0) idx = 0;
|
|
63
70
|
|
|
64
|
-
|
|
71
|
+
const isVertical = window.getComputedStyle(container).flexDirection === 'column';
|
|
72
|
+
if (!isVertical && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
65
77
|
e.preventDefault();
|
|
66
78
|
setActive(cards[Math.max(0, idx - 1)]);
|
|
67
|
-
} else if (e.key === 'ArrowRight') {
|
|
79
|
+
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
68
80
|
e.preventDefault();
|
|
69
81
|
setActive(cards[Math.min(cards.length - 1, idx + 1)]);
|
|
70
82
|
} else if (e.key === 'Home') {
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Popover Component
|
|
3
|
+
*
|
|
4
|
+
* Multi-trigger popover (click | hover | focus) with auto-placement flip.
|
|
5
|
+
* Distinct from .vd-bubble (which is click-only rich HTML); popover is the
|
|
6
|
+
* general-purpose primitive documented in `sections/components/popover.html`.
|
|
7
|
+
*
|
|
8
|
+
* Markup:
|
|
9
|
+
* <button class="vd-popover-trigger"
|
|
10
|
+
* data-vd-popover-target="#my-panel"
|
|
11
|
+
* data-vd-popover-placement="top|bottom|left|right"
|
|
12
|
+
* data-vd-popover-trigger="click|hover|focus"
|
|
13
|
+
* aria-haspopup="dialog">…</button>
|
|
14
|
+
* <div id="my-panel" class="vd-popover-panel" role="dialog" hidden>…</div>
|
|
15
|
+
*
|
|
16
|
+
* Public API (window.VanduoPopover):
|
|
17
|
+
* init(root) Scan root for triggers, wire instances. Idempotent.
|
|
18
|
+
* destroy(trigger) Tear down one instance.
|
|
19
|
+
* destroyAll(root) Tear down every instance inside root.
|
|
20
|
+
* show(trigger) Open one trigger's panel.
|
|
21
|
+
* hide(trigger) Close one trigger's panel.
|
|
22
|
+
* flipPlacement(trigger) Recompute placement; flips when overflow detected.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
(function () {
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const PLACEMENTS = ['top', 'bottom', 'left', 'right'];
|
|
29
|
+
const TRIGGERS = ['click', 'hover', 'focus'];
|
|
30
|
+
const DEFAULT_GAP = 8;
|
|
31
|
+
|
|
32
|
+
function resolvePlacement(preferred) {
|
|
33
|
+
return PLACEMENTS.indexOf(preferred) !== -1 ? preferred : 'bottom';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveTrigger(value) {
|
|
37
|
+
if (!value) return ['click', 'focus'];
|
|
38
|
+
return value
|
|
39
|
+
.split(/\s+/)
|
|
40
|
+
.filter(function (t) { return TRIGGERS.indexOf(t) !== -1; });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findPanel(trigger) {
|
|
44
|
+
const targetSelector = trigger.getAttribute('data-vd-popover-target');
|
|
45
|
+
if (!targetSelector) return null;
|
|
46
|
+
const doc = trigger.ownerDocument || document;
|
|
47
|
+
const panel = doc.querySelector(targetSelector);
|
|
48
|
+
if (!panel) return null;
|
|
49
|
+
if (!panel.classList.contains('vd-popover-panel')) {
|
|
50
|
+
panel.classList.add('vd-popover-panel');
|
|
51
|
+
}
|
|
52
|
+
return panel;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const Popover = {
|
|
56
|
+
instances: new Map(),
|
|
57
|
+
_globalCleanups: [],
|
|
58
|
+
|
|
59
|
+
init: function (root) {
|
|
60
|
+
const scope = root || document;
|
|
61
|
+
const triggers = (scope.querySelectorAll
|
|
62
|
+
? scope.querySelectorAll('.vd-popover-trigger')
|
|
63
|
+
: document.querySelectorAll('.vd-popover-trigger'));
|
|
64
|
+
|
|
65
|
+
Array.prototype.forEach.call(triggers, function (trigger) {
|
|
66
|
+
if (Popover.instances.has(trigger)) return;
|
|
67
|
+
Popover.initInstance(trigger);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (Popover._globalCleanups.length === 0) {
|
|
71
|
+
const outsideClick = function (event) {
|
|
72
|
+
Popover.instances.forEach(function (inst, trigger) {
|
|
73
|
+
if (inst.trigger !== 'click') return;
|
|
74
|
+
if (inst.panel.contains(event.target) || trigger.contains(event.target)) return;
|
|
75
|
+
Popover.hide(trigger);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
const escHandler = function (event) {
|
|
79
|
+
if (event.key !== 'Escape') return;
|
|
80
|
+
let lastOpen = null;
|
|
81
|
+
Popover.instances.forEach(function (inst, trigger) {
|
|
82
|
+
if (inst.panel.hasAttribute('hidden') === false) lastOpen = trigger;
|
|
83
|
+
});
|
|
84
|
+
if (lastOpen) Popover.hide(lastOpen);
|
|
85
|
+
};
|
|
86
|
+
document.addEventListener('click', outsideClick, true);
|
|
87
|
+
document.addEventListener('keydown', escHandler);
|
|
88
|
+
Popover._globalCleanups.push(function () {
|
|
89
|
+
document.removeEventListener('click', outsideClick, true);
|
|
90
|
+
});
|
|
91
|
+
Popover._globalCleanups.push(function () {
|
|
92
|
+
document.removeEventListener('keydown', escHandler);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
initInstance: function (trigger) {
|
|
98
|
+
const panel = findPanel(trigger);
|
|
99
|
+
if (!panel) return;
|
|
100
|
+
const cleanup = [];
|
|
101
|
+
const placement = resolvePlacement(trigger.getAttribute('data-vd-popover-placement'));
|
|
102
|
+
const triggers = resolveTrigger(trigger.getAttribute('data-vd-popover-trigger'));
|
|
103
|
+
const allowFlip = trigger.getAttribute('data-vd-popover-flip') !== 'false';
|
|
104
|
+
|
|
105
|
+
if (!panel.id) {
|
|
106
|
+
panel.id = 'vd-popover-' + Math.random().toString(36).slice(2, 9);
|
|
107
|
+
}
|
|
108
|
+
if (!panel.hasAttribute('role')) panel.setAttribute('role', 'dialog');
|
|
109
|
+
if (!panel.hasAttribute('aria-modal')) panel.setAttribute('aria-modal', 'false');
|
|
110
|
+
|
|
111
|
+
trigger.setAttribute('aria-haspopup', 'dialog');
|
|
112
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
113
|
+
trigger.setAttribute('aria-controls', panel.id);
|
|
114
|
+
|
|
115
|
+
if (triggers.indexOf('click') !== -1) {
|
|
116
|
+
const clickHandler = function (event) {
|
|
117
|
+
event.stopPropagation();
|
|
118
|
+
const expanded = trigger.getAttribute('aria-expanded') === 'true';
|
|
119
|
+
if (expanded) Popover.hide(trigger);
|
|
120
|
+
else {
|
|
121
|
+
Popover._closeOthers(trigger);
|
|
122
|
+
Popover.show(trigger);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
trigger.addEventListener('click', clickHandler);
|
|
126
|
+
cleanup.push(function () { trigger.removeEventListener('click', clickHandler); });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (triggers.indexOf('hover') !== -1) {
|
|
130
|
+
const enterHandler = function () { Popover.show(trigger); };
|
|
131
|
+
const leaveHandler = function () {
|
|
132
|
+
setTimeout(function () {
|
|
133
|
+
if (!panel.matches(':hover') && !trigger.matches(':hover')) Popover.hide(trigger);
|
|
134
|
+
}, 80);
|
|
135
|
+
};
|
|
136
|
+
trigger.addEventListener('mouseenter', enterHandler);
|
|
137
|
+
trigger.addEventListener('mouseleave', leaveHandler);
|
|
138
|
+
panel.addEventListener('mouseenter', enterHandler);
|
|
139
|
+
panel.addEventListener('mouseleave', leaveHandler);
|
|
140
|
+
cleanup.push(function () { trigger.removeEventListener('mouseenter', enterHandler); });
|
|
141
|
+
cleanup.push(function () { trigger.removeEventListener('mouseleave', leaveHandler); });
|
|
142
|
+
cleanup.push(function () { panel.removeEventListener('mouseenter', enterHandler); });
|
|
143
|
+
cleanup.push(function () { panel.removeEventListener('mouseleave', leaveHandler); });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (triggers.indexOf('focus') !== -1) {
|
|
147
|
+
const focusHandler = function () { Popover.show(trigger); };
|
|
148
|
+
const blurHandler = function (event) {
|
|
149
|
+
if (panel.contains(event.relatedTarget)) return;
|
|
150
|
+
Popover.hide(trigger);
|
|
151
|
+
};
|
|
152
|
+
trigger.addEventListener('focus', focusHandler);
|
|
153
|
+
trigger.addEventListener('blur', blurHandler);
|
|
154
|
+
cleanup.push(function () { trigger.removeEventListener('focus', focusHandler); });
|
|
155
|
+
cleanup.push(function () { trigger.removeEventListener('blur', blurHandler); });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const resizeHandler = function () { Popover.flipPlacement(trigger); };
|
|
159
|
+
window.addEventListener('resize', resizeHandler);
|
|
160
|
+
window.addEventListener('scroll', resizeHandler, true);
|
|
161
|
+
cleanup.push(function () { window.removeEventListener('resize', resizeHandler); });
|
|
162
|
+
cleanup.push(function () { window.removeEventListener('scroll', resizeHandler, true); });
|
|
163
|
+
|
|
164
|
+
Popover.instances.set(trigger, {
|
|
165
|
+
panel: panel,
|
|
166
|
+
cleanup: cleanup,
|
|
167
|
+
placement: placement,
|
|
168
|
+
trigger: triggers.join(' '),
|
|
169
|
+
allowFlip: allowFlip
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
show: function (trigger) {
|
|
174
|
+
const inst = Popover.instances.get(trigger);
|
|
175
|
+
if (!inst) return;
|
|
176
|
+
inst.panel.hidden = false;
|
|
177
|
+
// Two rAFs: first lets layout settle, second positions after styles applied.
|
|
178
|
+
requestAnimationFrame(function () {
|
|
179
|
+
Popover.position(trigger, inst.panel, inst.placement);
|
|
180
|
+
trigger.setAttribute('aria-expanded', 'true');
|
|
181
|
+
inst.panel.setAttribute('data-placement', inst.placement);
|
|
182
|
+
trigger.dispatchEvent(new CustomEvent('popover:show', {
|
|
183
|
+
bubbles: true,
|
|
184
|
+
detail: { trigger: trigger, placement: inst.placement }
|
|
185
|
+
}));
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
hide: function (trigger) {
|
|
190
|
+
const inst = Popover.instances.get(trigger);
|
|
191
|
+
if (!inst) return;
|
|
192
|
+
inst.panel.hidden = true;
|
|
193
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
194
|
+
trigger.dispatchEvent(new CustomEvent('popover:hide', {
|
|
195
|
+
bubbles: true,
|
|
196
|
+
detail: { trigger: trigger }
|
|
197
|
+
}));
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
_closeOthers: function (currentTrigger) {
|
|
201
|
+
Popover.instances.forEach(function (_inst, trigger) {
|
|
202
|
+
if (trigger !== currentTrigger) Popover.hide(trigger);
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
position: function (trigger, panel, placement) {
|
|
207
|
+
const rect = trigger.getBoundingClientRect();
|
|
208
|
+
const popRect = panel.getBoundingClientRect();
|
|
209
|
+
const gap = DEFAULT_GAP;
|
|
210
|
+
let top;
|
|
211
|
+
let left;
|
|
212
|
+
const win = panel.ownerDocument.defaultView || window;
|
|
213
|
+
const scrollX = win.pageXOffset || 0;
|
|
214
|
+
const scrollY = win.pageYOffset || 0;
|
|
215
|
+
|
|
216
|
+
switch (placement) {
|
|
217
|
+
case 'top':
|
|
218
|
+
top = rect.top - popRect.height - gap + scrollY;
|
|
219
|
+
left = rect.left + (rect.width - popRect.width) / 2 + scrollX;
|
|
220
|
+
break;
|
|
221
|
+
case 'left':
|
|
222
|
+
top = rect.top + (rect.height - popRect.height) / 2 + scrollY;
|
|
223
|
+
left = rect.left - popRect.width - gap + scrollX;
|
|
224
|
+
break;
|
|
225
|
+
case 'right':
|
|
226
|
+
top = rect.top + (rect.height - popRect.height) / 2 + scrollY;
|
|
227
|
+
left = rect.right + gap + scrollX;
|
|
228
|
+
break;
|
|
229
|
+
default: // bottom
|
|
230
|
+
top = rect.bottom + gap + scrollY;
|
|
231
|
+
left = rect.left + (rect.width - popRect.width) / 2 + scrollX;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
left = Math.max(8, Math.min(left, win.innerWidth - popRect.width - 8));
|
|
235
|
+
top = Math.max(8, top);
|
|
236
|
+
|
|
237
|
+
panel.style.position = 'absolute';
|
|
238
|
+
panel.style.top = top + 'px';
|
|
239
|
+
panel.style.left = left + 'px';
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
flipPlacement: function (trigger) {
|
|
243
|
+
const inst = Popover.instances.get(trigger);
|
|
244
|
+
if (!inst || !inst.allowFlip) return;
|
|
245
|
+
if (inst.panel.hidden) return;
|
|
246
|
+
const win = inst.panel.ownerDocument.defaultView || window;
|
|
247
|
+
const rect = trigger.getBoundingClientRect();
|
|
248
|
+
const popRect = inst.panel.getBoundingClientRect();
|
|
249
|
+
const gap = DEFAULT_GAP;
|
|
250
|
+
let flipped = null;
|
|
251
|
+
const current = inst.placement;
|
|
252
|
+
|
|
253
|
+
if (current === 'top' && rect.top - popRect.height - gap < 0) flipped = 'bottom';
|
|
254
|
+
else if (current === 'bottom' && rect.bottom + popRect.height + gap > win.innerHeight) flipped = 'top';
|
|
255
|
+
else if (current === 'left' && rect.left - popRect.width - gap < 0) flipped = 'right';
|
|
256
|
+
else if (current === 'right' && rect.right + popRect.width + gap > win.innerWidth) flipped = 'left';
|
|
257
|
+
|
|
258
|
+
if (flipped) {
|
|
259
|
+
inst.placement = flipped;
|
|
260
|
+
Popover.position(trigger, inst.panel, flipped);
|
|
261
|
+
inst.panel.setAttribute('data-placement', flipped);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
destroy: function (trigger) {
|
|
266
|
+
const inst = Popover.instances.get(trigger);
|
|
267
|
+
if (!inst) return;
|
|
268
|
+
inst.cleanup.forEach(function (fn) { fn(); });
|
|
269
|
+
Popover.hide(trigger);
|
|
270
|
+
trigger.removeAttribute('aria-haspopup');
|
|
271
|
+
trigger.removeAttribute('aria-expanded');
|
|
272
|
+
trigger.removeAttribute('aria-controls');
|
|
273
|
+
Popover.instances.delete(trigger);
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
destroyAll: function (root) {
|
|
277
|
+
const triggers = [];
|
|
278
|
+
Popover.instances.forEach(function (_inst, trigger) {
|
|
279
|
+
if (!root || (trigger.closest && trigger.closest(root))) triggers.push(trigger);
|
|
280
|
+
});
|
|
281
|
+
triggers.forEach(function (trigger) { Popover.destroy(trigger); });
|
|
282
|
+
if (Popover.instances.size === 0) {
|
|
283
|
+
Popover._globalCleanups.forEach(function (fn) { fn(); });
|
|
284
|
+
Popover._globalCleanups = [];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
if (typeof window !== 'undefined') {
|
|
290
|
+
if (typeof window.Vanduo !== 'undefined' && typeof window.Vanduo.register === 'function') {
|
|
291
|
+
window.Vanduo.register('popover', Popover);
|
|
292
|
+
}
|
|
293
|
+
window.VanduoPopover = Popover;
|
|
294
|
+
}
|
|
295
|
+
})();
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Search Helper
|
|
3
|
+
*
|
|
4
|
+
* Small registry that lets consumers plug named data sources into a search
|
|
5
|
+
* overlay. The framework does not ship a UI — overlays like
|
|
6
|
+
* `GlobalSearchModal` consume the registry.
|
|
7
|
+
*
|
|
8
|
+
* Source shape:
|
|
9
|
+
* {
|
|
10
|
+
* name: 'sections', // unique key
|
|
11
|
+
* label: 'Documentation Sections', // display name in groups
|
|
12
|
+
* icon: 'ph-book-open', // optional Phosphor class
|
|
13
|
+
* limit: 10, // optional cap (default 10)
|
|
14
|
+
* fetch: async (query, signal) => [...] // returns Result[]
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* Result shape:
|
|
18
|
+
* { title, subtitle?, href, group?, icon? }
|
|
19
|
+
*
|
|
20
|
+
* Public API (window.VanduoSearch):
|
|
21
|
+
* register(source) Add a source. Throws on duplicate name.
|
|
22
|
+
* unregister(name) Remove a source. Returns boolean.
|
|
23
|
+
* list() Return registered sources (frozen array).
|
|
24
|
+
* query(text, options?) Search every source in parallel.
|
|
25
|
+
* options: { signal?, limitPerSource? }
|
|
26
|
+
* Returns { sources: { name, results, error? }[] }
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
(function () {
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
const DEFAULT_LIMIT = 10;
|
|
33
|
+
|
|
34
|
+
const sources = new Map();
|
|
35
|
+
|
|
36
|
+
function register(source) {
|
|
37
|
+
if (!source || typeof source.name !== 'string' || source.name.length === 0) {
|
|
38
|
+
throw new Error('VanduoSearch.register: source.name is required');
|
|
39
|
+
}
|
|
40
|
+
if (typeof source.fetch !== 'function') {
|
|
41
|
+
throw new Error('VanduoSearch.register: source.fetch must be a function');
|
|
42
|
+
}
|
|
43
|
+
if (sources.has(source.name)) {
|
|
44
|
+
throw new Error('VanduoSearch.register: source "' + source.name + '" already registered');
|
|
45
|
+
}
|
|
46
|
+
sources.set(source.name, Object.freeze({
|
|
47
|
+
name: source.name,
|
|
48
|
+
label: source.label || source.name,
|
|
49
|
+
icon: source.icon || null,
|
|
50
|
+
limit: typeof source.limit === 'number' ? source.limit : DEFAULT_LIMIT,
|
|
51
|
+
fetch: source.fetch
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function unregister(name) {
|
|
56
|
+
return sources.delete(name);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function list() {
|
|
60
|
+
return Object.freeze(Array.from(sources.values()));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function query(text, options) {
|
|
64
|
+
options = options || {};
|
|
65
|
+
const signal = options.signal;
|
|
66
|
+
const limitPerSource = typeof options.limitPerSource === 'number'
|
|
67
|
+
? options.limitPerSource
|
|
68
|
+
: null;
|
|
69
|
+
const queryText = (text || '').trim();
|
|
70
|
+
const allSources = Array.from(sources.values());
|
|
71
|
+
|
|
72
|
+
if (queryText.length === 0) {
|
|
73
|
+
return Promise.resolve({
|
|
74
|
+
text: queryText,
|
|
75
|
+
sources: allSources.map(function (src) {
|
|
76
|
+
return { name: src.name, label: src.label, results: [] };
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const promises = allSources.map(function (src) {
|
|
82
|
+
const effectiveLimit = limitPerSource != null ? limitPerSource : src.limit;
|
|
83
|
+
return Promise.resolve()
|
|
84
|
+
.then(function () { return src.fetch(queryText, { signal: signal, limit: effectiveLimit }); })
|
|
85
|
+
.then(function (results) {
|
|
86
|
+
const safe = Array.isArray(results) ? results : [];
|
|
87
|
+
return { name: src.name, label: src.label, results: safe };
|
|
88
|
+
})
|
|
89
|
+
.catch(function (error) {
|
|
90
|
+
if (error && error.name === 'AbortError') throw error;
|
|
91
|
+
return { name: src.name, label: src.label, results: [], error: error.message || 'fetch failed' };
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return Promise.all(promises).then(function (perSource) {
|
|
96
|
+
return { text: queryText, sources: perSource };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const Search = { register: register, unregister: unregister, list: list, query: query };
|
|
101
|
+
|
|
102
|
+
if (typeof window !== 'undefined') {
|
|
103
|
+
if (typeof window.Vanduo !== 'undefined' && typeof window.Vanduo.register === 'function') {
|
|
104
|
+
window.Vanduo.register('search', Search);
|
|
105
|
+
}
|
|
106
|
+
window.VanduoSearch = Search;
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
const ThemeCustomizer = {
|
|
11
11
|
// Storage keys
|
|
12
12
|
STORAGE_KEYS: {
|
|
13
|
+
PALETTE: 'vanduo-palette',
|
|
13
14
|
PRIMARY: 'vanduo-primary-color',
|
|
14
15
|
NEUTRAL: 'vanduo-neutral-color',
|
|
15
16
|
RADIUS: 'vanduo-radius',
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
|
|
20
21
|
// Default values
|
|
21
22
|
DEFAULTS: {
|
|
23
|
+
PALETTE: 'open-color',
|
|
22
24
|
PRIMARY_LIGHT: 'black',
|
|
23
25
|
PRIMARY_DARK: 'amber',
|
|
24
26
|
NEUTRAL: 'charcoal',
|
|
@@ -27,7 +29,13 @@
|
|
|
27
29
|
THEME: 'system'
|
|
28
30
|
},
|
|
29
31
|
|
|
30
|
-
//
|
|
32
|
+
// Palette options (the Open Color default + the optional Fibonacci palette)
|
|
33
|
+
PALETTE_OPTIONS: {
|
|
34
|
+
'open-color': { name: 'Open Color', description: 'The Open Color palette (MIT) — the Vanduo default.' },
|
|
35
|
+
'fibonacci': { name: 'Fibonacci', description: 'Golden-angle generated palette — optional, opt in via data-palette="fibonacci".' }
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Primary color definitions (palette-aware: shown over the active palette)
|
|
31
39
|
PRIMARY_COLORS: {
|
|
32
40
|
'black': { name: 'Black', color: '#000000' },
|
|
33
41
|
'red': { name: 'Red', color: '#fa5252' },
|
|
@@ -76,6 +84,7 @@
|
|
|
76
84
|
|
|
77
85
|
// State
|
|
78
86
|
state: {
|
|
87
|
+
palette: null,
|
|
79
88
|
primary: null,
|
|
80
89
|
neutral: null,
|
|
81
90
|
radius: null,
|
|
@@ -194,6 +203,7 @@
|
|
|
194
203
|
* Load preferences from localStorage
|
|
195
204
|
*/
|
|
196
205
|
loadPreferences: function () {
|
|
206
|
+
this.state.palette = this.getStorageValue(this.STORAGE_KEYS.PALETTE, this.DEFAULTS.PALETTE);
|
|
197
207
|
this.state.theme = this.getStorageValue(this.STORAGE_KEYS.THEME, this.DEFAULTS.THEME);
|
|
198
208
|
this.state.primary = this.getStorageValue(this.STORAGE_KEYS.PRIMARY, this.getDefaultPrimary(this.state.theme));
|
|
199
209
|
this._normalizeDefaultPrimaryIfStaleWithStoredTheme();
|
|
@@ -213,6 +223,7 @@
|
|
|
213
223
|
* Apply all preferences
|
|
214
224
|
*/
|
|
215
225
|
applyAllPreferences: function () {
|
|
226
|
+
this.applyPalette(this.state.palette);
|
|
216
227
|
this.applyPrimary(this.state.primary);
|
|
217
228
|
this.applyNeutral(this.state.neutral);
|
|
218
229
|
this.applyRadius(this.state.radius);
|
|
@@ -220,6 +231,21 @@
|
|
|
220
231
|
this.applyTheme(this.state.theme);
|
|
221
232
|
},
|
|
222
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Apply color palette (Fibonacci default / Open Color optional)
|
|
236
|
+
*/
|
|
237
|
+
applyPalette: function (paletteKey) {
|
|
238
|
+
if (!this.PALETTE_OPTIONS[paletteKey]) {
|
|
239
|
+
paletteKey = this.DEFAULTS.PALETTE;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.state.palette = paletteKey;
|
|
243
|
+
document.documentElement.setAttribute('data-palette', paletteKey);
|
|
244
|
+
this.savePreference(this.STORAGE_KEYS.PALETTE, paletteKey);
|
|
245
|
+
|
|
246
|
+
this.dispatchEvent('palette-change', { palette: paletteKey });
|
|
247
|
+
},
|
|
248
|
+
|
|
223
249
|
/**
|
|
224
250
|
* Apply primary color
|
|
225
251
|
*/
|
|
@@ -492,6 +518,14 @@
|
|
|
492
518
|
|
|
493
519
|
this.elements.panel.setAttribute('data-customizer-initialized', 'true');
|
|
494
520
|
|
|
521
|
+
// Palette buttons
|
|
522
|
+
this.elements.panel.querySelectorAll('[data-palette]').forEach(btn => {
|
|
523
|
+
this.addListener(btn, 'click', () => {
|
|
524
|
+
this.applyPalette(btn.dataset.palette);
|
|
525
|
+
this.updateUI();
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
495
529
|
// Primary color swatches
|
|
496
530
|
this.elements.panel.querySelectorAll('[data-color]').forEach(swatch => {
|
|
497
531
|
this.addListener(swatch, 'click', () => {
|
|
@@ -569,6 +603,12 @@
|
|
|
569
603
|
return '#000000';
|
|
570
604
|
};
|
|
571
605
|
|
|
606
|
+
// Generate palette options
|
|
607
|
+
let paletteOptions = '';
|
|
608
|
+
for (const [key, value] of Object.entries(this.PALETTE_OPTIONS)) {
|
|
609
|
+
paletteOptions += `<button class="tc-palette-btn${key === this.state.palette ? ' is-active' : ''}" data-palette="${esc(key)}" title="${esc(value.description)}">${esc(value.name)}</button>`;
|
|
610
|
+
}
|
|
611
|
+
|
|
572
612
|
// Generate primary color swatches
|
|
573
613
|
let primarySwatches = '';
|
|
574
614
|
for (const [key, value] of Object.entries(this.PRIMARY_COLORS)) {
|
|
@@ -602,6 +642,12 @@
|
|
|
602
642
|
</div>
|
|
603
643
|
<div class="tc-body">
|
|
604
644
|
|
|
645
|
+
<div class="tc-section">
|
|
646
|
+
<label class="tc-label">Palette</label>
|
|
647
|
+
<div class="tc-palette-group">
|
|
648
|
+
${paletteOptions}
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
605
651
|
<div class="tc-section">
|
|
606
652
|
<label class="tc-label">Primary Color</label>
|
|
607
653
|
<div class="tc-color-grid">
|
|
@@ -798,6 +844,11 @@
|
|
|
798
844
|
updateUI: function () {
|
|
799
845
|
if (!this.elements.panel) return;
|
|
800
846
|
|
|
847
|
+
// Update palette buttons
|
|
848
|
+
this.elements.panel.querySelectorAll('[data-palette]').forEach(btn => {
|
|
849
|
+
btn.classList.toggle('is-active', btn.dataset.palette === this.state.palette);
|
|
850
|
+
});
|
|
851
|
+
|
|
801
852
|
// Update primary color swatches
|
|
802
853
|
this.elements.panel.querySelectorAll('[data-color]').forEach(swatch => {
|
|
803
854
|
swatch.classList.toggle('is-active', swatch.dataset.color === this.state.primary);
|
|
@@ -825,6 +876,7 @@
|
|
|
825
876
|
* Reset all preferences to defaults
|
|
826
877
|
*/
|
|
827
878
|
reset: function () {
|
|
879
|
+
this.applyPalette(this.DEFAULTS.PALETTE);
|
|
828
880
|
this.applyTheme(this.DEFAULTS.THEME);
|
|
829
881
|
this.applyPrimary(this.getDefaultPrimary(this.DEFAULTS.THEME));
|
|
830
882
|
this.applyNeutral(this.DEFAULTS.NEUTRAL);
|
|
@@ -846,6 +898,7 @@
|
|
|
846
898
|
* Programmatically set preferences
|
|
847
899
|
*/
|
|
848
900
|
setPreferences: function (prefs) {
|
|
901
|
+
if (prefs.palette) this.applyPalette(prefs.palette);
|
|
849
902
|
if (prefs.primary) this.applyPrimary(prefs.primary);
|
|
850
903
|
if (prefs.neutral) this.applyNeutral(prefs.neutral);
|
|
851
904
|
if (prefs.radius) this.applyRadius(prefs.radius);
|
package/js/index.js
CHANGED
|
@@ -68,6 +68,8 @@ import './components/rating.js';
|
|
|
68
68
|
import './components/transfer.js';
|
|
69
69
|
import './components/tree.js';
|
|
70
70
|
import './components/spotlight.js';
|
|
71
|
+
import './components/popover.js';
|
|
72
|
+
import './components/search.js';
|
|
71
73
|
|
|
72
74
|
// Re-export for ESM / CJS consumers
|
|
73
75
|
const Vanduo = window.Vanduo;
|
package/js/utils/helpers.js
CHANGED
|
@@ -248,9 +248,14 @@ function getPosition(element) {
|
|
|
248
248
|
*/
|
|
249
249
|
function escapeHtml(str) {
|
|
250
250
|
if (!str) return '';
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return
|
|
251
|
+
// Escape quotes too, so output is safe in attribute context (e.g.
|
|
252
|
+
// title="${escapeHtml(x)}"), not just element-text context.
|
|
253
|
+
return String(str)
|
|
254
|
+
.replace(/&/g, '&')
|
|
255
|
+
.replace(/</g, '<')
|
|
256
|
+
.replace(/>/g, '>')
|
|
257
|
+
.replace(/"/g, '"')
|
|
258
|
+
.replace(/'/g, ''');
|
|
254
259
|
}
|
|
255
260
|
|
|
256
261
|
/**
|
|
@@ -271,7 +276,10 @@ function sanitizeHtml(input, options = {}) {
|
|
|
271
276
|
return escapeHtml(input);
|
|
272
277
|
}
|
|
273
278
|
const allowSvg = options && options.allowSvg === true;
|
|
274
|
-
|
|
279
|
+
// Secure default: deny inline `style` unless explicitly opted in. (Inline
|
|
280
|
+
// style can't run JS but enables CSS exfiltration/clickjacking on untrusted
|
|
281
|
+
// HTML.) Callers that need rich styling pass { allowStyle: true }.
|
|
282
|
+
const allowStyle = options && options.allowStyle === true;
|
|
275
283
|
const baseAllowed = ['B', 'STRONG', 'I', 'EM', 'BR', 'A', 'SPAN', 'U', 'DIV', 'P', 'KBD', 'CODE', 'SMALL', 'MARK'];
|
|
276
284
|
const svgAllowed = ['SVG', 'PATH', 'LINE', 'CIRCLE', 'POLYLINE', 'RECT', 'G'];
|
|
277
285
|
const allowed = allowSvg ? baseAllowed.concat(svgAllowed) : baseAllowed;
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanduo-oss/framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
4
7
|
"description": "Zero-dependency CSS/JS framework built on Fibonacci/Golden Ratio design system with Open Color integration",
|
|
5
8
|
"keywords": [
|
|
6
9
|
"css",
|
|
@@ -44,12 +47,12 @@
|
|
|
44
47
|
],
|
|
45
48
|
"devDependencies": {
|
|
46
49
|
"@eslint/js": "^10.0.1",
|
|
47
|
-
"@playwright/test": "^1.
|
|
50
|
+
"@playwright/test": "^1.61.1",
|
|
48
51
|
"esbuild": "^0.28.1",
|
|
49
|
-
"eslint": "^10.
|
|
52
|
+
"eslint": "^10.5.0",
|
|
50
53
|
"husky": "^9.1.7",
|
|
51
54
|
"lightningcss": "^1.32.0",
|
|
52
|
-
"stylelint": "^17.
|
|
55
|
+
"stylelint": "^17.13.0",
|
|
53
56
|
"stylelint-config-standard": "^40.0.0"
|
|
54
57
|
},
|
|
55
58
|
"engines": {
|