@farming-labs/svelte-theme 0.0.3-beta.2 → 0.0.3-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,91 +1,390 @@
1
1
  <script>
2
2
  /**
3
- * SearchDialogCmd+K search overlay.
4
- * Fetches from /api/docs?query=… (same unified handler as Next.js version).
3
+ * Omni command palette pixel-perfect default for SvelteKit, aligned with
4
+ * website/components/ui/omni-command-palette.tsx. Recents when empty,
5
+ * /api/docs search, keyboard nav, same copy and structure.
5
6
  */
6
- import { onMount } from "svelte";
7
+ import { onMount, onDestroy, tick } from "svelte";
7
8
  import { goto } from "$app/navigation";
8
9
 
10
+ const STORAGE_KEY = "fd:omni:recents";
11
+ const MAX_RECENTS = 8;
12
+ const DEBOUNCE_MS = 120;
13
+ const PLACEHOLDER = "Search documentation…";
14
+
9
15
  let { onclose } = $props();
16
+
10
17
  let query = $state("");
11
- let results = $state([]);
18
+ let currentResults = $state([]);
12
19
  let loading = $state(false);
13
- let inputEl;
14
- let debounceTimer;
20
+ let activeId = $state(null);
21
+ let recentsList = $state([]);
22
+ let inputEl = $state(null);
23
+ let listRef = $state(null);
24
+ let debounceTimer = null;
15
25
 
