@birdapi/velinstyle 0.9.0 → 1.1.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/README.de.md +15 -11
- package/README.md +15 -11
- package/cli/cli-manifest.json +1 -1
- package/cli/docgen/extract-a11y.js +4 -2
- package/cli/index.js +4 -2
- package/components/index.js +2 -0
- package/components/runtime/component-loaders.js +2 -0
- package/components/velin-data-table.js +361 -0
- package/components/velin-form-summary.js +348 -0
- package/core/a11y/component-contracts.json +18 -1
- package/core/highlight/languages/go.js +28 -0
- package/core/highlight/languages/python.js +29 -0
- package/core/highlight/languages/rust.js +32 -0
- package/core/highlight/languages/yaml.js +23 -0
- package/core/highlight/registry.js +13 -2
- package/core/meta/build.js +1 -1
- package/core/search/worker-client.js +2 -3
- package/dist/chunks/attributes-353XDOAX.js +391 -0
- package/dist/chunks/attributes-ADOKLKA7.js +391 -0
- package/dist/chunks/chunk-ERF5YVP4.js +253 -0
- package/dist/chunks/chunk-IEHKRARD.js +109 -0
- package/dist/chunks/chunk-QVHYL3R4.js +111 -0
- package/dist/chunks/go-HZ6XTFCK.js +31 -0
- package/dist/chunks/highlight-IBDX4XWS.js +37 -0
- package/dist/chunks/python-BMPKDKNO.js +32 -0
- package/dist/chunks/runtime-entry.js +1 -1
- package/dist/chunks/rust-2BWKI2MN.js +35 -0
- package/dist/chunks/velin-code-block-ITCLIC6T.js +132 -0
- package/dist/chunks/velin-data-table-KK7IDGTP.js +302 -0
- package/dist/chunks/velin-form-summary-XRQ7COQH.js +273 -0
- package/dist/chunks/velin-lightbox-NXA3U2FM.js +149 -0
- package/dist/chunks/velin-search-FDHEMCSO.js +557 -0
- package/dist/chunks/worker-client-MPT7PNQ4.js +47 -0
- package/dist/chunks/yaml-O67LEQYT.js +26 -0
- package/dist/llms.txt +2 -2
- package/dist/search-index.json +28 -2
- package/dist/velin-agent.json +31 -5
- package/dist/velinstyle-components.iife.js +745 -7
- package/dist/velinstyle-components.js +746 -5
- package/dist/velinstyle-components.min.js +85 -85
- package/dist/velinstyle.css +141 -19
- package/dist/velinstyle.d.ts +2 -0
- package/dist/velinstyle.min.css +1 -1
- package/package.json +8 -2
- package/src/base/wc-placeholder.css +7 -0
- package/src/components/data-table.css +93 -0
- package/src/components/form-validation.css +40 -0
- package/src/velinstyle.css +1 -0
- package/dist/llms.test.txt +0 -40
- package/dist/search-index.test.json +0 -1773
- package/dist/velin-agent.test.json +0 -684
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearBackgroundInert,
|
|
3
|
+
setBackgroundInert,
|
|
4
|
+
trapFocus
|
|
5
|
+
} from "./chunk-42VVPW6N.js";
|
|
6
|
+
import {
|
|
7
|
+
announce
|
|
8
|
+
} from "./chunk-Z4JYQ2XX.js";
|
|
9
|
+
import {
|
|
10
|
+
escapeHTML,
|
|
11
|
+
sanitizeURL
|
|
12
|
+
} from "./chunk-RDE3QE7Z.js";
|
|
13
|
+
|
|
14
|
+
// components/velin-lightbox.js
|
|
15
|
+
var styles = `
|
|
16
|
+
:host { display: contents; }
|
|
17
|
+
.overlay {
|
|
18
|
+
position: fixed; inset: 0; z-index: var(--velin-z-modal, 500);
|
|
19
|
+
display: flex; align-items: center; justify-content: center;
|
|
20
|
+
background: rgba(0,0,0,0.85); opacity: 0; visibility: hidden;
|
|
21
|
+
transition: opacity 200ms ease, visibility 200ms ease;
|
|
22
|
+
}
|
|
23
|
+
:host([open]) .overlay { opacity: 1; visibility: visible; }
|
|
24
|
+
.content {
|
|
25
|
+
position: relative; max-inline-size: 90vw; max-block-size: 90vh;
|
|
26
|
+
}
|
|
27
|
+
.content img, .content video {
|
|
28
|
+
max-inline-size: 100%; max-block-size: 85vh; object-fit: contain;
|
|
29
|
+
border-radius: var(--velin-radius-md, 0.375rem);
|
|
30
|
+
}
|
|
31
|
+
.close {
|
|
32
|
+
position: absolute; top: -2.5rem; right: 0;
|
|
33
|
+
background: none; border: none; color: #fff; font-size: 1.5rem;
|
|
34
|
+
cursor: pointer; min-width: 2.75rem; min-height: 2.75rem;
|
|
35
|
+
display: flex; align-items: center; justify-content: center;
|
|
36
|
+
}
|
|
37
|
+
.close:focus-visible { outline: 3px solid var(--velin-color-focus, #2563eb); outline-offset: 2px; }
|
|
38
|
+
.nav {
|
|
39
|
+
position: absolute; top: 50%; transform: translateY(-50%);
|
|
40
|
+
background: rgba(255,255,255,0.15); border: none; color: #fff;
|
|
41
|
+
font-size: 1.5rem; cursor: pointer; border-radius: 50%;
|
|
42
|
+
min-width: 2.75rem; min-height: 2.75rem;
|
|
43
|
+
display: flex; align-items: center; justify-content: center;
|
|
44
|
+
}
|
|
45
|
+
.nav:focus-visible { outline: 3px solid var(--velin-color-focus, #2563eb); outline-offset: 2px; }
|
|
46
|
+
.nav--prev { left: -3.5rem; }
|
|
47
|
+
.nav--next { right: -3.5rem; }
|
|
48
|
+
.counter { position: absolute; bottom: -2rem; left: 50%; transform: translateX(-50%); color: #ccc; font-size: 0.875rem; }
|
|
49
|
+
@media (prefers-reduced-motion: reduce) { .overlay { transition: none; } }
|
|
50
|
+
@media (max-width: 48rem) {
|
|
51
|
+
.nav--prev { left: 0.5rem; }
|
|
52
|
+
.nav--next { right: 0.5rem; }
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
var VelinLightbox = class extends HTMLElement {
|
|
56
|
+
constructor() {
|
|
57
|
+
super();
|
|
58
|
+
this.attachShadow({ mode: "open" });
|
|
59
|
+
this._index = 0;
|
|
60
|
+
this._items = [];
|
|
61
|
+
this._previousFocus = null;
|
|
62
|
+
this._onTrapKey = (e) => {
|
|
63
|
+
if (e.key === "Tab") trapFocus(this.shadowRoot, e);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
connectedCallback() {
|
|
67
|
+
this.shadowRoot.innerHTML = `
|
|
68
|
+
<style>${styles}</style>
|
|
69
|
+
<slot></slot>
|
|
70
|
+
<div class="overlay" role="dialog" aria-modal="true" aria-label="Image lightbox" aria-roledescription="lightbox" part="overlay">
|
|
71
|
+
<button class="nav nav--prev" aria-label="Previous">‹</button>
|
|
72
|
+
<div class="content" part="content"></div>
|
|
73
|
+
<button class="nav nav--next" aria-label="Next">›</button>
|
|
74
|
+
<button class="close" aria-label="Close">×</button>
|
|
75
|
+
<div class="counter" part="counter" aria-live="polite" aria-atomic="true"></div>
|
|
76
|
+
</div>
|
|
77
|
+
`;
|
|
78
|
+
const slot = this.shadowRoot.querySelector("slot");
|
|
79
|
+
slot.addEventListener("slotchange", () => {
|
|
80
|
+
this._items = slot.assignedElements().filter((el) => el.tagName === "IMG" || el.tagName === "VIDEO" || el.dataset.velinLightbox);
|
|
81
|
+
this._items.forEach((item, i) => {
|
|
82
|
+
item.style.cursor = "pointer";
|
|
83
|
+
item.addEventListener("click", () => this.open(i));
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
this.shadowRoot.querySelector(".close").addEventListener("click", () => this.close());
|
|
87
|
+
this.shadowRoot.querySelector(".nav--prev").addEventListener("click", () => this._prev());
|
|
88
|
+
this.shadowRoot.querySelector(".nav--next").addEventListener("click", () => this._next());
|
|
89
|
+
this.shadowRoot.querySelector(".overlay").addEventListener("click", (e) => {
|
|
90
|
+
if (e.target.classList.contains("overlay")) this.close();
|
|
91
|
+
});
|
|
92
|
+
this.shadowRoot.querySelector(".overlay").addEventListener("keydown", (e) => {
|
|
93
|
+
if (e.key === "Escape") this.close();
|
|
94
|
+
if (e.key === "ArrowLeft") this._prev();
|
|
95
|
+
if (e.key === "ArrowRight") this._next();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
open(index = 0) {
|
|
99
|
+
this._previousFocus = document.activeElement;
|
|
100
|
+
this._index = index;
|
|
101
|
+
this.setAttribute("open", "");
|
|
102
|
+
setBackgroundInert(this);
|
|
103
|
+
this._render();
|
|
104
|
+
document.body.style.overflow = "hidden";
|
|
105
|
+
const overlay = this.shadowRoot.querySelector(".overlay");
|
|
106
|
+
overlay.removeEventListener("keydown", this._onTrapKey);
|
|
107
|
+
overlay.addEventListener("keydown", this._onTrapKey);
|
|
108
|
+
this.shadowRoot.querySelector(".close").focus();
|
|
109
|
+
}
|
|
110
|
+
close() {
|
|
111
|
+
this.removeAttribute("open");
|
|
112
|
+
clearBackgroundInert();
|
|
113
|
+
document.body.style.overflow = "";
|
|
114
|
+
if (this._previousFocus) {
|
|
115
|
+
this._previousFocus.focus();
|
|
116
|
+
this._previousFocus = null;
|
|
117
|
+
}
|
|
118
|
+
this.dispatchEvent(new CustomEvent("velin-close", { bubbles: true }));
|
|
119
|
+
}
|
|
120
|
+
_prev() {
|
|
121
|
+
this._index = (this._index - 1 + this._items.length) % this._items.length;
|
|
122
|
+
this._render();
|
|
123
|
+
}
|
|
124
|
+
_next() {
|
|
125
|
+
this._index = (this._index + 1) % this._items.length;
|
|
126
|
+
this._render();
|
|
127
|
+
}
|
|
128
|
+
_render() {
|
|
129
|
+
const container = this.shadowRoot.querySelector(".content");
|
|
130
|
+
const item = this._items[this._index];
|
|
131
|
+
if (!item) return;
|
|
132
|
+
const src = sanitizeURL(item.dataset.velinLightbox || item.src);
|
|
133
|
+
const alt = escapeHTML(item.alt || "");
|
|
134
|
+
if (item.tagName === "VIDEO") {
|
|
135
|
+
container.innerHTML = `<video src="${escapeHTML(src)}" controls autoplay></video>`;
|
|
136
|
+
} else {
|
|
137
|
+
container.innerHTML = `<img src="${escapeHTML(src)}" alt="${alt}">`;
|
|
138
|
+
}
|
|
139
|
+
const counter = this.shadowRoot.querySelector(".counter");
|
|
140
|
+
const label = `${this._index + 1} / ${this._items.length}`;
|
|
141
|
+
counter.textContent = label;
|
|
142
|
+
announce(`Slide ${label}`, "polite");
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
customElements.define("velin-lightbox", VelinLightbox);
|
|
146
|
+
var velin_lightbox_default = VelinLightbox;
|
|
147
|
+
export {
|
|
148
|
+
velin_lightbox_default as default
|
|
149
|
+
};
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeHTMLAttribute,
|
|
3
|
+
sanitizeSearchUrl
|
|
4
|
+
} from "./chunk-RDE3QE7Z.js";
|
|
5
|
+
|
|
6
|
+
// core/search/types.js
|
|
7
|
+
var SEARCH_CATEGORIES = (
|
|
8
|
+
/** @type {const} */
|
|
9
|
+
["docs", "components", "api", "examples"]
|
|
10
|
+
);
|
|
11
|
+
var CATEGORY_BOOST = {
|
|
12
|
+
components: 1.25,
|
|
13
|
+
api: 1.15,
|
|
14
|
+
docs: 1,
|
|
15
|
+
examples: 0.9
|
|
16
|
+
};
|
|
17
|
+
function normalizeEntry(entry) {
|
|
18
|
+
if (!entry || typeof entry !== "object") return null;
|
|
19
|
+
const e = (
|
|
20
|
+
/** @type {Record<string, unknown>} */
|
|
21
|
+
entry
|
|
22
|
+
);
|
|
23
|
+
const id = String(e.id || e.url || e.title || "").trim();
|
|
24
|
+
const title = String(e.title || "").trim();
|
|
25
|
+
const url = String(e.url || "").trim();
|
|
26
|
+
if (!id || !title) return null;
|
|
27
|
+
const keywords = Array.isArray(e.keywords) ? e.keywords.map((k) => String(k)) : typeof e.keywords === "string" ? e.keywords.split(/\s+/).filter(Boolean) : [];
|
|
28
|
+
let category = (
|
|
29
|
+
/** @type {SearchCategory} */
|
|
30
|
+
SEARCH_CATEGORIES.includes(
|
|
31
|
+
/** @type {SearchCategory} */
|
|
32
|
+
e.category
|
|
33
|
+
) ? e.category : "docs"
|
|
34
|
+
);
|
|
35
|
+
if (!e.category && typeof e.url === "string") {
|
|
36
|
+
if (e.url.includes("/components/")) category = "components";
|
|
37
|
+
else if (e.url.includes("/cli/") || e.url.includes("/api/")) category = "api";
|
|
38
|
+
else if (e.url.includes("samples/") || e.url.includes("examples/")) category = "examples";
|
|
39
|
+
}
|
|
40
|
+
const safeUrl = url ? sanitizeSearchUrl(url) : `#${id}`;
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
title,
|
|
44
|
+
excerpt: String(e.excerpt || e.section || "").slice(0, 200),
|
|
45
|
+
url: safeUrl,
|
|
46
|
+
category: (
|
|
47
|
+
/** @type {SearchCategory} */
|
|
48
|
+
category
|
|
49
|
+
),
|
|
50
|
+
keywords,
|
|
51
|
+
weight: typeof e.weight === "number" ? e.weight : 1
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// core/search/engine.js
|
|
56
|
+
var VelinSearchEngine = class {
|
|
57
|
+
constructor() {
|
|
58
|
+
this._entries = [];
|
|
59
|
+
}
|
|
60
|
+
/** @param {import('./types.js').SearchEntry[]} entries */
|
|
61
|
+
setEntries(entries) {
|
|
62
|
+
this._entries = entries.map((e) => normalizeEntry(e)).filter(Boolean);
|
|
63
|
+
}
|
|
64
|
+
/** @param {import('./types.js').SearchEntry[]} entries */
|
|
65
|
+
addEntries(entries) {
|
|
66
|
+
const next = entries.map((e) => normalizeEntry(e)).filter(Boolean);
|
|
67
|
+
const ids = new Set(this._entries.map((e) => e.id));
|
|
68
|
+
for (const e of next) {
|
|
69
|
+
if (!ids.has(e.id)) {
|
|
70
|
+
this._entries.push(e);
|
|
71
|
+
ids.add(e.id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} query
|
|
77
|
+
* @param {object} [opts]
|
|
78
|
+
* @param {number} [opts.limit]
|
|
79
|
+
* @param {number} [opts.minChars]
|
|
80
|
+
* @param {number} [opts.fuzzy] 0–1 threshold for typo tolerance
|
|
81
|
+
* @param {import('./types.js').SearchCategory[]} [opts.categories]
|
|
82
|
+
*/
|
|
83
|
+
query(query, opts = {}) {
|
|
84
|
+
const q = String(query || "").trim().toLowerCase();
|
|
85
|
+
const minChars = opts.minChars ?? 2;
|
|
86
|
+
const limit = opts.limit ?? 12;
|
|
87
|
+
const fuzzy = opts.fuzzy ?? 0.2;
|
|
88
|
+
const categories = opts.categories;
|
|
89
|
+
if (q.length < minChars) {
|
|
90
|
+
return { results: [], groups: {} };
|
|
91
|
+
}
|
|
92
|
+
const scored = [];
|
|
93
|
+
for (const entry of this._entries) {
|
|
94
|
+
if (categories && categories.length && !categories.includes(entry.category)) continue;
|
|
95
|
+
const s = scoreEntry(entry, q, fuzzy);
|
|
96
|
+
if (s > 0) scored.push({ entry, score: s });
|
|
97
|
+
}
|
|
98
|
+
scored.sort((a, b) => b.score - a.score);
|
|
99
|
+
const results = scored.slice(0, limit).map((x) => ({ ...x.entry, _score: x.score }));
|
|
100
|
+
const groups = {};
|
|
101
|
+
for (const r of results) {
|
|
102
|
+
if (!groups[r.category]) groups[r.category] = [];
|
|
103
|
+
groups[r.category].push(r);
|
|
104
|
+
}
|
|
105
|
+
return { results, groups };
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
function scoreEntry(entry, q, fuzzy) {
|
|
109
|
+
const title = entry.title.toLowerCase();
|
|
110
|
+
const excerpt = (entry.excerpt || "").toLowerCase();
|
|
111
|
+
const keywords = (entry.keywords || []).join(" ").toLowerCase();
|
|
112
|
+
const boost = (CATEGORY_BOOST[entry.category] || 1) * (entry.weight || 1);
|
|
113
|
+
let score = 0;
|
|
114
|
+
if (title === q) score = 100;
|
|
115
|
+
else if (title.startsWith(q)) score = 70;
|
|
116
|
+
else if (title.includes(q)) score = 50;
|
|
117
|
+
else if (keywords.includes(q)) score = 35;
|
|
118
|
+
else if (excerpt.includes(q)) score = 25;
|
|
119
|
+
else if (fuzzy > 0 && fuzzyMatch(title, q, fuzzy)) score = 40;
|
|
120
|
+
else if (fuzzy > 0 && fuzzyMatch(keywords, q, fuzzy)) score = 28;
|
|
121
|
+
else if (fuzzy > 0 && fuzzyMatch(excerpt, q, fuzzy)) score = 18;
|
|
122
|
+
else if (subsequenceMatch(title, q)) score = 22;
|
|
123
|
+
else if (fuzzy > 0) {
|
|
124
|
+
for (const word of title.split(/[^a-z0-9]+/)) {
|
|
125
|
+
if (word.length >= 3 && fuzzyMatch(word, q, fuzzy)) {
|
|
126
|
+
score = 32;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return score > 0 ? score * boost : 0;
|
|
132
|
+
}
|
|
133
|
+
function subsequenceMatch(haystack, needle) {
|
|
134
|
+
let j = 0;
|
|
135
|
+
for (let i = 0; i < haystack.length && j < needle.length; i++) {
|
|
136
|
+
if (haystack[i] === needle[j]) j++;
|
|
137
|
+
}
|
|
138
|
+
return j === needle.length;
|
|
139
|
+
}
|
|
140
|
+
function fuzzyMatch(text, query, threshold) {
|
|
141
|
+
if (!text || !query) return false;
|
|
142
|
+
if (text.includes(query)) return true;
|
|
143
|
+
if (subsequenceMatch(text, query)) return true;
|
|
144
|
+
const maxDist = Math.max(1, Math.floor(query.length * threshold * 2));
|
|
145
|
+
return levenshtein(text.slice(0, Math.min(text.length, query.length + 8)), query) <= maxDist;
|
|
146
|
+
}
|
|
147
|
+
function levenshtein(a, b) {
|
|
148
|
+
const m = a.length;
|
|
149
|
+
const n = b.length;
|
|
150
|
+
if (m === 0) return n;
|
|
151
|
+
if (n === 0) return m;
|
|
152
|
+
const dp = new Uint16Array((n + 1) * (m + 1));
|
|
153
|
+
for (let j = 0; j <= n; j++) dp[j] = j;
|
|
154
|
+
for (let i = 1; i <= m; i++) {
|
|
155
|
+
dp[i * (n + 1)] = i;
|
|
156
|
+
for (let j = 1; j <= n; j++) {
|
|
157
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
158
|
+
dp[i * (n + 1) + j] = Math.min(
|
|
159
|
+
dp[(i - 1) * (n + 1) + j] + 1,
|
|
160
|
+
dp[i * (n + 1) + (j - 1)] + 1,
|
|
161
|
+
dp[(i - 1) * (n + 1) + (j - 1)] + cost
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return dp[m * (n + 1) + n];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// core/search/highlight.js
|
|
169
|
+
function highlightHtml(text, query) {
|
|
170
|
+
const raw = String(text || "");
|
|
171
|
+
const q = String(query || "").trim();
|
|
172
|
+
if (!q || q.length < 2) return escapeHtml(raw);
|
|
173
|
+
const lower = raw.toLowerCase();
|
|
174
|
+
const ql = q.toLowerCase();
|
|
175
|
+
let start = lower.indexOf(ql);
|
|
176
|
+
let len = q.length;
|
|
177
|
+
if (start === -1) {
|
|
178
|
+
start = fuzzySubsequenceIndex(lower, ql);
|
|
179
|
+
if (start === -1) return escapeHtml(raw);
|
|
180
|
+
len = Math.min(q.length, raw.length - start);
|
|
181
|
+
}
|
|
182
|
+
return escapeHtml(raw.slice(0, start)) + '<mark class="velin-search-hit">' + escapeHtml(raw.slice(start, start + len)) + "</mark>" + escapeHtml(raw.slice(start + len));
|
|
183
|
+
}
|
|
184
|
+
function escapeHtml(s) {
|
|
185
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
186
|
+
}
|
|
187
|
+
function fuzzySubsequenceIndex(haystack, needle) {
|
|
188
|
+
let j = 0;
|
|
189
|
+
let start = -1;
|
|
190
|
+
for (let i = 0; i < haystack.length && j < needle.length; i++) {
|
|
191
|
+
if (haystack[i] === needle[j]) {
|
|
192
|
+
if (j === 0) start = i;
|
|
193
|
+
j++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return j === needle.length ? start : -1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// core/search/docs-url.js
|
|
200
|
+
var DOCS_MOUNT = "/docs/";
|
|
201
|
+
var DOC_ROOT_SEGMENTS = /* @__PURE__ */ new Set([
|
|
202
|
+
"getting-started",
|
|
203
|
+
"extend",
|
|
204
|
+
"guides",
|
|
205
|
+
"utilities",
|
|
206
|
+
"components",
|
|
207
|
+
"forms",
|
|
208
|
+
"layout",
|
|
209
|
+
"content",
|
|
210
|
+
"customize",
|
|
211
|
+
"animations",
|
|
212
|
+
"about",
|
|
213
|
+
"helpers",
|
|
214
|
+
"generated",
|
|
215
|
+
"migration"
|
|
216
|
+
]);
|
|
217
|
+
function getDocsBaseUrl() {
|
|
218
|
+
if (typeof document === "undefined") {
|
|
219
|
+
return "https://example.invalid/docs/";
|
|
220
|
+
}
|
|
221
|
+
const script = document.querySelector('script[src*="doc-search"]');
|
|
222
|
+
if (script?.src) {
|
|
223
|
+
const u = new URL(script.src);
|
|
224
|
+
const i = u.pathname.indexOf(DOCS_MOUNT);
|
|
225
|
+
if (i !== -1) {
|
|
226
|
+
return u.origin + u.pathname.slice(0, i + DOCS_MOUNT.length);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const path = window.location.pathname.replace(/\\/g, "/");
|
|
230
|
+
const idx = path.indexOf(DOCS_MOUNT);
|
|
231
|
+
if (idx >= 0) {
|
|
232
|
+
return new URL(path.slice(0, idx + DOCS_MOUNT.length), window.location.origin).href;
|
|
233
|
+
}
|
|
234
|
+
return new URL("./", window.location.href).href;
|
|
235
|
+
}
|
|
236
|
+
function splitUrlHash(url) {
|
|
237
|
+
const i = url.indexOf("#");
|
|
238
|
+
if (i === -1) return { path: url, hash: "" };
|
|
239
|
+
return { path: url.slice(0, i), hash: url.slice(i) };
|
|
240
|
+
}
|
|
241
|
+
function relativizeDocsPathname(pathname) {
|
|
242
|
+
const docsIdx = pathname.indexOf(DOCS_MOUNT);
|
|
243
|
+
if (docsIdx < 0) return null;
|
|
244
|
+
const rest = pathname.slice(docsIdx + DOCS_MOUNT.length);
|
|
245
|
+
const parts = rest.split("/").filter(Boolean);
|
|
246
|
+
if (parts.length >= 2 && DOC_ROOT_SEGMENTS.has(parts[0]) && DOC_ROOT_SEGMENTS.has(parts[1]) && parts[0] !== parts[1]) {
|
|
247
|
+
return parts.slice(1).join("/");
|
|
248
|
+
}
|
|
249
|
+
return rest;
|
|
250
|
+
}
|
|
251
|
+
function resolveDocsSearchUrl(url) {
|
|
252
|
+
if (!url || url === "#") return "";
|
|
253
|
+
if (typeof window === "undefined") return url;
|
|
254
|
+
const { path, hash } = splitUrlHash(url);
|
|
255
|
+
if (!path) return hash || "";
|
|
256
|
+
let rel = path;
|
|
257
|
+
if (/^https?:\/\//i.test(path)) {
|
|
258
|
+
try {
|
|
259
|
+
const fixed = relativizeDocsPathname(new URL(path).pathname);
|
|
260
|
+
rel = fixed ?? path;
|
|
261
|
+
} catch {
|
|
262
|
+
return "";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const target = new URL(rel || "", getDocsBaseUrl());
|
|
267
|
+
return target.href + (hash && !target.href.includes("#") ? hash : "");
|
|
268
|
+
} catch {
|
|
269
|
+
return "";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// core/search/index.js
|
|
274
|
+
var defaultEngine = new VelinSearchEngine();
|
|
275
|
+
function parseIndexPayload(data) {
|
|
276
|
+
if (Array.isArray(data)) return data;
|
|
277
|
+
if (data && Array.isArray(data.entries)) return data.entries;
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
function createSearch(config = {}) {
|
|
281
|
+
const engine = new VelinSearchEngine();
|
|
282
|
+
const wantWorker = config.worker === true;
|
|
283
|
+
let workerClient = null;
|
|
284
|
+
async function getWorker() {
|
|
285
|
+
if (!wantWorker || workerClient) return workerClient;
|
|
286
|
+
if (typeof Worker === "undefined") return null;
|
|
287
|
+
const { createSearchWorker } = await import("./worker-client-MPT7PNQ4.js");
|
|
288
|
+
workerClient = createSearchWorker(config.workerUrl);
|
|
289
|
+
return workerClient;
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
engine,
|
|
293
|
+
async loadIndex(source) {
|
|
294
|
+
let entries = [];
|
|
295
|
+
if (typeof source === "string") {
|
|
296
|
+
const res = await fetch(source);
|
|
297
|
+
entries = parseIndexPayload(await res.json());
|
|
298
|
+
} else if (Array.isArray(source)) {
|
|
299
|
+
entries = source;
|
|
300
|
+
}
|
|
301
|
+
engine.setEntries(entries);
|
|
302
|
+
const w = await getWorker();
|
|
303
|
+
if (w) await w.setEntries(engine._entries);
|
|
304
|
+
return entries.length;
|
|
305
|
+
},
|
|
306
|
+
async query(q, opts) {
|
|
307
|
+
const w = await getWorker();
|
|
308
|
+
if (w) return w.query(q, opts);
|
|
309
|
+
return engine.query(q, opts);
|
|
310
|
+
},
|
|
311
|
+
addEntries(entries) {
|
|
312
|
+
engine.addEntries(entries);
|
|
313
|
+
void getWorker().then((w) => w?.setEntries(engine._entries));
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// components/velin-search.js
|
|
319
|
+
var CATEGORY_LABELS = {
|
|
320
|
+
docs: "Documentation",
|
|
321
|
+
components: "Components",
|
|
322
|
+
api: "API",
|
|
323
|
+
examples: "Examples"
|
|
324
|
+
};
|
|
325
|
+
var VelinSearch = class extends HTMLElement {
|
|
326
|
+
static get observedAttributes() {
|
|
327
|
+
return ["index", "categories", "min-chars", "fuzzy", "placeholder", "debounce"];
|
|
328
|
+
}
|
|
329
|
+
constructor() {
|
|
330
|
+
super();
|
|
331
|
+
this._search = createSearch();
|
|
332
|
+
this._debounceTimer = null;
|
|
333
|
+
this._activeIndex = -1;
|
|
334
|
+
this._flatResults = [];
|
|
335
|
+
this._indexLoaded = false;
|
|
336
|
+
this._listId = `velin-search-listbox-${Math.random().toString(36).slice(2, 9)}`;
|
|
337
|
+
this._mounted = false;
|
|
338
|
+
}
|
|
339
|
+
connectedCallback() {
|
|
340
|
+
if (this._mounted) return;
|
|
341
|
+
this._mounted = true;
|
|
342
|
+
this._ensureMarkup();
|
|
343
|
+
this._loadIndex();
|
|
344
|
+
this._bindEvents();
|
|
345
|
+
}
|
|
346
|
+
disconnectedCallback() {
|
|
347
|
+
this._teardown?.();
|
|
348
|
+
this._mounted = false;
|
|
349
|
+
}
|
|
350
|
+
get indexUrl() {
|
|
351
|
+
return this.getAttribute("index") || "/search-index.json";
|
|
352
|
+
}
|
|
353
|
+
get minChars() {
|
|
354
|
+
return parseInt(this.getAttribute("min-chars") || "2", 10);
|
|
355
|
+
}
|
|
356
|
+
get fuzzy() {
|
|
357
|
+
const v = this.getAttribute("fuzzy");
|
|
358
|
+
return v === null ? 0.2 : parseFloat(v) || 0;
|
|
359
|
+
}
|
|
360
|
+
get categories() {
|
|
361
|
+
const raw = this.getAttribute("categories");
|
|
362
|
+
if (!raw) return null;
|
|
363
|
+
return raw.split(",").map((c) => c.trim()).filter((c) => SEARCH_CATEGORIES.includes(c));
|
|
364
|
+
}
|
|
365
|
+
get debounceMs() {
|
|
366
|
+
return parseInt(this.getAttribute("debounce") || "120", 10);
|
|
367
|
+
}
|
|
368
|
+
_ensureMarkup() {
|
|
369
|
+
if (this.querySelector("[data-velin-search-input]")) return;
|
|
370
|
+
const ph = escapeHTMLAttribute(this.getAttribute("placeholder") || "Search\u2026");
|
|
371
|
+
const label = escapeHTMLAttribute(this.getAttribute("aria-label") || "Search");
|
|
372
|
+
this.innerHTML = `
|
|
373
|
+
<div class="velin-search">
|
|
374
|
+
<div class="velin-search__field">
|
|
375
|
+
<input type="search" class="velin-search__input" data-velin-search-input
|
|
376
|
+
placeholder="${ph}" autocomplete="off" role="combobox" aria-expanded="false"
|
|
377
|
+
aria-label="${label}" aria-controls="${this._listId}" aria-autocomplete="list" />
|
|
378
|
+
</div>
|
|
379
|
+
<div class="velin-search__status velin-visually-hidden" data-velin-search-status aria-live="polite" aria-atomic="true"></div>
|
|
380
|
+
<div class="velin-search__results" data-velin-search-results role="listbox"
|
|
381
|
+
id="${this._listId}" hidden></div>
|
|
382
|
+
</div>
|
|
383
|
+
`;
|
|
384
|
+
}
|
|
385
|
+
_inputEl() {
|
|
386
|
+
return this.querySelector("[data-velin-search-input]");
|
|
387
|
+
}
|
|
388
|
+
_resultsEl() {
|
|
389
|
+
return this.querySelector("[data-velin-search-results]");
|
|
390
|
+
}
|
|
391
|
+
async _loadIndex() {
|
|
392
|
+
try {
|
|
393
|
+
await this._search.loadIndex(this.indexUrl);
|
|
394
|
+
this._indexLoaded = true;
|
|
395
|
+
} catch {
|
|
396
|
+
this._indexLoaded = false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
_bindEvents() {
|
|
400
|
+
const input = this._inputEl();
|
|
401
|
+
const panel = this._resultsEl();
|
|
402
|
+
if (!input || !panel) return;
|
|
403
|
+
input.addEventListener("input", () => {
|
|
404
|
+
clearTimeout(this._debounceTimer);
|
|
405
|
+
this._debounceTimer = setTimeout(() => this._runQuery(input.value), this.debounceMs);
|
|
406
|
+
});
|
|
407
|
+
input.addEventListener("focus", () => {
|
|
408
|
+
if (input.value.trim().length >= this.minChars) this._runQuery(input.value);
|
|
409
|
+
});
|
|
410
|
+
input.addEventListener("keydown", (e) => this._onKeydown(e, input, panel));
|
|
411
|
+
const onDocClick = (e) => {
|
|
412
|
+
if (!this.contains(e.target)) this._hide(panel, input);
|
|
413
|
+
};
|
|
414
|
+
document.addEventListener("click", onDocClick);
|
|
415
|
+
this._teardown = () => {
|
|
416
|
+
document.removeEventListener("click", onDocClick);
|
|
417
|
+
clearTimeout(this._debounceTimer);
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
async _runQuery(raw) {
|
|
421
|
+
const input = this._inputEl();
|
|
422
|
+
const panel = this._resultsEl();
|
|
423
|
+
if (!input || !panel) return;
|
|
424
|
+
const q = raw.trim();
|
|
425
|
+
if (q.length < this.minChars) {
|
|
426
|
+
this._hide(panel, input);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (!this._indexLoaded) await this._loadIndex();
|
|
430
|
+
const { groups } = this._search.query(q, {
|
|
431
|
+
minChars: this.minChars,
|
|
432
|
+
fuzzy: this.fuzzy,
|
|
433
|
+
categories: this.categories || void 0,
|
|
434
|
+
limit: 12
|
|
435
|
+
});
|
|
436
|
+
this._activeIndex = -1;
|
|
437
|
+
this._renderResults(panel, input, q, groups);
|
|
438
|
+
if (this._flatResults.length) {
|
|
439
|
+
panel.hidden = false;
|
|
440
|
+
panel.classList.add("velin-search__results--open");
|
|
441
|
+
input.setAttribute("aria-expanded", "true");
|
|
442
|
+
} else {
|
|
443
|
+
this._hide(panel, input);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
_renderResults(panel, input, q, groups) {
|
|
447
|
+
panel.innerHTML = "";
|
|
448
|
+
const order = this.categories || [...SEARCH_CATEGORIES];
|
|
449
|
+
this._flatResults = [];
|
|
450
|
+
let globalIdx = 0;
|
|
451
|
+
for (const cat of order) {
|
|
452
|
+
const items = groups[cat];
|
|
453
|
+
if (!items?.length) continue;
|
|
454
|
+
const groupId = `${this._listId}-group-${cat}`;
|
|
455
|
+
const group = document.createElement("div");
|
|
456
|
+
group.setAttribute("role", "group");
|
|
457
|
+
group.setAttribute("aria-labelledby", groupId);
|
|
458
|
+
const heading = document.createElement("div");
|
|
459
|
+
heading.className = "velin-search__group-label";
|
|
460
|
+
heading.id = groupId;
|
|
461
|
+
heading.textContent = CATEGORY_LABELS[cat] || cat;
|
|
462
|
+
group.appendChild(heading);
|
|
463
|
+
for (const item of items) {
|
|
464
|
+
const a = document.createElement("a");
|
|
465
|
+
const navHref = resolveDocsSearchUrl(item.url);
|
|
466
|
+
if (navHref) a.href = navHref;
|
|
467
|
+
a.className = "velin-search__item";
|
|
468
|
+
a.setAttribute("role", "option");
|
|
469
|
+
a.id = `velin-search-opt-${globalIdx}`;
|
|
470
|
+
a.innerHTML = `<span class="velin-search__title">${highlightHtml(item.title, q)}</span><span class="velin-search__excerpt">${highlightHtml(item.excerpt || "", q)}</span>`;
|
|
471
|
+
a.addEventListener(
|
|
472
|
+
"click",
|
|
473
|
+
(e) => {
|
|
474
|
+
e.preventDefault();
|
|
475
|
+
if (navHref) window.location.assign(navHref);
|
|
476
|
+
},
|
|
477
|
+
true
|
|
478
|
+
);
|
|
479
|
+
a.addEventListener("mouseenter", () => this._setActive(globalIdx, panel, input));
|
|
480
|
+
group.appendChild(a);
|
|
481
|
+
this._flatResults.push(item);
|
|
482
|
+
globalIdx++;
|
|
483
|
+
}
|
|
484
|
+
panel.appendChild(group);
|
|
485
|
+
}
|
|
486
|
+
const status = this._statusEl();
|
|
487
|
+
if (status) {
|
|
488
|
+
status.textContent = this._flatResults.length ? `${this._flatResults.length} results` : "No results";
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
_statusEl() {
|
|
492
|
+
return this.querySelector("[data-velin-search-status]");
|
|
493
|
+
}
|
|
494
|
+
_setActive(idx, panel, input) {
|
|
495
|
+
this._activeIndex = idx;
|
|
496
|
+
panel.querySelectorAll(".velin-search__item").forEach((el, i) => {
|
|
497
|
+
el.classList.toggle("velin-search__item--active", i === idx);
|
|
498
|
+
if (i === idx) {
|
|
499
|
+
input.setAttribute("aria-activedescendant", el.id);
|
|
500
|
+
el.scrollIntoView({ block: "nearest" });
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
_onKeydown(e, input, panel) {
|
|
505
|
+
if (e.key === "Escape") {
|
|
506
|
+
this._hide(panel, input);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (!this._flatResults.length) return;
|
|
510
|
+
if (e.key === "ArrowDown") {
|
|
511
|
+
e.preventDefault();
|
|
512
|
+
const next = this._activeIndex < this._flatResults.length - 1 ? this._activeIndex + 1 : 0;
|
|
513
|
+
this._setActive(next, panel, input);
|
|
514
|
+
} else if (e.key === "ArrowUp") {
|
|
515
|
+
e.preventDefault();
|
|
516
|
+
const prev = this._activeIndex > 0 ? this._activeIndex - 1 : this._flatResults.length - 1;
|
|
517
|
+
this._setActive(prev, panel, input);
|
|
518
|
+
} else if (e.key === "Enter") {
|
|
519
|
+
const target = this._activeIndex >= 0 ? this._flatResults[this._activeIndex] : this._flatResults[0];
|
|
520
|
+
const navHref = target?.url ? resolveDocsSearchUrl(target.url) : "";
|
|
521
|
+
if (navHref) {
|
|
522
|
+
e.preventDefault();
|
|
523
|
+
window.location.assign(navHref);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
_hide(panel, input) {
|
|
528
|
+
panel.hidden = true;
|
|
529
|
+
panel.classList.remove("velin-search__results--open");
|
|
530
|
+
input.setAttribute("aria-expanded", "false");
|
|
531
|
+
input.removeAttribute("aria-activedescendant");
|
|
532
|
+
this._activeIndex = -1;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
var velin_search_default = VelinSearch;
|
|
536
|
+
if (typeof HTMLElement !== "undefined" && typeof customElements !== "undefined" && !customElements.get("velin-search")) {
|
|
537
|
+
customElements.define("velin-search", VelinSearch);
|
|
538
|
+
}
|
|
539
|
+
function bindDeclarativeSearch(root = document) {
|
|
540
|
+
root.querySelectorAll("[velin-search-input]").forEach((input) => {
|
|
541
|
+
if (input.dataset.velinSearchBound) return;
|
|
542
|
+
input.dataset.velinSearchBound = "1";
|
|
543
|
+
let host = input.closest("velin-search");
|
|
544
|
+
if (!host) {
|
|
545
|
+
host = document.createElement("velin-search");
|
|
546
|
+
host.setAttribute("index", input.getAttribute("data-search-index") || "/search-index.json");
|
|
547
|
+
const results = input.parentElement?.querySelector("[velin-search-results]");
|
|
548
|
+
input.before(host);
|
|
549
|
+
host.appendChild(input);
|
|
550
|
+
if (results) host.appendChild(results);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
export {
|
|
555
|
+
bindDeclarativeSearch,
|
|
556
|
+
velin_search_default as default
|
|
557
|
+
};
|