@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.
Files changed (51) hide show
  1. package/README.de.md +15 -11
  2. package/README.md +15 -11
  3. package/cli/cli-manifest.json +1 -1
  4. package/cli/docgen/extract-a11y.js +4 -2
  5. package/cli/index.js +4 -2
  6. package/components/index.js +2 -0
  7. package/components/runtime/component-loaders.js +2 -0
  8. package/components/velin-data-table.js +361 -0
  9. package/components/velin-form-summary.js +348 -0
  10. package/core/a11y/component-contracts.json +18 -1
  11. package/core/highlight/languages/go.js +28 -0
  12. package/core/highlight/languages/python.js +29 -0
  13. package/core/highlight/languages/rust.js +32 -0
  14. package/core/highlight/languages/yaml.js +23 -0
  15. package/core/highlight/registry.js +13 -2
  16. package/core/meta/build.js +1 -1
  17. package/core/search/worker-client.js +2 -3
  18. package/dist/chunks/attributes-353XDOAX.js +391 -0
  19. package/dist/chunks/attributes-ADOKLKA7.js +391 -0
  20. package/dist/chunks/chunk-ERF5YVP4.js +253 -0
  21. package/dist/chunks/chunk-IEHKRARD.js +109 -0
  22. package/dist/chunks/chunk-QVHYL3R4.js +111 -0
  23. package/dist/chunks/go-HZ6XTFCK.js +31 -0
  24. package/dist/chunks/highlight-IBDX4XWS.js +37 -0
  25. package/dist/chunks/python-BMPKDKNO.js +32 -0
  26. package/dist/chunks/runtime-entry.js +1 -1
  27. package/dist/chunks/rust-2BWKI2MN.js +35 -0
  28. package/dist/chunks/velin-code-block-ITCLIC6T.js +132 -0
  29. package/dist/chunks/velin-data-table-KK7IDGTP.js +302 -0
  30. package/dist/chunks/velin-form-summary-XRQ7COQH.js +273 -0
  31. package/dist/chunks/velin-lightbox-NXA3U2FM.js +149 -0
  32. package/dist/chunks/velin-search-FDHEMCSO.js +557 -0
  33. package/dist/chunks/worker-client-MPT7PNQ4.js +47 -0
  34. package/dist/chunks/yaml-O67LEQYT.js +26 -0
  35. package/dist/llms.txt +2 -2
  36. package/dist/search-index.json +28 -2
  37. package/dist/velin-agent.json +31 -5
  38. package/dist/velinstyle-components.iife.js +745 -7
  39. package/dist/velinstyle-components.js +746 -5
  40. package/dist/velinstyle-components.min.js +85 -85
  41. package/dist/velinstyle.css +141 -19
  42. package/dist/velinstyle.d.ts +2 -0
  43. package/dist/velinstyle.min.css +1 -1
  44. package/package.json +8 -2
  45. package/src/base/wc-placeholder.css +7 -0
  46. package/src/components/data-table.css +93 -0
  47. package/src/components/form-validation.css +40 -0
  48. package/src/velinstyle.css +1 -0
  49. package/dist/llms.test.txt +0 -40
  50. package/dist/search-index.test.json +0 -1773
  51. 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">&#8249;</button>
72
+ <div class="content" part="content"></div>
73
+ <button class="nav nav--next" aria-label="Next">&#8250;</button>
74
+ <button class="close" aria-label="Close">&times;</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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
+ };