@aravindc26/velu 0.12.7 → 0.12.9

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 (74) hide show
  1. package/package.json +1 -1
  2. package/src/build.ts +13 -0
  3. package/src/cli.ts +51 -9
  4. package/src/engine/app/(docs)/[...slug]/layout.tsx +21 -537
  5. package/src/engine/app/_preview/[sessionId]/[...slug]/layout.tsx +96 -0
  6. package/src/engine/app/_preview/[sessionId]/[...slug]/page.tsx +298 -0
  7. package/src/engine/app/_preview/[sessionId]/layout.tsx +56 -0
  8. package/src/{preview-engine/app → engine/app/_preview}/[sessionId]/page.tsx +7 -3
  9. package/src/engine/app/_preview/api/sessions/[sessionId]/assets/[...path]/route.ts +51 -0
  10. package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/init/route.ts +2 -2
  11. package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/route.ts +3 -3
  12. package/src/{preview-engine/app → engine/app/_preview}/api/sessions/[sessionId]/sync/route.ts +2 -2
  13. package/src/{preview-engine/app → engine/app/_preview}/layout.tsx +4 -1
  14. package/src/engine/app/global.css +0 -3623
  15. package/src/engine/app/layout.tsx +4 -3
  16. package/src/engine/components/sidebar-links.tsx +11 -5
  17. package/src/engine/lib/docs-layout.tsx +605 -0
  18. package/src/engine/lib/layout.shared.ts +7 -7
  19. package/src/engine/lib/preview-config.ts +129 -0
  20. package/src/{preview-engine/lib/content-generator.ts → engine/lib/preview-content.ts} +242 -42
  21. package/src/engine/lib/source.ts +80 -97
  22. package/src/engine/lib/velu.ts +79 -55
  23. package/src/engine/mdx-components.tsx +14 -650
  24. package/src/engine/source.config.ts +11 -89
  25. package/src/engine/tsconfig.json +1 -0
  26. package/src/engine-core/components/assistant.tsx +361 -0
  27. package/src/engine-core/components/banner.tsx +80 -0
  28. package/src/engine-core/components/changelog-filters.tsx +114 -0
  29. package/src/engine-core/components/code-group.tsx +383 -0
  30. package/src/engine-core/components/color.tsx +118 -0
  31. package/src/engine-core/components/copy-page.tsx +223 -0
  32. package/src/engine-core/components/dropdown-switcher.tsx +142 -0
  33. package/src/engine-core/components/expandable.tsx +77 -0
  34. package/src/engine-core/components/header-tab-link.tsx +43 -0
  35. package/src/engine-core/components/icon.tsx +136 -0
  36. package/src/engine-core/components/image-zoom-fallback.tsx +147 -0
  37. package/src/engine-core/components/image.tsx +111 -0
  38. package/src/engine-core/components/lang-switcher.tsx +101 -0
  39. package/src/engine-core/components/manual-api-playground.tsx +154 -0
  40. package/src/engine-core/components/mermaid.tsx +142 -0
  41. package/src/engine-core/components/openapi-toc-sync.tsx +59 -0
  42. package/src/engine-core/components/openapi.tsx +1682 -0
  43. package/src/engine-core/components/page-feedback-api.test.ts +83 -0
  44. package/src/engine-core/components/page-feedback-api.ts +89 -0
  45. package/src/engine-core/components/page-feedback.tsx +200 -0
  46. package/src/engine-core/components/product-switcher.tsx +107 -0
  47. package/src/engine-core/components/prompt.tsx +90 -0
  48. package/src/engine-core/components/providers.tsx +21 -0
  49. package/src/engine-core/components/search.tsx +318 -0
  50. package/src/engine-core/components/sidebar-links.tsx +54 -0
  51. package/src/engine-core/components/synced-tabs.tsx +57 -0
  52. package/src/engine-core/components/theme-toggle.tsx +39 -0
  53. package/src/engine-core/components/toc-examples.tsx +110 -0
  54. package/src/engine-core/components/version-switcher.tsx +95 -0
  55. package/src/engine-core/components/view.tsx +344 -0
  56. package/src/engine-core/css/assistant.css +326 -0
  57. package/src/engine-core/css/copy-page.css +206 -0
  58. package/src/engine-core/css/search.css +142 -0
  59. package/src/engine-core/css/shared.css +3628 -0
  60. package/src/engine-core/lib/remark-plugins.ts +102 -0
  61. package/src/engine-core/lib/source-plugins.ts +105 -0
  62. package/src/engine-core/mdx-components.tsx +654 -0
  63. package/src/engine-core/types.ts +49 -0
  64. package/src/preview-engine/app/[sessionId]/[...slug]/page.tsx +0 -41
  65. package/src/preview-engine/app/[sessionId]/layout.tsx +0 -23
  66. package/src/preview-engine/app/global.css +0 -3
  67. package/src/preview-engine/lib/session-config.ts +0 -86
  68. package/src/preview-engine/lib/source.ts +0 -60
  69. package/src/preview-engine/next.config.mjs +0 -20
  70. package/src/preview-engine/postcss.config.mjs +0 -8
  71. package/src/preview-engine/source.config.ts +0 -26
  72. package/src/preview-engine/tsconfig.json +0 -32
  73. /package/src/{preview-engine/app → engine/app/_preview}/page.tsx +0 -0
  74. /package/src/{preview-engine/lib/auth.ts → engine/lib/preview-auth.ts} +0 -0
