@aravindc26/velu 0.9.1 → 0.11.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.
@@ -1,5 +1,4 @@
1
1
  @import 'tailwindcss';
2
- @import 'fumadocs-ui/css/neutral.css';
3
2
  @import 'fumadocs-ui/css/preset.css';
4
3
  @import './velu-theme.css';
5
4
 
@@ -8,6 +7,10 @@ body {
8
7
  }
9
8
 
10
9
  /* Ensure sidebar/toc widths are set on the grid layout */
10
+ nextjs-portal {
11
+ display: none !important;
12
+ }
13
+
11
14
  @media (min-width: 768px) {
12
15
  #nd-docs-layout[data-sidebar-collapsed='false'] {
13
16
  --fd-sidebar-width: 268px;
@@ -19,3 +22,345 @@ body {
19
22
  --fd-toc-width: 268px;
20
23
  }
21
24
  }
25
+
26
+ .velu-sidebar-footer {
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: 0.5rem;
30
+ padding-top: 0.5rem;
31
+ }
32
+
33
+ .velu-sidebar-banner {
34
+ display: flex;
35
+ width: 100%;
36
+ padding: 0.125rem 0;
37
+ order: -1;
38
+ }
39
+
40
+ /* Keep the sidebar title row above the product switcher */
41
+ #nd-sidebar .flex.flex-col > :first-child {
42
+ order: -2;
43
+ }
44
+
45
+ .velu-header-version-switcher {
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: flex-end;
49
+ }
50
+
51
+ .velu-version-switcher-wrap {
52
+ position: relative;
53
+ }
54
+
55
+ .velu-version-switcher {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ gap: 0.35rem;
59
+ padding: 0.25rem 0.5rem;
60
+ border: none;
61
+ border-radius: 0.375rem;
62
+ background: transparent;
63
+ color: var(--color-fd-muted-foreground);
64
+ font-size: 0.75rem;
65
+ font-weight: 500;
66
+ cursor: pointer;
67
+ transition: color 0.15s, background-color 0.15s;
68
+ }
69
+
70
+ .velu-version-switcher:hover {
71
+ color: var(--color-fd-foreground);
72
+ background-color: var(--color-fd-accent);
73
+ }
74
+
75
+ .velu-version-switcher svg {
76
+ width: 0.9rem;
77
+ height: 0.9rem;
78
+ }
79
+
80
+ .velu-version-switcher svg:last-child {
81
+ width: 0.8rem;
82
+ height: 0.8rem;
83
+ }
84
+
85
+ .velu-version-menu {
86
+ position: absolute;
87
+ top: calc(100% + 0.25rem);
88
+ right: 0;
89
+ min-width: 7.5rem;
90
+ padding: 0.25rem;
91
+ border-radius: 0.5rem;
92
+ background-color: var(--color-fd-popover);
93
+ border: 1px solid var(--color-fd-border);
94
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
95
+ z-index: 60;
96
+ }
97
+
98
+ .velu-version-option {
99
+ display: block;
100
+ width: 100%;
101
+ padding: 0.35rem 0.5rem;
102
+ border: none;
103
+ border-radius: 0.375rem;
104
+ background: transparent;
105
+ color: var(--color-fd-muted-foreground);
106
+ font-size: 0.8rem;
107
+ text-align: left;
108
+ cursor: pointer;
109
+ transition: color 0.15s, background-color 0.15s;
110
+ }
111
+
112
+ .velu-version-option:hover {
113
+ background-color: var(--color-fd-accent);
114
+ color: var(--color-fd-foreground);
115
+ }
116
+
117
+ .velu-version-option.active {
118
+ color: var(--color-fd-primary);
119
+ }
120
+
121
+ .velu-product-switcher-wrap {
122
+ position: relative;
123
+ width: 100%;
124
+ }
125
+
126
+ .velu-product-switcher {
127
+ display: flex;
128
+ align-items: center;
129
+ justify-content: space-between;
130
+ width: 100%;
131
+ padding: 0.5rem 0.625rem;
132
+ border: 1px solid var(--color-fd-border);
133
+ border-radius: 0.5rem;
134
+ background: var(--color-fd-background);
135
+ color: var(--color-fd-foreground);
136
+ font-size: 0.8125rem;
137
+ font-weight: 500;
138
+ cursor: pointer;
139
+ transition: border-color 0.15s, background-color 0.15s;
140
+ }
141
+
142
+ .velu-product-switcher:hover {
143
+ border-color: var(--color-fd-primary);
144
+ background-color: var(--color-fd-accent);
145
+ }
146
+
147
+ .velu-product-switcher svg {
148
+ width: 0.875rem;
149
+ height: 0.875rem;
150
+ flex-shrink: 0;
151
+ opacity: 0.6;
152
+ }
153
+
154
+ .velu-product-switcher-label {
155
+ flex: 1;
156
+ text-align: left;
157
+ }
158
+
159
+ .velu-product-menu {
160
+ position: absolute;
161
+ top: calc(100% + 0.25rem);
162
+ left: 0;
163
+ right: 0;
164
+ padding: 0.25rem;
165
+ border-radius: 0.5rem;
166
+ background-color: var(--color-fd-popover);
167
+ border: 1px solid var(--color-fd-border);
168
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
169
+ z-index: 60;
170
+ }
171
+
172
+ .velu-product-option {
173
+ display: flex;
174
+ flex-direction: column;
175
+ width: 100%;
176
+ padding: 0.5rem 0.625rem;
177
+ border: none;
178
+ border-radius: 0.375rem;
179
+ background: transparent;
180
+ text-align: left;
181
+ cursor: pointer;
182
+ transition: background-color 0.15s;
183
+ }
184
+
185
+ .velu-product-option:hover {
186
+ background-color: var(--color-fd-accent);
187
+ }
188
+
189
+ .velu-product-option.active {
190
+ background-color: var(--color-fd-accent);
191
+ }
192
+
193
+ .velu-product-option-name {
194
+ font-size: 0.8125rem;
195
+ font-weight: 500;
196
+ color: var(--color-fd-foreground);
197
+ }
198
+
199
+ .velu-product-option.active .velu-product-option-name {
200
+ color: var(--color-fd-primary);
201
+ }
202
+
203
+ .velu-product-option-desc {
204
+ font-size: 0.6875rem;
205
+ color: var(--color-fd-muted-foreground);
206
+ margin-top: 0.125rem;
207
+ }
208
+
209
+ .velu-sidebar-footer-row {
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: space-between;
213
+ }
214
+
215
+ .velu-sidebar-links {
216
+ display: flex;
217
+ flex-direction: column;
218
+ gap: 0;
219
+ }
220
+
221
+ .velu-sidebar-link {
222
+ display: flex;
223
+ align-items: center;
224
+ justify-content: space-between;
225
+ padding: 0.375rem 0.5rem;
226
+ border-radius: 0.375rem;
227
+ font-size: 0.875rem;
228
+ color: var(--color-fd-muted-foreground);
229
+ text-decoration: none;
230
+ transition: color 0.15s, background-color 0.15s;
231
+ }
232
+
233
+ .velu-sidebar-link:hover {
234
+ color: var(--color-fd-foreground);
235
+ background-color: var(--color-fd-accent);
236
+ }
237
+
238
+ .velu-sidebar-link-text {
239
+ flex: 1;
240
+ }
241
+
242
+ .velu-sidebar-link-icon {
243
+ width: 0.875rem;
244
+ height: 0.875rem;
245
+ opacity: 0.5;
246
+ flex-shrink: 0;
247
+ }
248
+
249
+ .velu-sidebar-link:hover .velu-sidebar-link-icon {
250
+ opacity: 0.8;
251
+ }
252
+
253
+ .velu-theme-toggle {
254
+ display: flex;
255
+ align-items: center;
256
+ gap: 0.25rem;
257
+ padding: 0.25rem;
258
+ border-radius: 0.5rem;
259
+ background-color: var(--color-fd-accent);
260
+ width: fit-content;
261
+ margin-left: auto;
262
+ }
263
+
264
+ .velu-lang-switcher {
265
+ display: flex;
266
+ align-items: center;
267
+ gap: 0.375rem;
268
+ padding: 0.375rem 0.5rem;
269
+ border: none;
270
+ border-radius: 0.375rem;
271
+ background: transparent;
272
+ color: var(--color-fd-muted-foreground);
273
+ font-size: 0.875rem;
274
+ cursor: pointer;
275
+ transition: color 0.15s, background-color 0.15s;
276
+ position: relative;
277
+ }
278
+
279
+ .velu-lang-switcher:hover {
280
+ color: var(--color-fd-foreground);
281
+ background-color: var(--color-fd-accent);
282
+ }
283
+
284
+ .velu-lang-switcher svg {
285
+ width: 1rem;
286
+ height: 1rem;
287
+ }
288
+
289
+ .velu-lang-menu {
290
+ position: absolute;
291
+ bottom: 100%;
292
+ left: 0;
293
+ margin-bottom: 0.25rem;
294
+ min-width: 8rem;
295
+ padding: 0.25rem;
296
+ border-radius: 0.5rem;
297
+ background-color: var(--color-fd-popover);
298
+ border: 1px solid var(--color-fd-border);
299
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
300
+ z-index: 50;
301
+ }
302
+
303
+ .velu-lang-option {
304
+ display: block;
305
+ width: 100%;
306
+ padding: 0.375rem 0.5rem;
307
+ border: none;
308
+ border-radius: 0.375rem;
309
+ background: transparent;
310
+ color: var(--color-fd-muted-foreground);
311
+ font-size: 0.875rem;
312
+ text-align: left;
313
+ cursor: pointer;
314
+ transition: color 0.15s, background-color 0.15s;
315
+ }
316
+
317
+ .velu-lang-option:hover {
318
+ background-color: var(--color-fd-accent);
319
+ color: var(--color-fd-foreground);
320
+ }
321
+
322
+ .velu-lang-option.active {
323
+ color: var(--color-fd-primary);
324
+ }
325
+
326
+ .velu-theme-btn {
327
+ display: flex;
328
+ align-items: center;
329
+ justify-content: center;
330
+ padding: 0.375rem;
331
+ border: none;
332
+ border-radius: 0.375rem;
333
+ background: transparent;
334
+ color: var(--color-fd-muted-foreground);
335
+ cursor: pointer;
336
+ transition: color 0.15s, background-color 0.15s;
337
+ }
338
+
339
+ .velu-theme-btn:hover {
340
+ color: var(--color-fd-foreground);
341
+ }
342
+
343
+ .velu-theme-btn.active {
344
+ background-color: var(--color-fd-background);
345
+ color: var(--color-fd-foreground);
346
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
347
+ }
348
+
349
+ .velu-footer {
350
+ margin-top: 3rem;
351
+ padding-top: 1.5rem;
352
+ border-top: 1px solid var(--color-fd-border);
353
+ text-align: right;
354
+ font-size: 1.2rem;
355
+ color: var(--color-fd-muted-foreground);
356
+ }
357
+
358
+ .velu-footer a {
359
+ color: var(--color-fd-primary);
360
+ font-weight: 600;
361
+ text-decoration: none;
362
+ }
363
+
364
+ .velu-footer a:hover {
365
+ text-decoration: underline;
366
+ }
@@ -1,7 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
- import { RootProvider } from 'fumadocs-ui/provider/next';
3
2
  import { getAppearance } from '@/lib/velu';