16
- onMount(() => {
17
- inputEl?.focus();
26
+ let flatItems = $derived.by(() => {
27
+ const q = query.trim();
28
+ if (q && currentResults.length) {
29
+ return currentResults.map((r) => ({
30
+ id: r.url,
31
+ label: r.content,
32
+ url: r.url,
33
+ subtitle: r.description ?? "Page",
34
+ }));
35
+ }
36
+ return recentsList.map((r) => ({
37
+ id: r.id,
38
+ label: r.label,
39
+ url: r.url,
40
+ subtitle: "Recently used",
41
+ }));
18
42
  });
19
43
 
20
- $effect(() => {
21
- clearTimeout(debounceTimer);
22
- if (!query.trim()) {
23
- results = [];
24
- return;
44
+ let showRecents = $derived(!query.trim());
45
+ let showDocs = $derived(!!query.trim() && currentResults.length > 0);
46
+ let showEmpty = $derived(
47
+ query.trim() ? currentResults.length === 0 && !loading : recentsList.length === 0
48
+ );
49
+ let emptyText = $derived(
50
+ query.trim()
51
+ ? "No results found. Try a different query."
52
+ : "Type to search the docs, or browse recent items."
53
+ );
54
+
55
+ function getRecents() {
56
+ if (typeof document === "undefined" || typeof localStorage === "undefined") return [];
57
+ try {
58
+ const raw = localStorage.getItem(STORAGE_KEY);
59
+ return raw ? JSON.parse(raw) : [];
60
+ } catch {
61
+ return [];
62
+ }
63
+ }
64
+
65
+ function saveRecent(entry) {
66
+ if (typeof localStorage === "undefined") return;
67
+ try {
68
+ const recents = getRecents();
69
+ const next = [entry, ...recents.filter((r) => r.id !== entry.id)].slice(0, MAX_RECENTS);
70
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
71
+ } catch {}
72
+ }
73
+
74
+ function loadRecents() {
75
+ recentsList = getRecents();
76
+ }
77
+
78
+ function close() {
79
+ if (typeof document !== "undefined") document.body.style.overflow = "";
80
+ onclose?.();
81
+ }
82
+
83
+ function executeItem(item) {
84
+ saveRecent({ id: item.url, label: item.label ?? item.content ?? item.url, url: item.url });
85
+ if (item.url.startsWith("http")) {
86
+ if (typeof window !== "undefined") window.open(item.url, "_blank", "noopener,noreferrer");
87
+ } else {
88
+ goto(item.url);
25
89
  }
26
- loading = true;
90
+ close();
91
+ }
92
+
93
+ function moveActive(delta) {
94
+ if (!flatItems.length) return;
95
+ const idx = flatItems.findIndex((i) => i.id === activeId);
96
+ const nextIdx = idx < 0 ? 0 : (((idx + delta) % flatItems.length) + flatItems.length) % flatItems.length;
97
+ activeId = flatItems[nextIdx].id;
98
+ scrollActiveIntoView();
99
+ }
100
+
101
+ function executeActive() {
102
+ const idx = activeId != null ? flatItems.findIndex((i) => i.id === activeId) : 0;
103
+ const item = flatItems[idx ?? 0];
104
+ if (item) executeItem(item);
105
+ }
106
+
107
+ function scrollActiveIntoView() {
108
+ tick().then(() => {
109
+ if (typeof document === "undefined" || !listRef) return;
110
+ const node = listRef.querySelector(`[data-id="${activeId}"]`);
111
+ node?.scrollIntoView({ block: "nearest" });
112
+ });
113
+ }
114
+
115
+ function onInput() {
116
+ loadRecents();
117
+ loading = false;
118
+ currentResults = [];
119
+ activeId = null;
120
+ const q = query.trim();
121
+ if (!q) return;
122
+ if (debounceTimer) clearTimeout(debounceTimer);
27
123
  debounceTimer = setTimeout(async () => {
124
+ loading = true;
28
125
  try {
29
- const res = await fetch(`/api/docs?query=${encodeURIComponent(query)}`);
30
- if (res.ok) {
31
- const data = await res.json();
32
- results = data ?? [];
33
- }
126
+ const res = await fetch(`/api/docs?query=${encodeURIComponent(q)}`);
127
+ const data = res.ok ? await res.json() : [];
128
+ currentResults = Array.isArray(data) ? data : [];
129
+ activeId = currentResults[0]?.url ?? null;
34
130
  } catch {
35
- results = [];
131
+ currentResults = [];
36
132
  } finally {
37
133
  loading = false;
38
134
  }
39
- }, 200);
40
- });
41
-
42
- function navigate(url) {
43
- onclose?.();
44
- goto(url);
135
+ }, DEBOUNCE_MS);
45
136
  }
46
137
 
138
+ $effect(() => {
139
+ void query;
140
+ onInput();
141
+ });
142
+
47
143
  function handleKeydown(e) {
48
144
  if (e.key === "Escape") {
49
- onclose?.();
145
+ e.preventDefault();
146
+ close();
147
+ return;
148
+ }
149
+ if (e.key === "ArrowDown") {
150
+ e.preventDefault();
151
+ moveActive(1);
152
+ return;
153
+ }
154
+ if (e.key === "ArrowUp") {
155
+ e.preventDefault();
156
+ moveActive(-1);
157
+ return;
158
+ }
159
+ if (e.key === "Enter") {
160
+ e.preventDefault();
161
+ executeActive();
162
+ }
163
+ }
164
+
165
+ function onOverlayClick() {
166
+ close();
167
+ }
168
+
169
+ function onContentClick(e) {
170
+ e.stopPropagation();
171
+ }
172
+
173
+ function onExternalClick(e, url) {
174
+ e.preventDefault();
175
+ e.stopPropagation();
176
+ try {
177
+ if (typeof window !== "undefined") window.open(url, "_blank", "noopener,noreferrer");
178
+ } catch {
179
+ if (typeof window !== "undefined") window.location.href = url;
50
180
  }
51
181
  }
182
+
183
+ 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>`;
184
+ 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>`;
185
+ 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>`;
186
+ 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>`;
187
+
188
+ onMount(() => {
189
+ loadRecents();
190
+ if (typeof document !== "undefined") document.body.style.overflow = "hidden";
191
+ tick().then(() => inputEl?.focus());
192
+ });
193
+
194
+ onDestroy(() => {
195
+ if (typeof document !== "undefined") document.body.style.overflow = "";
196
+ if (debounceTimer) clearTimeout(debounceTimer);
197
+ });
52
198
  </script>
53
199
 
200
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
54
201
  <!-- svelte-ignore a11y_no_static_element_interactions -->
55
- <div class="fd-search-overlay" onclick={onclose} onkeydown={handleKeydown} role="dialog">
202
+ <div
203
+ class="omni-overlay"
204
+ aria-hidden="true"
205
+ role="presentation"
206
+ onclick={onOverlayClick}
207
+ onkeydown={handleKeydown}
208
+ >
56
209
  <!-- svelte-ignore a11y_click_events_have_key_events -->
57
210
  <!-- svelte-ignore a11y_no_static_element_interactions -->
58
- <div class="fd-search-dialog" onclick={(e) => e.stopPropagation()} role="document">
59
- <div class="fd-search-input-wrap">
60
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
61
- <circle cx="11" cy="11" r="8" />
62
- <line x1="21" y1="21" x2="16.65" y2="16.65" />
63
- </svg>
64
- <input
65
- bind:this={inputEl}
66
- bind:value={query}
67
- class="fd-search-input"
68
- placeholder="Search documentation..."
69
- type="text"
70
- />
71
- <kbd class="fd-search-kbd">ESC</kbd>
211
+ <div
212
+ class="omni-content"
213
+ role="dialog"
214
+ aria-label="Command palette"
215
+ tabindex="-1"
216
+ onclick={onContentClick}
217
+ >
218
+ <div class="omni-header">
219
+ <div class="omni-search-row">
220
+ <span class="omni-search-icon" aria-hidden="true">
221
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
222
+ <circle cx="11" cy="11" r="8" />
223
+ <path d="m21 21-4.3-4.3" />
224
+ </svg>
225
+ </span>
226
+ <input
227
+ bind:this={inputEl}
228
+ bind:value={query}
229
+ type="text"
230
+ class="omni-search-input"
231
+ role="combobox"
232
+ aria-expanded="true"
233
+ aria-controls="omni-listbox"
234
+ aria-activedescendant={activeId ? `omni-item-${activeId}` : undefined}
235
+ placeholder={PLACEHOLDER}
236
+ autocomplete="off"
237
+ onkeydown={handleKeydown}
238
+ />
239
+ <kbd class="omni-kbd">⌘K</kbd>
240
+ <button type="button" aria-label="Close" class="omni-close-btn" onclick={close}>
241
+ <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">
242
+ <path d="M18 6 6 18" />
243
+ <path d="m6 6 12 12" />
244
+ </svg>
245
+ </button>
246
+ </div>
72
247
  </div>
73
248
 
74
- <div class="fd-search-results">
249
+ <div
250
+ bind:this={listRef}
251
+ id="omni-listbox"
252
+ class="omni-body"
253
+ role="listbox"
254
+ aria-label="Command results"
255
+ >
75
256
  {#if loading}
76
- <div class="fd-search-empty">Searching...</div>
77
- {:else if query && results.length === 0}
78
- <div class="fd-search-empty">No results found for "{query}"</div>
79
- {:else}
80
- {#each results as result}
81
- <button class="fd-search-result" onclick={() => navigate(result.url)}>
82
- <span class="fd-search-result-title">{result.content}</span>
83
- {#if result.url}
84
- <span class="fd-search-result-url">{result.url}</span>
85
- {/if}
86
- </button>
87
- {/each}
257
+ <div class="omni-loading">
258
+ <svg class="omni-spin" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
259
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
260
+ </svg>
261
+ Fetching results
262
+ </div>
263
+
264
+ {:else if showRecents && recentsList.length > 0}
265
+ <div class="omni-group">
266
+ <div class="omni-group-label">Recent</div>
267
+ <div class="omni-group-items">
268
+ {#each flatItems as item, i}
269
+ {@const active = item.id === activeId || (activeId == null && i === 0)}
270
+ <button
271
+ type="button"
272
+ id="omni-item-{item.id}"
273
+ data-id={item.id}
274
+ class="omni-item"
275
+ class:omni-item-active={active}
276
+ role="option"
277
+ aria-selected={active}
278
+ onmouseenter={() => (activeId = item.id)}
279
+ onclick={() => executeItem(item)}
280
+ >
281
+ <div class="omni-item-icon">
282
+ {@html FileIcon}
283
+ </div>
284
+ <div class="omni-item-text">
285
+ <div class="omni-item-label">{item.label}</div>
286
+ <div class="omni-item-subtitle">{item.subtitle}</div>
287
+ </div>
288
+ <a
289
+ href={item.url}
290
+ class="omni-item-badge"
291
+ title="Open in new tab"
292
+ target="_blank"
293
+ rel="noopener noreferrer"
294
+ aria-hidden="true"
295
+ onclick={(e) => onExternalClick(e, item.url)}
296
+ >
297
+ {@html ExternalIcon}
298
+ </a>
299
+ <span class="omni-item-chevron" aria-hidden="true">
300
+ {@html ChevronIcon}
301
+ </span>
302
+ </button>
303
+ {/each}
304
+ </div>
305
+ </div>
306
+
307
+ {:else if showDocs}
308
+ <div class="omni-group">
309
+ <div class="omni-group-label">Documentation</div>
310
+ <div class="omni-group-items">
311
+ {#each flatItems as item, i}
312
+ {@const active = item.id === activeId || (activeId == null && i === 0)}
313
+ <button
314
+ type="button"
315
+ id="omni-item-{item.id}"
316
+ data-id={item.id}
317
+ class="omni-item"
318
+ class:omni-item-active={active}
319
+ role="option"
320
+ aria-selected={active}
321
+ onmouseenter={() => (activeId = item.id)}
322
+ onclick={() => executeItem(item)}
323
+ >
324
+ <div class="omni-item-icon">
325
+ {@html FileIcon}
326
+ </div>
327
+ <div class="omni-item-text">
328
+ <div class="omni-item-label">{item.label}</div>
329
+ <div class="omni-item-subtitle">{item.subtitle}</div>
330
+ </div>
331
+ <a
332
+ href={item.url}
333
+ class="omni-item-badge"
334
+ title="Open in new tab"
335
+ target="_blank"
336
+ rel="noopener noreferrer"
337
+ aria-hidden="true"
338
+ onclick={(e) => onExternalClick(e, item.url)}
339
+ >
340
+ {@html ExternalIcon}
341
+ </a>
342
+ <span class="omni-item-chevron" aria-hidden="true">
343
+ {@html ChevronIcon}
344
+ </span>
345
+ </button>
346
+ {/each}
347
+ </div>
348
+ </div>
88
349
  {/if}
350
+
351
+ {#if showEmpty}
352
+ <div class="omni-empty">
353
+ <div class="omni-empty-icon">
354
+ {@html EmptyIcon}
355
+ </div>
356
+ {emptyText}
357
+ </div>
358
+ {/if}
359
+ </div>
360
+
361
+ <div class="omni-footer">
362
+ <div class="omni-footer-inner">
363
+ <div class="omni-footer-hints">
364
+ <span class="omni-footer-hint">
365
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
366
+ <polyline points="9 18 15 12 9 6" />
367
+ </svg>
368
+ to select
369
+ </span>
370
+ <span class="omni-footer-hint">
371
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
372
+ <path d="M18 15l-6-6-6 6" />
373
+ </svg>
374
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
375
+ <path d="M6 9l6 6 6-6" />
376
+ </svg>
377
+ to navigate
378
+ </span>
379
+ <span class="omni-footer-hint omni-footer-hint-desktop">
380
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
381
+ <path d="M18 6 6 18" />
382
+ <path d="m6 6 12 12" />
383
+ </svg>
384
+ to close
385
+ </span>
386
+ </div>
387
+ </div>
89
388
  </div>
90
389
  </div>
91
390
  </div>
@@ -24,7 +24,7 @@ const ColorfulUIDefaults = {
24
24
  layout: {
25
25
  contentWidth: 768,
26
26
  sidebarWidth: 260,
27
- toc: { enabled: true, depth: 3, style: "directional" },
27
+ toc: { enabled: true, depth: 3, style: "default" },
28
28
  header: { height: 56, sticky: true },
29
29
  },
30
30
  components: {
@@ -84,6 +84,57 @@
84
84
  }
85
85
  }
86
86
 
87
+ /* ─── Omni Command Palette — colorful theme ────────────────────────── */
88
+
89
+ .omni-content {
90
+ border-radius: 0.75rem;
91
+ }
92
+
93
+ .omni-item-active {
94
+ background: color-mix(in srgb, var(--color-fd-primary) 15%, transparent);
95
+ }
96
+
97
+ .omni-highlight {
98
+ background: color-mix(in srgb, var(--color-fd-primary) 30%, transparent);
99
+ }
100
+
101
+ .omni-search-input:focus {
102
+ caret-color: var(--color-fd-primary);
103
+ }
104
+
105
+ /* ─── Page Actions — colorful theme ────────────────────────────────── */
106
+
107
+ .fd-page-action-btn {
108
+ border-radius: 0.375rem !important;
109
+ font-size: 0.8125rem !important;
110
+ letter-spacing: normal !important;
111
+ box-shadow: none !important;
112
+ text-transform: capitalize !important;
113
+ font-family: var(--fd-font-sans, ui-sans-serif, system-ui, sans-serif) !important;
114
+ }
115
+
116
+ .fd-page-action-dropdown {
117
+ position: relative !important;
118
+ }
119
+
120
+ .fd-page-action-menu {
121
+ border-radius: 0.5rem !important;
122
+ border: 1px solid var(--color-fd-border) !important;
123
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2) !important;
124
+ background: var(--color-fd-popover) !important;
125
+ }
126
+
127
+ .fd-page-action-menu-item {
128
+ border-radius: 0.25rem !important;
129
+ font-size: 0.8125rem !important;
130
+ color: var(--color-fd-foreground) !important;
131
+ }
132
+
133
+ .fd-page-action-menu-item:hover {
134
+ background: var(--color-fd-accent) !important;
135
+ color: var(--color-fd-accent-foreground) !important;
136
+ }
137
+
87
138
  /* ─── Page nav cards ───────────────────────────────────────────────── */
88
139
 
89
140
  .fd-page-nav-card {
@@ -148,3 +199,24 @@
148
199
  .fd-docs-content hr {
149
200
  border-color: var(--color-fd-border);
150
201
  }
202
+
203
+ /* ─── Ask AI button (floating + custom trigger) — sync with Next.js ───── */
204
+
205
+ .fd-ai-floating-btn {
206
+ border-radius: 26px;
207
+ box-shadow: 0 8px 32px rgba(180, 140, 20, 0.3);
208
+ }
209
+
210
+ .fd-ai-floating-btn:hover {
211
+ box-shadow: 0 10px 40px rgba(180, 140, 20, 0.4);
212
+ }
213
+
214
+ .fd-ai-floating-trigger .ask-ai-trigger {
215
+ font-family: var(--fd-font-sans, inherit);
216
+ border-radius: 26px !important;
217
+ box-shadow: 0 8px 32px rgba(180, 140, 20, 0.3) !important;
218
+ }
219
+
220
+ .fd-ai-floating-trigger .ask-ai-trigger:hover {
221
+ box-shadow: 0 10px 40px rgba(180, 140, 20, 0.4);
222
+ }
@@ -204,3 +204,74 @@ code:not(pre code) {
204
204
  .fd-ai-fm-trigger-btn:hover {
205
205
  transform: none;
206
206
  }
207
+
208
+ /* Custom Ask AI trigger — same as fd-ai-floating-btn for theme */
209
+ .fd-ai-floating-trigger .ask-ai-trigger {
210
+ font-family: var(--fd-font-sans, inherit);
211
+ border-radius: 2px !important;
212
+ }
213
+
214
+ .fd-ai-floating-trigger .ask-ai-trigger:hover {
215
+ box-shadow: 0 0 0 1px var(--color-fd-ring);
216
+ transform: none;
217
+ }
218
+
219
+ .fd-ai-fm-input-bar .fd-ai-floating-trigger .ask-ai-trigger {
220
+ border-radius: 2px !important;
221
+ }
222
+
223
+ .fd-ai-fm-input-bar .fd-ai-floating-trigger .ask-ai-trigger:hover {
224
+ transform: none;
225
+ }
226
+
227
+ /* ─── Omni Command Palette — darksharp theme ────────────────────── */
228
+
229
+ .omni-content {
230
+ border-radius: 0.5rem !important;
231
+ border: 1px solid var(--color-fd-border) !important;
232
+ background: var(--color-fd-popover) !important;
233
+ box-shadow:
234
+ 0 24px 60px -12px rgba(0, 0, 0, 0.7),
235
+ 0 0 0 1px rgba(255, 255, 255, 0.06) !important;
236
+ }
237
+
238
+ .omni-item {
239
+ border-radius: 0.25rem !important;
240
+ }
241
+
242
+ .omni-highlight {
243
+ background: color-mix(in srgb, var(--color-fd-primary) 30%, transparent) !important;
244
+ }
245
+
246
+ /* ─── Page Actions — darksharp theme ────────────────────────────── */
247
+
248
+ .fd-page-action-btn {
249
+ border-radius: 0.2rem !important;
250
+ font-size: 0.8125rem !important;
251
+ letter-spacing: normal !important;
252
+ box-shadow: none !important;
253
+ text-transform: capitalize !important;
254
+ font-family: var(--fd-font-sans, ui-sans-serif, system-ui, sans-serif) !important;
255
+ }
256
+
257
+ .fd-page-action-dropdown {
258
+ position: relative !important;
259
+ }
260
+
261
+ .fd-page-action-menu {
262
+ border-radius: 0.2rem !important;
263
+ border: 1px solid var(--color-fd-border) !important;
264
+ box-shadow: 0 4px 24px hsl(0 0% 0% / 0.4) !important;
265
+ background: var(--color-fd-popover) !important;
266
+ }
267
+
268
+ .fd-page-action-menu-item {
269
+ border-radius: 0.1rem !important;
270
+ font-size: 0.8125rem !important;
271
+ color: var(--color-fd-foreground) !important;
272
+ }
273
+
274
+ .fd-page-action-menu-item:hover {
275
+ background: var(--color-fd-accent) !important;
276
+ color: var(--color-fd-accent-foreground) !important;
277
+ }