@@ -0,0 +1,344 @@
1
+ "use client";
2
+
3
+ import { createPortal } from "react-dom";
4
+ import { useEffect, useMemo, useState, type ReactNode } from "react";
5
+ import { VeluIcon } from "./icon";
6
+
7
+ const VELU_VIEW_OPTIONS_KEY = "__veluViewOptions";
8
+ const VELU_VIEW_OPTIONS_EVENT = "velu:view-options";
9
+ const VELU_VIEW_TOC_HOST_ID = "velu-view-toc-host";
10
+ const VELU_TAB_SYNC_KEY = "__veluTabSyncLabel";
11
+ const VELU_TAB_SYNC_EVENT = "velu:tab-sync";
12
+
13
+ type ViewOption = {
14
+ title: string;
15
+ icon?: string;
16
+ iconType?: string;
17
+ };
18
+
19
+ const VIEW_ICON_URL: Record<string, string> = {
20
+ javascript: "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/javascript/javascript-original.svg",
21
+ typescript: "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/typescript/typescript-original.svg",
22
+ python: "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/python/python-original.svg",
23
+ java: "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/java/java-original.svg",
24
+ ruby: "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/ruby/ruby-original.svg",
25
+ shell: "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/bash/bash-original.svg",
26
+ bash: "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/bash/bash-original.svg",
27
+ yaml: "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/yaml/yaml-original.svg",
28
+ markdown: "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/markdown/markdown-original.svg",
29
+ };
30
+
31
+ const VIEW_ICON_ALIAS: Record<string, string> = {
32
+ js: "javascript",
33
+ jsx: "javascript",
34
+ javascript: "javascript",
35
+ ts: "typescript",
36
+ tsx: "typescript",
37
+ typescript: "typescript",
38
+ py: "python",
39
+ python: "python",
40
+ java: "java",
41
+ rb: "ruby",
42
+ ruby: "ruby",
43
+ sh: "shell",
44
+ shell: "shell",
45
+ bash: "bash",
46
+ yml: "yaml",
47
+ yaml: "yaml",
48
+ md: "markdown",
49
+ markdown: "markdown",
50
+ };
51
+
52
+ function normalizeTitle(value: string): string {
53
+ return value.trim().toLowerCase();
54
+ }
55
+
56
+ function normalizeIconKey(value: string | undefined): string | undefined {
57
+ if (!value || !value.trim()) return undefined;
58
+ const key = value.trim().toLowerCase();
59
+ return VIEW_ICON_ALIAS[key] ?? key;
60
+ }
61
+
62
+ function resolveViewIconUrl(icon: string | undefined, title: string): string | undefined {
63
+ const iconKey = normalizeIconKey(icon);
64
+ if (iconKey && VIEW_ICON_URL[iconKey]) return VIEW_ICON_URL[iconKey];
65
+
66
+ const titleKey = normalizeIconKey(title);
67
+ if (titleKey && VIEW_ICON_URL[titleKey]) return VIEW_ICON_URL[titleKey];
68
+ return undefined;
69
+ }
70
+
71
+ function ViewOptionIcon({ title, icon, iconType }: { title: string; icon?: string; iconType?: string }) {
72
+ const src = resolveViewIconUrl(icon, title);
73
+ if (src) {
74
+ return <img src={src} alt="" aria-hidden="true" className="velu-view-option-icon velu-view-option-icon-img" loading="lazy" decoding="async" />;
75
+ }
76
+ if (!icon) return null;
77
+ return <VeluIcon name={icon} iconType={iconType} className="velu-view-option-icon" />;
78
+ }
79
+
80
+ function readSharedOptions(): ViewOption[] {
81
+ if (typeof window === "undefined") return [];
82
+ const value = (window as any)[VELU_VIEW_OPTIONS_KEY];
83
+ if (!Array.isArray(value)) return [];
84
+ return value.filter((item) => item && typeof item.title === "string" && item.title.trim());
85
+ }
86
+
87
+ function writeSharedOptions(options: ViewOption[]) {
88
+ if (typeof window === "undefined") return;
89
+ (window as any)[VELU_VIEW_OPTIONS_KEY] = options;
90
+ }
91
+
92
+ function upsertSharedOption(option: ViewOption): ViewOption[] {
93
+ const existing = readSharedOptions();
94
+ const key = normalizeTitle(option.title);
95
+ const next = [...existing];
96
+ const index = next.findIndex((item) => normalizeTitle(item.title) === key);
97
+ if (index >= 0) {
98
+ next[index] = { ...next[index], ...option };
99
+ } else {
100
+ next.push(option);
101
+ }
102
+ writeSharedOptions(next);
103
+ return next;
104
+ }
105
+
106
+ function broadcastOptions(options: ViewOption[]) {
107
+ if (typeof window === "undefined") return;
108
+ window.dispatchEvent(new CustomEvent(VELU_VIEW_OPTIONS_EVENT, { detail: { options } }));
109
+ }
110
+
111
+ function readSharedSelected(): string | null {
112
+ if (typeof window === "undefined") return null;
113
+ const value = (window as any)[VELU_TAB_SYNC_KEY];
114
+ return typeof value === "string" && value.trim() ? value : null;
115
+ }
116
+
117
+ function writeSharedSelected(title: string) {
118
+ if (typeof window === "undefined") return;
119
+ (window as any)[VELU_TAB_SYNC_KEY] = title;
120
+ }
121
+
122
+ function broadcastSelected(title: string) {
123
+ if (typeof window === "undefined") return;
124
+ const existing = readSharedSelected();
125
+ if (existing && normalizeTitle(existing) === normalizeTitle(title)) return;
126
+ writeSharedSelected(title);
127
+ window.dispatchEvent(new CustomEvent(VELU_TAB_SYNC_EVENT, { detail: { label: title } }));
128
+ }
129
+
130
+ function syncTocForVisibleHeadings() {
131
+ if (typeof document === "undefined") return;
132
+ const toc = document.getElementById("nd-toc");
133
+ if (!toc) return;
134
+
135
+ const links = Array.from(toc.querySelectorAll<HTMLAnchorElement>('a[href^="#"]'));
136
+ for (const link of links) {
137
+ const href = link.getAttribute("href");
138
+ if (!href || href === "#") continue;
139
+ const id = decodeURIComponent(href.slice(1));
140
+ const target = document.getElementById(id);
141
+ const row = link.closest("li") ?? link.parentElement;
142
+ if (!row) continue;
143
+ row.style.display = target ? "" : "none";
144
+ }
145
+ }
146
+
147
+ function ensureTocHost(): HTMLDivElement | null {
148
+ if (typeof document === "undefined") return null;
149
+ const toc = document.getElementById("nd-toc");
150
+ if (!toc) return null;
151
+
152
+ let host = document.getElementById(VELU_VIEW_TOC_HOST_ID) as HTMLDivElement | null;
153
+ if (!host) {
154
+ host = document.createElement("div");
155
+ host.id = VELU_VIEW_TOC_HOST_ID;
156
+ host.className = "velu-view-toc-host";
157
+ toc.prepend(host);
158
+ }
159
+ return host;
160
+ }
161
+
162
+ export function VeluView({
163
+ title,
164
+ icon,
165
+ iconType,
166
+ children,
167
+ className,
168
+ }: {
169
+ title?: string;
170
+ icon?: string;
171
+ iconType?: string;
172
+ children?: ReactNode;
173
+ className?: string;
174
+ }) {
175
+ const resolvedTitle = (typeof title === "string" && title.trim()) ? title.trim() : "View";
176
+ const normalizedResolvedTitle = useMemo(() => normalizeTitle(resolvedTitle), [resolvedTitle]);
177
+ const [selectedTitle, setSelectedTitle] = useState<string>(resolvedTitle);
178
+ const [menuOpen, setMenuOpen] = useState(false);
179
+ const [tocHost, setTocHost] = useState<HTMLDivElement | null>(null);
180
+ const [options, setOptions] = useState<ViewOption[]>(() => {
181
+ const initial = readSharedOptions();
182
+ return initial.length ? initial : [{ title: resolvedTitle, icon, iconType }];
183
+ });
184
+
185
+ useEffect(() => {
186
+ const nextOptions = upsertSharedOption({ title: resolvedTitle, icon, iconType });
187
+ setOptions(nextOptions);
188
+ broadcastOptions(nextOptions);
189
+
190
+ const existingSelected = readSharedSelected();
191
+ const hasExistingSelection = Boolean(
192
+ existingSelected && nextOptions.some((option) => normalizeTitle(option.title) === normalizeTitle(existingSelected)),
193
+ );
194
+
195
+ if (existingSelected && hasExistingSelection) {
196
+ setSelectedTitle(existingSelected);
197
+ } else {
198
+ setSelectedTitle(resolvedTitle);
199
+ broadcastSelected(resolvedTitle);
200
+ }
201
+ queueMicrotask(syncTocForVisibleHeadings);
202
+
203
+ const onSelected = (event: Event) => {
204
+ const detail = (event as CustomEvent<{ title?: string; label?: string }>).detail;
205
+ const next = detail?.label ?? detail?.title;
206
+ if (!next || !next.trim()) return;
207
+ setSelectedTitle(next);
208
+ queueMicrotask(syncTocForVisibleHeadings);
209
+ };
210
+
211
+ const onOptions = (event: Event) => {
212
+ const incoming = (event as CustomEvent<{ options?: ViewOption[] }>).detail?.options;
213
+ const next = Array.isArray(incoming) ? incoming : readSharedOptions();
214
+ setOptions(next.length ? next : [{ title: resolvedTitle, icon, iconType }]);
215
+ };
216
+
217
+ window.addEventListener(VELU_TAB_SYNC_EVENT, onSelected);
218
+ window.addEventListener(VELU_VIEW_OPTIONS_EVENT, onOptions);
219
+ return () => {
220
+ window.removeEventListener(VELU_TAB_SYNC_EVENT, onSelected);
221
+ window.removeEventListener(VELU_VIEW_OPTIONS_EVENT, onOptions);
222
+ };
223
+ }, [resolvedTitle, icon, iconType]);
224
+
225
+ useEffect(() => {
226
+ let frame = 0;
227
+ const attachHost = () => {
228
+ const host = ensureTocHost();
229
+ if (host) {
230
+ setTocHost(host);
231
+ return;
232
+ }
233
+ frame = window.requestAnimationFrame(attachHost);
234
+ };
235
+ attachHost();
236
+ return () => {
237
+ if (frame) window.cancelAnimationFrame(frame);
238
+ };
239
+ }, []);
240
+
241
+ useEffect(() => {
242
+ if (!menuOpen) return;
243
+ const onPointerDown = (event: PointerEvent) => {
244
+ const target = event.target as Element | null;
245
+ if (target?.closest(`#${VELU_VIEW_TOC_HOST_ID}`)) return;
246
+ setMenuOpen(false);
247
+ };
248
+ const onEscape = (event: KeyboardEvent) => {
249
+ if (event.key === "Escape") setMenuOpen(false);
250
+ };
251
+ window.addEventListener("pointerdown", onPointerDown);
252
+ window.addEventListener("keydown", onEscape);
253
+ return () => {
254
+ window.removeEventListener("pointerdown", onPointerDown);
255
+ window.removeEventListener("keydown", onEscape);
256
+ };
257
+ }, [menuOpen]);
258
+
259
+ useEffect(() => {
260
+ if (!options.length) return;
261
+ const hasCurrent = options.some((option) => normalizeTitle(option.title) === normalizeTitle(selectedTitle));
262
+ if (hasCurrent) return;
263
+ const fallback = options[0]?.title;
264
+ if (!fallback) return;
265
+ setSelectedTitle(fallback);
266
+ broadcastSelected(fallback);
267
+ queueMicrotask(syncTocForVisibleHeadings);
268
+ }, [options, selectedTitle]);
269
+
270
+ const effectiveOptions = options.length ? options : [{ title: resolvedTitle, icon, iconType }];
271
+ const isActive = normalizeTitle(selectedTitle) === normalizedResolvedTitle;
272
+ const selectedOption = effectiveOptions.find((option) => normalizeTitle(option.title) === normalizeTitle(selectedTitle))
273
+ ?? effectiveOptions[0];
274
+
275
+ const selectView = (nextTitle: string) => {
276
+ if (!nextTitle || normalizeTitle(nextTitle) === normalizeTitle(selectedTitle)) return;
277
+ setSelectedTitle(nextTitle);
278
+ setMenuOpen(false);
279
+ broadcastSelected(nextTitle);
280
+ queueMicrotask(syncTocForVisibleHeadings);
281
+ };
282
+
283
+ const viewSwitcher = (
284
+ tocHost && isActive && effectiveOptions.length > 1
285
+ ? createPortal(
286
+ <div className="velu-view-toc-switcher">
287
+ <button
288
+ type="button"
289
+ className="velu-view-toc-trigger"
290
+ aria-haspopup="listbox"
291
+ aria-expanded={menuOpen}
292
+ onClick={() => setMenuOpen((prev) => !prev)}
293
+ >
294
+ {selectedOption ? <ViewOptionIcon title={selectedOption.title} icon={selectedOption.icon} iconType={selectedOption.iconType} /> : null}
295
+ <span>{selectedOption?.title ?? "View"}</span>
296
+ <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={["velu-view-toc-chevron", menuOpen ? "open" : ""].join(" ")}>
297
+ <path d="m6 8 4 4 4-4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
298
+ </svg>
299
+ </button>
300
+ {menuOpen ? (
301
+ <div className="velu-view-toc-menu" role="listbox" aria-label="Select view">
302
+ {effectiveOptions.map((option) => {
303
+ const key = normalizeTitle(option.title);
304
+ const selected = normalizeTitle(selectedTitle) === key;
305
+ return (
306
+ <button
307
+ key={key}
308
+ type="button"
309
+ role="option"
310
+ aria-selected={selected}
311
+ className={["velu-view-toc-option", selected ? "active" : ""].filter(Boolean).join(" ")}
312
+ onClick={() => selectView(option.title)}
313
+ >
314
+ <span className="velu-view-toc-option-main">
315
+ <ViewOptionIcon title={option.title} icon={option.icon} iconType={option.iconType} />
316
+ <span>{option.title}</span>
317
+ </span>
318
+ {selected ? (
319
+ <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="velu-view-check">
320
+ <path d="m4.5 10.5 3.2 3.2 7.8-7.8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
321
+ </svg>
322
+ ) : null}
323
+ </button>
324
+ );
325
+ })}
326
+ </div>
327
+ ) : null}
328
+ </div>,
329
+ tocHost,
330
+ )
331
+ : null
332
+ );
333
+
334
+ if (!isActive) {
335
+ return viewSwitcher;
336
+ }
337
+
338
+ return (
339
+ <section className={["velu-view", className].filter(Boolean).join(" ")} data-velu-view={resolvedTitle}>
340
+ {viewSwitcher}
341
+ <div className="velu-view-content">{children}</div>
342
+ </section>
343
+ );
344
+ }
@@ -0,0 +1,326 @@
1
+ /* ── Velu AI Assistant ─────────────────────────────────────────────────── */
2
+
3
+ /* Fixed bottom ask bar */
4
+ .velu-ask-bar {
5
+ position: fixed;
6
+ bottom: 1.5rem;
7
+ left: 50%;
8
+ transform: translateX(-50%);
9
+ z-index: 200;
10
+ width: 100%;
11
+ max-width: 36rem;
12
+ padding: 0 1rem;
13
+ transition: opacity 0.2s, transform 0.2s;
14
+ }
15
+
16
+ .velu-ask-bar-hidden {
17
+ opacity: 0;
18
+ pointer-events: none;
19
+ transform: translateX(-50%) translateY(1rem);
20
+ }
21
+
22
+ .velu-ask-bar-inner {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: 0.5rem;
26
+ padding: 0.5rem 0.75rem;
27
+ background: var(--color-fd-card, #18181b);
28
+ border: 1px solid var(--color-fd-border, #27272a);
29
+ border-radius: 0.75rem;
30
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
31
+ }
32
+
33
+ .velu-ask-input {
34
+ flex: 1;
35
+ background: none;
36
+ border: none;
37
+ outline: none;
38
+ font: inherit;
39
+ font-size: 0.875rem;
40
+ color: var(--color-fd-foreground, #fafafa);
41
+ }
42
+
43
+ .velu-ask-input::placeholder {
44
+ color: var(--color-fd-muted-foreground, #a1a1aa);
45
+ }
46
+
47
+ .velu-ask-submit {
48
+ flex-shrink: 0;
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ width: 28px;
53
+ height: 28px;
54
+ background: var(--color-fd-primary, #818cf8);
55
+ color: var(--color-fd-primary-foreground, #fff);
56
+ border: none;
57
+ border-radius: 50%;
58
+ cursor: pointer;
59
+ transition: opacity 0.15s;
60
+ }
61
+
62
+ .velu-ask-submit:hover { opacity: 0.85; }
63
+
64
+ /* Right-side assistant panel */
65
+ .velu-assistant-panel {
66
+ position: fixed;
67
+ top: 0;
68
+ right: 0;
69
+ bottom: 0;
70
+ width: 22rem;
71
+ z-index: 50;
72
+ pointer-events: auto;
73
+ display: flex;
74
+ flex-direction: column;
75
+ background: var(--color-fd-background, #09090b);
76
+ border-left: 1px solid var(--color-fd-border, #27272a);
77
+ box-shadow: -4px 0 24px rgba(0, 0, 0, 0.2);
78
+ transition: width 0.2s;
79
+ }
80
+
81
+ .velu-panel-closed { display: none !important; }
82
+
83
+ .velu-assistant-expanded { width: 40rem; }
84
+
85
+ .velu-assistant-header {
86
+ display: flex;
87
+ align-items: center;
88
+ justify-content: space-between;
89
+ padding: 0.75rem 1rem;
90
+ border-bottom: 1px solid var(--color-fd-border, #27272a);
91
+ flex-shrink: 0;
92
+ }
93
+
94
+ .velu-assistant-title {
95
+ font-weight: 600;
96
+ font-size: 0.9375rem;
97
+ color: var(--color-fd-foreground, #fafafa);
98
+ }
99
+
100
+ .velu-assistant-actions {
101
+ display: flex;
102
+ gap: 0.25rem;
103
+ }
104
+
105
+ .velu-assistant-action {
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ width: 28px;
110
+ height: 28px;
111
+ background: none;
112
+ border: none;
113
+ border-radius: 0.25rem;
114
+ color: var(--color-fd-muted-foreground, #a1a1aa);
115
+ cursor: pointer;
116
+ pointer-events: auto;
117
+ transition: color 0.15s, background-color 0.15s;
118
+ }
119
+
120
+ .velu-assistant-action:hover {
121
+ color: var(--color-fd-foreground, #fafafa);
122
+ background-color: var(--color-fd-accent, #27272a);
123
+ }
124
+
125
+ /* Messages area */
126
+ .velu-assistant-messages {
127
+ flex: 1;
128
+ overflow-y: auto;
129
+ padding: 1rem;
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: 0.75rem;
133
+ }
134
+
135
+ .velu-msg {
136
+ display: flex;
137
+ flex-direction: column;
138
+ gap: 0.35rem;
139
+ }
140
+
141
+ .velu-msg-user { align-items: flex-end; }
142
+ .velu-msg-assistant { align-items: flex-start; }
143
+
144
+ .velu-msg-bubble {
145
+ max-width: 85%;
146
+ padding: 0.6rem 0.85rem;
147
+ border-radius: 0.75rem;
148
+ font-size: 0.875rem;
149
+ line-height: 1.55;
150
+ word-break: break-word;
151
+ }
152
+
153
+ .velu-msg-bubble code {
154
+ background: var(--color-fd-accent, #27272a);
155
+ padding: 0.1rem 0.3rem;
156
+ border-radius: 0.2rem;
157
+ font-size: 0.85em;
158
+ }
159
+
160
+ .velu-msg-bubble-user {
161
+ background: var(--color-fd-primary, #818cf8);
162
+ color: var(--color-fd-primary-foreground, #fff);
163
+ border-bottom-right-radius: 0.2rem;
164
+ }
165
+
166
+ .velu-msg-bubble-assistant {
167
+ background: var(--color-fd-accent, #27272a);
168
+ color: var(--color-fd-foreground, #fafafa);
169
+ border-bottom-left-radius: 0.2rem;
170
+ }
171
+
172
+ /* Citations */
173
+ .velu-msg-citations {
174
+ display: flex;
175
+ flex-wrap: wrap;
176
+ gap: 0.35rem;
177
+ padding-left: 0.25rem;
178
+ }
179
+
180
+ .velu-citation-link {
181
+ font-size: 0.75rem;
182
+ color: var(--color-fd-primary, #818cf8);
183
+ text-decoration: none;
184
+ padding: 0.15rem 0.4rem;
185
+ background: var(--color-fd-accent, #27272a);
186
+ border-radius: 0.25rem;
187
+ transition: background-color 0.15s;
188
+ }
189
+
190
+ .velu-citation-link:hover {
191
+ background: var(--color-fd-border, #3f3f46);
192
+ }
193
+
194
+ .velu-citation-ref {
195
+ color: var(--color-fd-primary, #818cf8);
196
+ text-decoration: none;
197
+ font-weight: 600;
198
+ font-size: 0.8em;
199
+ vertical-align: super;
200
+ }
201
+
202
+ /* Message actions */
203
+ .velu-msg-actions {
204
+ display: flex;
205
+ gap: 0.15rem;
206
+ padding-left: 0.25rem;
207
+ }
208
+
209
+ .velu-msg-action {
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: center;
213
+ width: 24px;
214
+ height: 24px;
215
+ background: none;
216
+ border: none;
217
+ border-radius: 0.25rem;
218
+ color: var(--color-fd-muted-foreground, #a1a1aa);
219
+ cursor: pointer;
220
+ transition: color 0.15s, background-color 0.15s;
221
+ }
222
+
223
+ .velu-msg-action:hover {
224
+ color: var(--color-fd-foreground, #fafafa);
225
+ background-color: var(--color-fd-accent, #27272a);
226
+ }
227
+
228
+ /* Thinking dots */
229
+ .velu-thinking-dots {
230
+ display: inline-flex;
231
+ gap: 0.3rem;
232
+ padding: 0.2rem 0;
233
+ }
234
+
235
+ .velu-thinking-dots span {
236
+ width: 6px;
237
+ height: 6px;
238
+ border-radius: 50%;
239
+ background: var(--color-fd-muted-foreground, #a1a1aa);
240
+ animation: veluDotPulse 1.2s infinite;
241
+ }
242
+
243
+ .velu-thinking-dots span:nth-child(2) { animation-delay: 0.2s; }
244
+ .velu-thinking-dots span:nth-child(3) { animation-delay: 0.4s; }
245
+
246
+ @keyframes veluDotPulse {
247
+ 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
248
+ 40% { opacity: 1; transform: scale(1); }
249
+ }
250
+
251
+ /* Chat input area */
252
+ .velu-assistant-input-area {
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 0.5rem;
256
+ padding: 0.75rem 1rem;
257
+ border-top: 1px solid var(--color-fd-border, #27272a);
258
+ flex-shrink: 0;
259
+ }
260
+
261
+ .velu-assistant-chat-input {
262
+ flex: 1;
263
+ background: var(--color-fd-accent, #27272a);
264
+ border: 1px solid var(--color-fd-border, #27272a);
265
+ border-radius: 0.5rem;
266
+ padding: 0.5rem 0.75rem;
267
+ font: inherit;
268
+ font-size: 0.875rem;
269
+ color: var(--color-fd-foreground, #fafafa);
270
+ outline: none;
271
+ transition: border-color 0.15s;
272
+ }
273
+
274
+ .velu-assistant-chat-input:focus {
275
+ border-color: var(--color-fd-primary, #818cf8);
276
+ }
277
+
278
+ .velu-assistant-chat-input::placeholder {
279
+ color: var(--color-fd-muted-foreground, #a1a1aa);
280
+ }
281
+
282
+ .velu-assistant-send {
283
+ flex-shrink: 0;
284
+ display: flex;
285
+ align-items: center;
286
+ justify-content: center;
287
+ width: 32px;
288
+ height: 32px;
289
+ background: var(--color-fd-primary, #818cf8);
290
+ color: var(--color-fd-primary-foreground, #fff);
291
+ border: none;
292
+ border-radius: 50%;
293
+ cursor: pointer;
294
+ transition: opacity 0.15s;
295
+ }
296
+
297
+ .velu-assistant-send:hover { opacity: 0.85; }
298
+
299
+ /* Squeeze page layout when panel is open */
300
+ html.velu-assistant-open body {
301
+ margin-right: 22rem;
302
+ transition: margin-right 0.25s ease;
303
+ }
304
+
305
+ html.velu-assistant-wide body {
306
+ margin-right: 40rem;
307
+ }
308
+
309
+ /* Responsive */
310
+ @media (max-width: 50rem) {
311
+ .velu-assistant-panel {
312
+ width: 100%;
313
+ }
314
+ .velu-assistant-expanded {
315
+ width: 100%;
316
+ }
317
+ .velu-ask-bar {
318
+ max-width: calc(100% - 2rem);
319
+ }
320
+ html.velu-assistant-open body {
321
+ margin-right: 0;
322
+ }
323
+ html.velu-assistant-wide body {
324
+ margin-right: 0;
325
+ }
326
+ }