4
- import { PagefindSearch } from '@/components/search';
3
+ import { Providers } from '@/components/providers';
5
4
  import { VeluAssistant } from '@/components/assistant';
6
5
  import './global.css';
7
6
  import './search.css';
@@ -21,13 +20,10 @@ export default function RootLayout({ children }: { children: ReactNode }) {
21
20
  return (
22
21
  <html lang="en" suppressHydrationWarning>
23
22
  <body className="min-h-screen" suppressHydrationWarning>
24
- <RootProvider
25
- theme={theme}
26
- search={{ SearchDialog: PagefindSearch }}
27
- >
23
+ <Providers theme={theme}>
28
24
  {children}
29
25
  <VeluAssistant />
30
- </RootProvider>
26
+ </Providers>
31
27
  </body>
32
28
  </html>
33
29
  );
@@ -116,3 +116,23 @@
116
116
  border-radius: 2px;
117
117
  padding: 0 2px;
118
118
  }
119
+
120
+ /* Search filter indicator */
121
+ .fd-search-filters {
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 8px;
125
+ padding: 8px 16px;
126
+ border-bottom: 1px solid var(--color-fd-border, #27272a);
127
+ background: var(--color-fd-accent, #27272a);
128
+ font-size: 0.75rem;
129
+ }
130
+
131
+ .fd-search-filter-label {
132
+ color: var(--color-fd-muted-foreground, #a1a1aa);
133
+ }
134
+
135
+ .fd-search-filter-value {
136
+ color: var(--color-fd-primary, #818cf8);
137
+ font-weight: 500;
138
+ }
@@ -0,0 +1,95 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect, useMemo } from 'react';
4
+ import { usePathname } from 'next/navigation';
5
+
6
+ function GlobeIcon() {
7
+ return (
8
+ <svg
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth={2}
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ >
17
+ <circle cx="12" cy="12" r="10" />
18
+ <path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
19
+ <path d="M2 12h20" />
20
+ </svg>
21
+ );
22
+ }
23
+
24
+ function nativeLabel(code: string): string {
25
+ try {
26
+ const dn = new Intl.DisplayNames([code], { type: 'language' });
27
+ const name = dn.of(code);
28
+ if (name) return name.charAt(0).toUpperCase() + name.slice(1);
29
+ } catch {}
30
+ return code.toUpperCase();
31
+ }
32
+
33
+ export function LanguageSwitcher({ languages, defaultLang }: { languages: string[]; defaultLang: string }) {
34
+ const [open, setOpen] = useState(false);
35
+ const [mounted, setMounted] = useState(false);
36
+ const ref = useRef<HTMLDivElement>(null);
37
+ const pathname = usePathname();
38
+
39
+ useEffect(() => setMounted(true), []);
40
+
41
+ const current = useMemo(() => {
42
+ const firstSeg = pathname.split('/').filter(Boolean)[0];
43
+ return languages.includes(firstSeg ?? '') ? firstSeg! : defaultLang;
44
+ }, [pathname, languages, defaultLang]);
45
+
46
+ useEffect(() => {
47
+ function handleClick(e: MouseEvent) {
48
+ if (ref.current && !ref.current.contains(e.target as Node)) {
49
+ setOpen(false);
50
+ }
51
+ }
52
+ document.addEventListener('mousedown', handleClick);
53
+ return () => document.removeEventListener('mousedown', handleClick);
54
+ }, []);
55
+
56
+ if (languages.length <= 1) return null;
57
+
58
+ function switchTo(code: string) {
59
+ setOpen(false);
60
+ const segments = pathname.split('/').filter(Boolean);
61
+ const isLangPrefix = languages.includes(segments[0] ?? '');
62
+ const rest = isLangPrefix ? segments.slice(1) : segments;
63
+ const newPath = code === defaultLang
64
+ ? '/' + rest.join('/')
65
+ : '/' + code + '/' + rest.join('/');
66
+ window.location.href = newPath;
67
+ }
68
+
69
+ return (
70
+ <div ref={ref} style={{ position: 'relative', ...(mounted ? {} : { opacity: 0, pointerEvents: 'none' as const }) }}>
71
+ <button
72
+ type="button"
73
+ className="velu-lang-switcher"
74
+ onClick={() => mounted && setOpen(!open)}
75
+ >
76
+ <GlobeIcon />
77
+ <span>{nativeLabel(current)}</span>
78
+ </button>
79
+ {open && (
80
+ <div className="velu-lang-menu">
81
+ {languages.map((code) => (
82
+ <button
83
+ key={code}
84
+ type="button"
85
+ className={`velu-lang-option ${code === current ? 'active' : ''}`}
86
+ onClick={() => switchTo(code)}
87
+ >
88
+ {nativeLabel(code)}
89
+ </button>
90
+ ))}
91
+ </div>
92
+ )}
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { usePathname } from 'next/navigation';
5
+ import type { VeluProductOption } from '@/lib/velu';
6
+
7
+ function ChevronDownIcon() {
8
+ return (
9
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
10
+ <path d="m6 9 6 6 6-6" />
11
+ </svg>
12
+ );
13
+ }
14
+
15
+ export function ProductSwitcher({ products }: { products: VeluProductOption[] }) {
16
+ const pathname = usePathname();
17
+ const [open, setOpen] = useState(false);
18
+ const ref = useRef<HTMLDivElement>(null);
19
+
20
+ const current = useMemo(() => {
21
+ const firstSeg = pathname.split('/').filter(Boolean)[0] ?? '';
22
+ return products.find((p) => p.slug === firstSeg) ?? products[0];
23
+ }, [pathname, products]);
24
+
25
+ useEffect(() => {
26
+ function handleClick(e: MouseEvent) {
27
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
28
+ }
29
+ document.addEventListener('mousedown', handleClick);
30
+ return () => document.removeEventListener('mousedown', handleClick);
31
+ }, []);
32
+
33
+ if (!current || products.length <= 1) return null;
34
+
35
+ function switchTo(target: VeluProductOption) {
36
+ setOpen(false);
37
+
38
+ const segments = pathname.split('/').filter(Boolean);
39
+ const firstSeg = segments[0] ?? '';
40
+
41
+ if (current && current.slug === firstSeg) {
42
+ // Replace the product segment, keep tab/group/page segments
43
+ const rest = segments.slice(1);
44
+ if (rest.length > 0) {
45
+ window.location.href = '/' + [target.slug, ...rest].join('/');
46
+ return;
47
+ }
48
+ }
49
+
50
+ window.location.href = target.defaultPath;
51
+ }
52
+
53
+ return (
54
+ <div className="velu-product-switcher-wrap" ref={ref}>
55
+ <button type="button" className="velu-product-switcher" onClick={() => setOpen((v) => !v)}>
56
+ <span className="velu-product-switcher-label">{current.product}</span>
57
+ <ChevronDownIcon />
58
+ </button>
59
+ {open && (
60
+ <div className="velu-product-menu">
61
+ {products.map((product) => (
62
+ <button
63
+ key={product.slug}
64
+ type="button"
65
+ className={`velu-product-option ${product.slug === current.slug ? 'active' : ''}`}
66
+ onClick={() => switchTo(product)}
67
+ >
68
+ <span className="velu-product-option-name">{product.product}</span>
69
+ {product.description && (
70
+ <span className="velu-product-option-desc">{product.description}</span>
71
+ )}
72
+ </button>
73
+ ))}
74
+ </div>
75
+ )}
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import dynamic from 'next/dynamic';
5
+ import { RootProvider } from 'fumadocs-ui/provider/next';
6
+
7
+ const PagefindSearch = dynamic(
8
+ () => import('@/components/search').then((m) => m.PagefindSearch),
9
+ { ssr: false }
10
+ );
11
+
12
+ interface ProvidersProps {
13
+ children: ReactNode;
14
+ theme?: {
15
+ defaultTheme: string;
16
+ enableSystem: boolean;
17
+ };
18
+ }
19
+
20
+ export function Providers({ children, theme }: ProvidersProps) {
21
+ return (
22
+ <RootProvider theme={theme} search={{ SearchDialog: PagefindSearch }}>
23
+ {children}
24
+ </RootProvider>
25
+ );
26
+ }