@farming-labs/svelte-theme 0.2.38 → 0.2.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/svelte-theme",
3
- "version": "0.2.38",
3
+ "version": "0.2.39",
4
4
  "description": "Svelte UI components for @farming-labs/docs — layout, sidebar, TOC, search, and theme toggle",
5
5
  "keywords": [
6
6
  "docs",
@@ -122,8 +122,8 @@
122
122
  "dependencies": {
123
123
  "gray-matter": "^4.0.3",
124
124
  "sugar-high": "^0.9.5",
125
- "@farming-labs/docs": "0.2.38",
126
- "@farming-labs/svelte": "0.2.38"
125
+ "@farming-labs/svelte": "0.2.39",
126
+ "@farming-labs/docs": "0.2.39"
127
127
  },
128
128
  "peerDependencies": {
129
129
  "svelte": ">=5.0.0"
@@ -11,7 +11,13 @@
11
11
  const STORAGE_KEY = "fd:omni:recents";
12
12
  const MAX_RECENTS = 8;
13
13
  const DEBOUNCE_MS = 120;
14
- const PLACEHOLDER = "Search documentation…";
14
+ const BREADCRUMB_SEPARATOR = "\u00a0\u00a0>\u00a0\u00a0";
15
+ const FILTER_LABELS = {
16
+ all: "All",
17
+ pages: "Pages",
18
+ inside: "Inside pages",
19
+ };
20
+ const FILTER_OPTIONS = ["all", "pages", "inside"];
15
21
 
16
22
  let { onclose } = $props();
17
23
 
@@ -20,9 +26,13 @@
20
26
  let loading = $state(false);
21
27
  let activeId = $state(null);
22
28
  let recentsList = $state([]);
29
+ let filter = $state("all");
30
+ let filterOpen = $state(false);
23
31
  let inputEl = $state(null);
24
32
  let listRef = $state(null);
25
33
  let debounceTimer = null;
34
+ let abortController = null;
35
+ const searchCache = new Map();
26
36
 
27
37
  function withLang(url) {
28
38
  if (!url || url.startsWith("#")) return url;
@@ -37,14 +47,149 @@
37
47
  }
38
48
  }
39
49
 
50
+ function breadcrumbForUrl(url) {
51
+ try {
52
+ const parsed = new URL(url, "https://farming-labs.local");
53
+ const parts = parsed.pathname
54
+ .split("/")
55
+ .filter(Boolean)
56
+ .map((part) =>
57
+ decodeURIComponent(part)
58
+ .replace(/[-_]+/g, " ")
59
+ .replace(/\b\w/g, (char) => char.toUpperCase())
60
+ );
61
+ return parts.length ? parts.join(BREADCRUMB_SEPARATOR) : "Docs";
62
+ } catch {
63
+ return "Docs";
64
+ }
65
+ }
66
+
67
+ function displayLabelForResult(result) {
68
+ if (result.section) return result.section;
69
+ const label = result.content ?? "";
70
+ const parts = label.split(/\s+[—–]\s+/).map((part) => part.trim()).filter(Boolean);
71
+ return result.type === "heading" && parts.length > 1 ? parts[parts.length - 1] : label;
72
+ }
73
+
74
+ function normalizeSearchPhrase(value) {
75
+ return (value ?? "").toLowerCase().replace(/[?!.,;:]+$/g, "").replace(/\s+/g, " ").trim();
76
+ }
77
+
78
+ function escapeRegExp(value) {
79
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
80
+ }
81
+
82
+ function literalMatchPriority(searchQuery, value) {
83
+ const q = normalizeSearchPhrase(searchQuery);
84
+ const text = normalizeSearchPhrase(value);
85
+ if (!q || !text) return 0;
86
+ if (text === q) return 2;
87
+
88
+ const boundary = "[^\\p{L}\\p{N}]";
89
+ return new RegExp(`(^|${boundary})${escapeRegExp(q)}(?=$|${boundary})`, "u").test(text)
90
+ ? 1
91
+ : 0;
92
+ }
93
+
94
+ function tokenizeLiteralQuery(searchQuery) {
95
+ return Array.from(
96
+ new Set(
97
+ searchQuery
98
+ .toLowerCase()
99
+ .replace(/[^\p{L}\p{N}@/_:.-]+/gu, " ")
100
+ .split(/\s+/)
101
+ .map((word) => word.replace(/^[^\p{L}\p{N}@]+|[^\p{L}\p{N}]+$/gu, ""))
102
+ .filter((word) => word.length > 1)
103
+ )
104
+ );
105
+ }
106
+
107
+ function isLiteralLookupQuery(searchQuery) {
108
+ const q = normalizeSearchPhrase(searchQuery);
109
+ const words = tokenizeLiteralQuery(q);
110
+ return words.length > 0 && words.length <= 3 && words.join(" ") === q;
111
+ }
112
+
113
+ function hasDistinctSearchSection(result) {
114
+ if (result.type === "page") return false;
115
+ if (!result.section) return true;
116
+ const title = (result.content ?? "").split(/\s+[—–]\s+/)[0] ?? "";
117
+ return normalizeSearchPhrase(result.section) !== normalizeSearchPhrase(title);
118
+ }
119
+
120
+ function insideLiteralPriority(result, searchQuery) {
121
+ if (!hasDistinctSearchSection(result) || !isLiteralLookupQuery(searchQuery)) return 0;
122
+ return Math.max(
123
+ literalMatchPriority(searchQuery, result.section),
124
+ literalMatchPriority(searchQuery, result.description)
125
+ );
126
+ }
127
+
128
+ function sortResultsForFilter(results) {
129
+ const q = query.trim();
130
+ if (!q) return results;
131
+ return results
132
+ .map((result, index) => ({ result, index }))
133
+ .sort((a, b) => {
134
+ const aLiteralPriority = insideLiteralPriority(a.result, q);
135
+ const bLiteralPriority = insideLiteralPriority(b.result, q);
136
+ const literalDelta = bLiteralPriority - aLiteralPriority;
137
+ if (literalDelta) return literalDelta;
138
+ if (aLiteralPriority > 0 && bLiteralPriority > 0) return a.index - b.index;
139
+
140
+ const scoreDelta = (b.result.score ?? 0) - (a.result.score ?? 0);
141
+ if (scoreDelta) return scoreDelta;
142
+
143
+ return a.index - b.index;
144
+ })
145
+ .map(({ result }) => result);
146
+ }
147
+
148
+ function escapeHtml(value) {
149
+ return (value ?? "").replace(/[&<>"']/g, (char) => {
150
+ if (char === "&") return "&amp;";
151
+ if (char === "<") return "&lt;";
152
+ if (char === ">") return "&gt;";
153
+ if (char === '"') return "&quot;";
154
+ return "&#39;";
155
+ });
156
+ }
157
+
158
+ function highlightSnippet(text) {
159
+ const q = query.trim();
160
+ if (!q) return escapeHtml(text);
161
+ let html = "";
162
+ let lastIndex = 0;
163
+ const regex = new RegExp(escapeRegExp(q), "gi");
164
+ let match;
165
+
166
+ while ((match = regex.exec(text)) !== null) {
167
+ html += escapeHtml(text.slice(lastIndex, match.index));
168
+ html += `<mark class="omni-highlight">${escapeHtml(match[0])}</mark>`;
169
+ lastIndex = match.index + match[0].length;
170
+ }
171
+
172
+ return html + escapeHtml(text.slice(lastIndex));
173
+ }
174
+
175
+ let visibleResults = $derived.by(() => {
176
+ if (filter === "pages") return currentResults.filter((result) => result.type === "page");
177
+ if (filter === "inside") {
178
+ return sortResultsForFilter(currentResults.filter((result) => result.type !== "page"));
179
+ }
180
+ return sortResultsForFilter(currentResults);
181
+ });
182
+
40
183
  let flatItems = $derived.by(() => {
41
184
  const q = query.trim();
42
- if (q && currentResults.length) {
43
- return currentResults.map((r) => ({
185
+ if (q && visibleResults.length) {
186
+ return visibleResults.map((r) => ({
44
187
  id: r.url,
45
- label: r.content,
188
+ label: displayLabelForResult(r),
46
189
  url: r.url,
47
- subtitle: r.description ?? "Page",
190
+ subtitle: breadcrumbForUrl(r.url),
191
+ description: r.description,
192
+ descriptionHtml: r.description ? highlightSnippet(r.description) : "",
48
193
  }));
49
194
  }
50
195
  return recentsList.map((r) => ({
@@ -56,13 +201,15 @@
56
201
  });
57
202
 
58
203
  let showRecents = $derived(!query.trim());
59
- let showDocs = $derived(!!query.trim() && currentResults.length > 0);
204
+ let showDocs = $derived(!!query.trim() && visibleResults.length > 0);
60
205
  let showEmpty = $derived(
61
- query.trim() ? currentResults.length === 0 && !loading : recentsList.length === 0
206
+ query.trim() ? visibleResults.length === 0 && !loading : recentsList.length === 0
62
207
  );
63
208
  let emptyText = $derived(
64
209
  query.trim()
65
- ? "No results found. Try a different query."
210
+ ? currentResults.length > 0
211
+ ? `No ${FILTER_LABELS[filter].toLowerCase()} results found.`
212
+ : "No results found. Try a different query."
66
213
  : "Type to search the docs, or browse recent items."
67
214
  );
68
215
 
@@ -94,6 +241,12 @@
94
241
  onclose?.();
95
242
  }
96
243
 
244
+ function updateFilter(nextFilter) {
245
+ filter = nextFilter;
246
+ filterOpen = false;
247
+ activeId = flatItems[0]?.id ?? null;
248
+ }
249
+
97
250
  function executeItem(item) {
98
251
  const localizedUrl = withLang(item.url);
99
252
  saveRecent({ id: localizedUrl, label: item.label ?? item.content ?? item.url, url: localizedUrl });
@@ -133,19 +286,41 @@
133
286
  currentResults = [];
134
287
  activeId = null;
135
288
  const q = query.trim();
289
+ filterOpen = false;
290
+ if (abortController) abortController.abort();
136
291
  if (!q) return;
137
292
  if (debounceTimer) clearTimeout(debounceTimer);
293
+ const requestUrl = withLang(`/api/docs?query=${encodeURIComponent(q)}`);
294
+ const cached = searchCache.get(requestUrl);
295
+ if (cached) {
296
+ currentResults = cached;
297
+ activeId = cached[0]?.url ?? null;
298
+ return;
299
+ }
138
300
  debounceTimer = setTimeout(async () => {
301
+ const controller = new AbortController();
302
+ abortController = controller;
139
303
  loading = true;
140
304
  try {
141
- const res = await fetch(withLang(`/api/docs?query=${encodeURIComponent(q)}`));
305
+ const res = await fetch(requestUrl, { signal: controller.signal });
142
306
  const data = res.ok ? await res.json() : [];
143
- currentResults = Array.isArray(data) ? data : [];
307
+ if (controller.signal.aborted) return;
308
+ const nextResults = Array.isArray(data) ? data : [];
309
+ if (searchCache.size >= 20) {
310
+ const firstKey = searchCache.keys().next().value;
311
+ if (firstKey) searchCache.delete(firstKey);
312
+ }
313
+ searchCache.set(requestUrl, nextResults);
314
+ currentResults = nextResults;
144
315
  activeId = currentResults[0]?.url ?? null;
145
- } catch {
316
+ } catch (error) {
317
+ if (controller.signal.aborted) return;
146
318
  currentResults = [];
147
319
  } finally {
148
- loading = false;
320
+ if (abortController === controller) {
321
+ abortController = null;
322
+ loading = false;
323
+ }
149
324
  }
150
325
  }, DEBOUNCE_MS);
151
326
  }
@@ -155,6 +330,11 @@
155
330
  onInput();
156
331
  });
157
332
 
333
+ $effect(() => {
334
+ void filter;
335
+ activeId = flatItems[0]?.id ?? null;
336
+ });
337
+
158
338
  function handleKeydown(e) {
159
339
  if (e.key === "Escape") {
160
340
  e.preventDefault();
@@ -185,19 +365,6 @@
185
365
  e.stopPropagation();
186
366
  }
187
367
 
188
- function onExternalClick(e, url) {
189
- e.preventDefault();
190
- e.stopPropagation();
191
- try {
192
- if (typeof window !== "undefined") window.open(url, "_blank", "noopener,noreferrer");
193
- } catch {
194
- if (typeof window !== "undefined") window.location.href = url;
195
- }
196
- }
197
-
198
- const FileIcon = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>`;
199
- const ExternalIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`;
200
- const ChevronIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>`;
201
368
  const EmptyIcon = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>`;
202
369
 
203
370
  onMount(() => {
@@ -209,6 +376,7 @@
209
376
  onDestroy(() => {
210
377
  if (typeof document !== "undefined") document.body.style.overflow = "";
211
378
  if (debounceTimer) clearTimeout(debounceTimer);
379
+ if (abortController) abortController.abort();
212
380
  });
213
381
  </script>
214
382
 
@@ -247,16 +415,12 @@
247
415
  aria-expanded="true"
248
416
  aria-controls="omni-listbox"
249
417
  aria-activedescendant={activeId ? `omni-item-${activeId}` : undefined}
250
- placeholder={PLACEHOLDER}
418
+ placeholder="Search"
251
419
  autocomplete="off"
252
420
  onkeydown={handleKeydown}
253
421
  />
254
- <kbd class="omni-kbd">⌘ K</kbd>
255
422
  <button type="button" aria-label="Close" class="omni-close-btn" onclick={close}>
256
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
257
- <path d="M18 6 6 18" />
258
- <path d="m6 6 12 12" />
259
- </svg>
423
+ ESC
260
424
  </button>
261
425
  </div>
262
426
  </div>
@@ -293,27 +457,10 @@
293
457
  onmouseenter={() => (activeId = item.id)}
294
458
  onclick={() => executeItem(item)}
295
459
  >
296
- <div class="omni-item-icon">
297
- {@html FileIcon}
298
- </div>
299
460
  <div class="omni-item-text">
300
- <div class="omni-item-label">{item.label}</div>
301
461
  <div class="omni-item-subtitle">{item.subtitle}</div>
462
+ <div class="omni-item-label">{item.label}</div>
302
463
  </div>
303
- <a
304
- href={item.url}
305
- class="omni-item-badge"
306
- title="Open in new tab"
307
- target="_blank"
308
- rel="noopener noreferrer"
309
- aria-hidden="true"
310
- onclick={(e) => onExternalClick(e, item.url)}
311
- >
312
- {@html ExternalIcon}
313
- </a>
314
- <span class="omni-item-chevron" aria-hidden="true">
315
- {@html ChevronIcon}
316
- </span>
317
464
  </button>
318
465
  {/each}
319
466
  </div>
@@ -336,27 +483,15 @@
336
483
  onmouseenter={() => (activeId = item.id)}
337
484
  onclick={() => executeItem(item)}
338
485
  >
339
- <div class="omni-item-icon">
340
- {@html FileIcon}
341
- </div>
342
486
  <div class="omni-item-text">
343
- <div class="omni-item-label">{item.label}</div>
344
487
  <div class="omni-item-subtitle">{item.subtitle}</div>
488
+ <div class="omni-item-label">{item.label}</div>
489
+ {#if item.description}
490
+ <div class="omni-item-description">
491
+ {@html item.descriptionHtml}
492
+ </div>
493
+ {/if}
345
494
  </div>
346
- <a
347
- href={item.url}
348
- class="omni-item-badge"
349
- title="Open in new tab"
350
- target="_blank"
351
- rel="noopener noreferrer"
352
- aria-hidden="true"
353
- onclick={(e) => onExternalClick(e, item.url)}
354
- >
355
- {@html ExternalIcon}
356
- </a>
357
- <span class="omni-item-chevron" aria-hidden="true">
358
- {@html ChevronIcon}
359
- </span>
360
495
  </button>
361
496
  {/each}
362
497
  </div>
@@ -377,28 +512,55 @@
377
512
  <div class="omni-footer-inner">
378
513
  <div class="omni-footer-hints">
379
514
  <span class="omni-footer-hint">
380
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
381
- <polyline points="9 18 15 12 9 6" />
515
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
516
+ <path d="m9 10-5 5 5 5" />
517
+ <path d="M20 4v7a4 4 0 0 1-4 4H4" />
382
518
  </svg>
383
519
  to select
384
520
  </span>
385
521
  <span class="omni-footer-hint">
386
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
387
- <path d="M18 15l-6-6-6 6" />
522
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
523
+ <path d="m18 15-6-6-6 6" />
388
524
  </svg>
389
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
390
- <path d="M6 9l6 6 6-6" />
525
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
526
+ <path d="m6 9 6 6 6-6" />
391
527
  </svg>
392
528
  to navigate
393
529
  </span>
394
530
  <span class="omni-footer-hint omni-footer-hint-desktop">
395
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
396
- <path d="M18 6 6 18" />
397
- <path d="m6 6 12 12" />
398
- </svg>
531
+ <span class="omni-kbd-sm">ESC</span>
399
532
  to close
400
533
  </span>
401
534
  </div>
535
+ <div class="omni-footer-filter">
536
+ <span class="omni-filter-label">Filter</span>
537
+ <button
538
+ type="button"
539
+ class="omni-filter-button"
540
+ aria-expanded={filterOpen}
541
+ onclick={() => (filterOpen = !filterOpen)}
542
+ >
543
+ {FILTER_LABELS[filter]}
544
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
545
+ <path d="M6 9l6 6 6-6" />
546
+ </svg>
547
+ </button>
548
+ {#if filterOpen}
549
+ <div class="omni-filter-menu" role="group" aria-label="Search filter">
550
+ {#each FILTER_OPTIONS as option}
551
+ <button
552
+ type="button"
553
+ aria-pressed={filter === option}
554
+ class="omni-filter-option"
555
+ class:omni-filter-option-active={filter === option}
556
+ onclick={() => updateFilter(option)}
557
+ >
558
+ {FILTER_LABELS[option]}
559
+ </button>
560
+ {/each}
561
+ </div>
562
+ {/if}
563
+ </div>
402
564
  </div>
403
565
  </div>
404
566
  </div>
@@ -622,6 +622,7 @@ body:has(#nd-docs-layout.dark),
622
622
  }
623
623
 
624
624
  #nd-docs-layout .omni-group-label {
625
+ display: block !important;
625
626
  letter-spacing: 0.14em;
626
627
  color: color-mix(in srgb, var(--color-fd-foreground) 72%, transparent) !important;
627
628
  }
@@ -622,6 +622,7 @@ body:has(#nd-docs-layout.dark),
622
622
  }
623
623
 
624
624
  #nd-docs-layout .omni-group-label {
625
+ display: block !important;
625
626
  letter-spacing: 0.14em;
626
627
  color: color-mix(in srgb, var(--color-fd-foreground) 72%, transparent) !important;
627
628
  }
package/styles/omni.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /* ═══════════════════════════════════════════════════════════════════
2
- * Omni Command Palette — pixel-perfect sync with website/components/ui/omni-command-palette.tsx
3
- * Uses same CSS variables as docs so it auto-adapts to every theme.
2
+ * Omni Command Palette — core styles
3
+ * Uses fumadocs CSS variables so it auto-adapts to every theme.
4
4
  * ═══════════════════════════════════════════════════════════════════ */
5
5
 
6
6
  @keyframes omni-fade-in {
@@ -22,7 +22,7 @@
22
22
  @keyframes omni-scale-in {
23
23
  from {
24
24
  opacity: 0;
25
- transform: translateX(-50%) scale(0.96) translateY(-4px);
25
+ transform: translateX(-50%) scale(0.98) translateY(-6px);
26
26
  }
27
27
  to {
28
28
  opacity: 1;
@@ -36,7 +36,7 @@
36
36
  }
37
37
  to {
38
38
  opacity: 0;
39
- transform: translateX(-50%) scale(0.96) translateY(-4px);
39
+ transform: translateX(-50%) scale(0.98) translateY(-6px);
40
40
  }
41
41
  }
42
42
 
@@ -49,150 +49,172 @@
49
49
  }
50
50
  }
51
51
 
52
- #nd-docs-layout .omni-spin {
52
+ .omni-spin {
53
53
  animation: omni-spin 1s linear infinite;
54
54
  }
55
55
 
56
- #nd-docs-layout .omni-overlay {
56
+ .omni-overlay {
57
57
  position: fixed;
58
58
  inset: 0;
59
- z-index: 100;
60
- background: rgba(0, 0, 0, 0.55);
61
- backdrop-filter: blur(4px);
59
+ z-index: 50;
60
+ background: rgba(0, 0, 0, 0.2);
61
+ backdrop-filter: blur(2px);
62
62
  animation: omni-fade-in 150ms ease-out;
63
63
  }
64
64
 
65
- #nd-docs-layout .omni-content {
65
+ .omni-content {
66
+ --omni-content-top: clamp(5rem, 16vh, 7rem);
66
67
  position: fixed;
67
- z-index: 101;
68
- top: 18%;
68
+ z-index: 51;
69
+ top: var(--omni-content-top);
69
70
  left: 50%;
70
71
  transform: translateX(-50%);
71
- width: min(720px, calc(100% - 32px));
72
- border-radius: var(--radius, 0.75rem);
73
- border: 1px solid var(--color-fd-border, #262626);
74
- background: var(--color-fd-popover, #0c0c0c);
75
- color: var(--color-fd-foreground, #fafafa);
76
- box-shadow:
77
- 0 24px 60px -12px rgba(0, 0, 0, 0.5),
78
- 0 0 0 1px rgba(255, 255, 255, 0.04);
72
+ display: flex;
73
+ flex-direction: column;
74
+ width: min(720px, calc(100% - 1rem));
75
+ border-radius: 0.75rem;
76
+ border: 1px solid color-mix(in srgb, var(--color-fd-border, #d4d4d4) 70%, transparent);
77
+ background: var(--color-fd-popover, #fafafa);
78
+ color: var(--color-fd-popover-foreground, var(--color-fd-foreground, #272727));
79
+ font-family: var(--font-sans, system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text",
80
+ ui-sans-serif, sans-serif);
81
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
79
82
  outline: none;
80
- overflow: hidden;
83
+ overscroll-behavior: contain;
81
84
  animation: omni-scale-in 200ms cubic-bezier(0.16, 1, 0.3, 1);
82
85
  }
83
86
 
84
87
  @media (max-width: 639px) {
85
- #nd-docs-layout .omni-content {
86
- top: 10%;
87
- width: calc(100% - 24px);
88
- max-height: 75vh;
89
- }
90
- #nd-docs-layout .omni-kbd {
91
- display: none;
88
+ .omni-content {
89
+ --omni-content-top: 1rem;
90
+ top: var(--omni-content-top);
91
+ width: calc(100% - 1rem);
92
+ max-height: calc(100vh - 2rem);
92
93
  }
93
94
  }
94
95
 
95
96
  /* Header */
96
- #nd-docs-layout .omni-header {
97
+ .omni-header {
97
98
  border-bottom: 1px solid var(--color-fd-border, #262626);
98
99
  }
99
- #nd-docs-layout .omni-search-row {
100
+ .omni-search-row {
100
101
  display: flex;
101
102
  align-items: center;
102
103
  gap: 0.5rem;
103
- padding: 0.625rem 0.875rem;
104
+ padding: 0.75rem;
104
105
  }
105
- #nd-docs-layout .omni-search-icon {
106
+ .omni-search-icon {
106
107
  color: var(--color-fd-muted-foreground, #a3a3a3);
107
108
  flex-shrink: 0;
108
109
  }
109
- #nd-docs-layout .omni-search-input {
110
+ .omni-search-icon svg {
111
+ width: 1.25rem;
112
+ height: 1.25rem;
113
+ }
114
+ .omni-search-input {
115
+ width: 0;
110
116
  flex: 1;
111
117
  background: transparent;
112
118
  outline: none;
113
- font-size: 0.875rem;
114
- line-height: 1.25rem;
115
- color: var(--color-fd-foreground, #fafafa);
119
+ font-size: 1.125rem;
120
+ line-height: 1.75rem;
121
+ color: var(--color-fd-popover-foreground, var(--color-fd-foreground, #272727));
116
122
  border: none;
117
123
  }
118
- #nd-docs-layout .omni-search-input::placeholder {
124
+ .omni-search-input::placeholder {
119
125
  color: var(--color-fd-muted-foreground, #a3a3a3);
120
126
  }
121
- #nd-docs-layout .omni-kbd {
122
- border-radius: 0.25rem;
123
- background: var(--color-fd-muted, #262626);
124
- padding: 0.125rem 0.375rem;
125
- font-size: 10px;
127
+ .omni-kbd {
128
+ border-radius: 0.375rem;
129
+ border: 1px solid var(--color-fd-border, #d4d4d4);
130
+ background: transparent;
131
+ padding: 0.375rem 0.5rem;
132
+ font-size: 0.75rem;
126
133
  color: var(--color-fd-muted-foreground, #a3a3a3);
127
- font-family: inherit;
128
- line-height: 1.4;
134
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
135
+ "Liberation Mono", "Courier New", monospace);
136
+ line-height: 1rem;
129
137
  }
130
- #nd-docs-layout .omni-close-btn {
131
- margin-left: 0.25rem;
132
- border-radius: 0.25rem;
133
- padding: 0.25rem;
138
+ .omni-close-btn {
139
+ display: inline-flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ gap: 0.25rem;
143
+ min-width: 2.5rem;
144
+ border-radius: 0.375rem;
145
+ border: 1px solid var(--color-fd-border, #d4d4d4);
146
+ padding: 0.375rem 0.5rem;
134
147
  color: var(--color-fd-muted-foreground, #a3a3a3);
135
- transition: background 120ms;
148
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
149
+ "Liberation Mono", "Courier New", monospace);
150
+ font-size: 0.75rem;
151
+ line-height: 1rem;
152
+ font-weight: 500;
153
+ transition:
154
+ background 120ms,
155
+ color 120ms;
136
156
  cursor: pointer;
137
- border: none;
138
157
  background: none;
139
158
  }
140
- #nd-docs-layout .omni-close-btn:hover {
141
- background: var(--color-fd-muted, #262626);
159
+ .omni-close-btn:hover {
160
+ color: var(--color-fd-accent-foreground, var(--color-fd-foreground, #171717));
161
+ background: var(--color-fd-accent, #e5e5e5);
162
+ }
163
+ .omni-close-btn svg {
164
+ display: none;
142
165
  }
143
166
 
144
167
  /* Body / listbox */
145
- #nd-docs-layout .omni-body {
146
- max-height: 60vh;
168
+ .omni-body {
169
+ flex: 1 1 auto;
170
+ min-height: 0;
171
+ max-height: min(540px, calc(100vh - var(--omni-content-top, 7rem) - 8rem));
147
172
  overflow: auto;
148
- padding: 0.25rem;
173
+ overscroll-behavior: contain;
174
+ padding: 0.5rem;
149
175
  }
150
- #nd-docs-layout .omni-body::-webkit-scrollbar {
176
+ .omni-body::-webkit-scrollbar {
151
177
  width: 6px;
152
178
  }
153
- #nd-docs-layout .omni-body::-webkit-scrollbar-track {
179
+ .omni-body::-webkit-scrollbar-track {
154
180
  background: transparent;
155
181
  }
156
- #nd-docs-layout .omni-body::-webkit-scrollbar-thumb {
182
+ .omni-body::-webkit-scrollbar-thumb {
157
183
  background: var(--color-fd-muted, #262626);
158
184
  border-radius: 3px;
159
185
  }
160
186
 
161
187
  /* Loading */
162
- #nd-docs-layout .omni-loading {
188
+ .omni-loading {
163
189
  display: flex;
164
190
  align-items: center;
165
191
  gap: 0.5rem;
166
- padding: 0.5rem 0.75rem;
192
+ padding: 0.75rem 0.625rem;
167
193
  color: var(--color-fd-muted-foreground, #a3a3a3);
168
- font-size: 0.75rem;
194
+ font-size: 0.875rem;
169
195
  }
170
196
 
171
197
  /* Groups */
172
- #nd-docs-layout .omni-group {
173
- padding: 0.25rem 0;
198
+ .omni-group {
199
+ padding: 0;
174
200
  }
175
- #nd-docs-layout .omni-group-label {
176
- padding: 0.25rem 0.75rem;
177
- font-size: 10px;
178
- text-transform: uppercase;
179
- letter-spacing: 0.06em;
180
- color: var(--color-fd-muted-foreground, #a3a3a3);
181
- font-weight: 500;
201
+ .omni-group-label {
202
+ display: none;
182
203
  }
183
- #nd-docs-layout .omni-group-items {
204
+ .omni-group-items {
184
205
  display: flex;
185
206
  flex-direction: column;
207
+ gap: 0.25rem;
186
208
  }
187
209
 
188
210
  /* Items */
189
- #nd-docs-layout .omni-item {
190
- display: flex;
211
+ .omni-item {
212
+ position: relative;
213
+ display: block;
191
214
  width: 100%;
192
- align-items: center;
193
- gap: 0.75rem;
194
- border-radius: calc(var(--radius, 0.75rem) - 4px);
195
- padding: 0.5rem 0.75rem;
215
+ flex-shrink: 0;
216
+ border-radius: 0.5rem;
217
+ padding: 0.75rem 0.875rem;
196
218
  text-align: left;
197
219
  font-size: 0.875rem;
198
220
  line-height: 1.25rem;
@@ -202,73 +224,75 @@
202
224
  color: var(--color-fd-foreground, #fafafa);
203
225
  transition: background 80ms;
204
226
  }
205
- #nd-docs-layout .omni-item:hover,
206
- #nd-docs-layout .omni-item-active {
207
- background: color-mix(in srgb, var(--color-fd-accent, #262626) 60%, transparent);
227
+ .omni-item:hover,
228
+ .omni-item-active {
229
+ background: var(--color-fd-accent, rgba(209, 209, 209, 0.5));
208
230
  }
209
- #nd-docs-layout .omni-item-active {
210
- color: var(--color-fd-accent-foreground, #fafafa);
231
+ .omni-item-active {
232
+ color: var(--color-fd-accent-foreground, #171717);
211
233
  }
212
- #nd-docs-layout .omni-item-disabled {
234
+ .omni-item-disabled {
213
235
  opacity: 0.5;
214
236
  cursor: not-allowed;
215
237
  }
216
238
 
217
- #nd-docs-layout .omni-item-icon {
218
- flex-shrink: 0;
219
- color: var(--color-fd-muted-foreground, #a3a3a3);
239
+ .omni-item-icon {
240
+ display: none;
220
241
  }
221
- #nd-docs-layout .omni-item-active .omni-item-icon {
242
+ .omni-item-active .omni-item-icon {
222
243
  color: var(--color-fd-accent-foreground, #fafafa);
223
244
  }
224
- #nd-docs-layout .omni-item-text {
225
- flex: 1;
245
+ .omni-item-text {
226
246
  min-width: 0;
227
247
  }
228
- #nd-docs-layout .omni-item-label {
248
+ .omni-item-label {
249
+ min-width: 0;
229
250
  overflow: hidden;
230
251
  text-overflow: ellipsis;
231
252
  white-space: nowrap;
253
+ color: var(--color-fd-popover-foreground, var(--color-fd-foreground, #272727));
254
+ font-weight: 500;
255
+ line-height: 1.35;
232
256
  }
233
- #nd-docs-layout .omni-item-subtitle {
257
+ .omni-item-subtitle {
258
+ min-width: 0;
234
259
  overflow: hidden;
235
260
  text-overflow: ellipsis;
236
261
  white-space: nowrap;
237
262
  font-size: 0.75rem;
263
+ line-height: 1rem;
238
264
  color: var(--color-fd-muted-foreground, #a3a3a3);
239
- opacity: 0.8;
265
+ opacity: 1;
240
266
  }
241
- #nd-docs-layout .omni-item-active .omni-item-subtitle {
242
- color: var(--color-fd-accent-foreground, #fafafa);
243
- opacity: 0.7;
267
+ .omni-item-active .omni-item-subtitle {
268
+ color: var(--color-fd-muted-foreground, #737373);
269
+ opacity: 1;
244
270
  }
245
- #nd-docs-layout .omni-item-badge {
246
- color: var(--color-fd-muted-foreground, #a3a3a3);
247
- flex-shrink: 0;
271
+ .omni-item-description {
272
+ min-width: 0;
273
+ overflow: hidden;
274
+ display: -webkit-box;
275
+ -webkit-line-clamp: 3;
276
+ -webkit-box-orient: vertical;
277
+ margin-top: 0.625rem;
278
+ padding-left: 0.75rem;
279
+ border-left: 1px solid color-mix(in srgb, var(--color-fd-border, #d4d4d4) 70%, transparent);
280
+ color: color-mix(in srgb, var(--color-fd-popover-foreground, #272727) 82%, transparent);
281
+ font-size: 0.875rem;
282
+ line-height: 1.5rem;
283
+ font-weight: 400;
248
284
  }
249
- #nd-docs-layout a.omni-item-badge:hover {
250
- color: var(--color-fd-foreground, #fafafa);
285
+ .omni-item-badge {
286
+ display: none;
251
287
  }
252
- #nd-docs-layout .omni-item-ext {
253
- position: relative;
254
- z-index: 2;
255
- display: inline-flex;
256
- align-items: center;
257
- justify-content: center;
258
- padding: 0.25rem;
259
- border-radius: 0.25rem;
260
- color: var(--color-fd-muted-foreground, #a3a3a3);
261
- flex-shrink: 0;
262
- transition:
263
- color 100ms,
264
- background 100ms;
265
- text-decoration: none;
288
+ .omni-item-ext {
289
+ display: none;
266
290
  }
267
- #nd-docs-layout .omni-item-ext:hover {
291
+ .omni-item-ext:hover {
268
292
  color: var(--color-fd-foreground, #fafafa);
269
293
  background: var(--color-fd-muted, #262626);
270
294
  }
271
- #nd-docs-layout .omni-item-shortcuts {
295
+ .omni-item-shortcuts {
272
296
  display: none;
273
297
  align-items: center;
274
298
  gap: 0.25rem;
@@ -276,50 +300,43 @@
276
300
  color: var(--color-fd-muted-foreground, #a3a3a3);
277
301
  }
278
302
  @media (min-width: 640px) {
279
- #nd-docs-layout .omni-item-shortcuts {
303
+ .omni-item-shortcuts {
280
304
  display: flex;
281
305
  }
282
306
  }
283
- #nd-docs-layout .omni-kbd-sm {
307
+ .omni-kbd-sm {
284
308
  border-radius: 0.25rem;
285
309
  background: var(--color-fd-muted, #262626);
286
310
  padding: 0.125rem 0.25rem;
287
- font-family: inherit;
311
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
312
+ "Liberation Mono", "Courier New", monospace);
288
313
  }
289
- #nd-docs-layout .omni-item-chevron {
290
- width: 0.875rem;
291
- height: 0.875rem;
292
- margin-left: 0.25rem;
293
- color: var(--color-fd-muted-foreground, #a3a3a3);
294
- opacity: 0;
295
- transition: opacity 120ms;
296
- flex-shrink: 0;
297
- }
298
- #nd-docs-layout .omni-item:hover .omni-item-chevron {
299
- opacity: 1;
314
+ .omni-item-chevron {
315
+ display: none;
300
316
  }
301
-
302
317
  /* Highlight */
303
- #nd-docs-layout .omni-highlight {
304
- border-radius: 2px;
305
- background: color-mix(in srgb, var(--color-fd-primary, #6366f1) 30%, transparent);
306
- padding: 0 2px;
307
- color: inherit;
318
+ .omni-highlight {
319
+ border-radius: 0;
320
+ background: transparent;
321
+ padding: 0;
322
+ color: var(--color-fd-primary, #ca8a04);
323
+ text-decoration: underline;
324
+ text-underline-offset: 2px;
308
325
  }
309
326
 
310
327
  /* Empty states */
311
- #nd-docs-layout .omni-empty-group {
328
+ .omni-empty-group {
312
329
  padding: 0.5rem 0.75rem;
313
330
  font-size: 0.75rem;
314
331
  color: var(--color-fd-muted-foreground, #a3a3a3);
315
332
  }
316
- #nd-docs-layout .omni-empty {
317
- padding: 2rem 0.75rem;
333
+ .omni-empty {
334
+ padding: 1.5rem 0.75rem;
318
335
  text-align: center;
319
336
  font-size: 0.875rem;
320
337
  color: var(--color-fd-muted-foreground, #a3a3a3);
321
338
  }
322
- #nd-docs-layout .omni-empty-icon {
339
+ .omni-empty-icon {
323
340
  width: 2rem;
324
341
  height: 2rem;
325
342
  margin: 0 auto 0.5rem;
@@ -331,32 +348,107 @@
331
348
  }
332
349
 
333
350
  /* Footer */
334
- #nd-docs-layout .omni-footer {
351
+ .omni-footer {
335
352
  border-top: 1px solid var(--color-fd-border, #262626);
353
+ background: color-mix(in srgb, var(--color-fd-secondary, #f5f5f5) 50%, transparent);
336
354
  }
337
- #nd-docs-layout .omni-footer-inner {
355
+ .omni-footer-inner {
338
356
  display: flex;
339
357
  align-items: center;
340
358
  justify-content: space-between;
341
- padding: 0.5rem 0.75rem;
359
+ gap: 0.75rem;
360
+ padding: 0.625rem 0.75rem;
342
361
  font-size: 0.75rem;
343
362
  color: var(--color-fd-muted-foreground, #a3a3a3);
344
363
  }
345
- #nd-docs-layout .omni-footer-hints {
364
+ .omni-footer-hints {
346
365
  display: flex;
347
366
  align-items: center;
348
- gap: 1rem;
367
+ flex-wrap: wrap;
368
+ gap: 0.75rem;
349
369
  }
350
- #nd-docs-layout .omni-footer-hint {
370
+ .omni-footer-hint {
351
371
  display: flex;
352
372
  align-items: center;
353
373
  gap: 0.25rem;
374
+ white-space: nowrap;
375
+ }
376
+ .omni-footer-hint svg {
377
+ box-sizing: border-box;
378
+ width: 1.375rem;
379
+ height: 1.375rem;
380
+ padding: 0.25rem;
381
+ border-radius: 0.375rem;
382
+ border: 1px solid var(--color-fd-border, #d4d4d4);
383
+ background: color-mix(in srgb, var(--color-fd-muted, #e5e5e5) 70%, transparent);
384
+ color: var(--color-fd-muted-foreground, #737373);
385
+ flex-shrink: 0;
354
386
  }
355
- #nd-docs-layout .omni-footer-hint-desktop {
387
+ .omni-footer-hint-desktop {
356
388
  display: none;
357
389
  }
390
+ .omni-footer-filter {
391
+ position: relative;
392
+ display: inline-flex;
393
+ align-items: center;
394
+ gap: 0.5rem;
395
+ margin-left: auto;
396
+ white-space: nowrap;
397
+ }
358
398
  @media (min-width: 640px) {
359
- #nd-docs-layout .omni-footer-hint-desktop {
399
+ .omni-footer-hint-desktop {
360
400
  display: flex;
361
401
  }
362
402
  }
403
+ .omni-filter-label {
404
+ color: var(--color-fd-muted-foreground, #a3a3a3);
405
+ }
406
+ .omni-filter-value {
407
+ display: inline-flex;
408
+ align-items: center;
409
+ gap: 0.25rem;
410
+ color: var(--color-fd-popover-foreground, var(--color-fd-foreground, #272727));
411
+ }
412
+ .omni-filter-button {
413
+ display: inline-flex;
414
+ align-items: center;
415
+ gap: 0.25rem;
416
+ border: 0;
417
+ background: transparent;
418
+ color: var(--color-fd-popover-foreground, var(--color-fd-foreground, #272727));
419
+ font: inherit;
420
+ cursor: pointer;
421
+ padding: 0;
422
+ }
423
+ .omni-filter-button:hover {
424
+ color: var(--color-fd-accent-foreground, var(--color-fd-foreground, #171717));
425
+ }
426
+ .omni-filter-menu {
427
+ position: absolute;
428
+ right: 0;
429
+ bottom: calc(100% + 0.5rem);
430
+ z-index: 1;
431
+ width: 10rem;
432
+ border: 1px solid var(--color-fd-border, #d4d4d4);
433
+ border-radius: 0.5rem;
434
+ background: var(--color-fd-popover, #fafafa);
435
+ padding: 0.25rem;
436
+ box-shadow: 0 16px 32px -20px rgba(0, 0, 0, 0.45);
437
+ }
438
+ .omni-filter-option {
439
+ display: flex;
440
+ width: 100%;
441
+ align-items: center;
442
+ border: 0;
443
+ border-radius: 0.375rem;
444
+ background: transparent;
445
+ color: var(--color-fd-popover-foreground, var(--color-fd-foreground, #272727));
446
+ cursor: pointer;
447
+ font: inherit;
448
+ padding: 0.5rem 0.625rem;
449
+ text-align: left;
450
+ }
451
+ .omni-filter-option:hover,
452
+ .omni-filter-option-active {
453
+ background: var(--color-fd-accent, rgba(209, 209, 209, 0.5));
454
+ }
@@ -844,7 +844,7 @@ body:has(#nd-docs-layout),
844
844
  }
845
845
 
846
846
  #nd-docs-layout .omni-search-input {
847
- font-family: var(--fd-font-mono, ui-monospace, monospace) !important;
847
+ font-family: var(--fd-font-sans, system-ui, -apple-system, sans-serif) !important;
848
848
  }
849
849
 
850
850
  #nd-docs-layout .omni-group-label {
@@ -856,6 +856,15 @@ body:has(#nd-docs-layout),
856
856
  font-family: var(--fd-font-mono, ui-monospace, monospace) !important;
857
857
  }
858
858
 
859
+ #nd-docs-layout .omni-footer-hint svg {
860
+ border-radius: 0 !important;
861
+ }
862
+
863
+ #nd-docs-layout .omni-filter-menu,
864
+ #nd-docs-layout .omni-filter-option {
865
+ border-radius: 0 !important;
866
+ }
867
+
859
868
  #nd-docs-layout .omni-highlight {
860
869
  background: color-mix(in srgb, var(--color-fd-primary, #6366f1) 25%, transparent) !important;
861
870
  border-radius: 0 !important;