@aravindc26/velu 0.11.6 → 0.11.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.
- package/package.json +1 -1
- package/schema/velu.schema.json +383 -122
- package/src/build.ts +679 -551
- package/src/cli.ts +65 -2
- package/src/engine/app/(docs)/[...slug]/layout.tsx +155 -13
- package/src/engine/app/(docs)/[...slug]/page.tsx +77 -9
- package/src/engine/app/copy-page.css +17 -1
- package/src/engine/app/global.css +111 -5
- package/src/engine/app/layout.tsx +8 -1
- package/src/engine/app/search.css +4 -0
- package/src/engine/components/banner.tsx +80 -0
- package/src/engine/components/copy-page.tsx +162 -35
- package/src/engine/components/dropdown-switcher.tsx +142 -0
- package/src/engine/components/header-tab-link.tsx +43 -0
- package/src/engine/components/search.tsx +136 -49
- package/src/engine/lib/layout.shared.ts +68 -68
- package/src/engine/lib/velu.ts +297 -0
- package/src/validate.ts +8 -0
package/src/engine/lib/velu.ts
CHANGED
|
@@ -107,12 +107,37 @@ interface VeluSeoConfig {
|
|
|
107
107
|
indexing?: 'navigable' | 'all' | string;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
interface VeluMetadataConfig {
|
|
111
|
+
timestamp?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
110
114
|
interface VeluThemeAsset {
|
|
111
115
|
light?: string;
|
|
112
116
|
dark?: string;
|
|
113
117
|
href?: string;
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
interface VeluFooterConfig {
|
|
121
|
+
socials?: Record<string, unknown> | VeluFooterSocialInput[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface VeluFooterSocialInput {
|
|
125
|
+
type?: string;
|
|
126
|
+
label?: string;
|
|
127
|
+
href?: string;
|
|
128
|
+
url?: string;
|
|
129
|
+
icon?: string;
|
|
130
|
+
iconType?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface VeluFooterSocialLink {
|
|
134
|
+
key: string;
|
|
135
|
+
label: string;
|
|
136
|
+
href: string;
|
|
137
|
+
icon: string;
|
|
138
|
+
iconType?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
116
141
|
export interface VeluProductOption {
|
|
117
142
|
product: string;
|
|
118
143
|
slug: string;
|
|
@@ -123,6 +148,16 @@ export interface VeluProductOption {
|
|
|
123
148
|
defaultPath: string;
|
|
124
149
|
}
|
|
125
150
|
|
|
151
|
+
export interface VeluDropdownOption {
|
|
152
|
+
dropdown: string;
|
|
153
|
+
slug: string;
|
|
154
|
+
description?: string;
|
|
155
|
+
icon?: string;
|
|
156
|
+
iconType?: string;
|
|
157
|
+
tabSlugs: string[];
|
|
158
|
+
defaultPath: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
126
161
|
export interface VeluVersionOption {
|
|
127
162
|
version: string;
|
|
128
163
|
slug: string;
|
|
@@ -131,8 +166,16 @@ export interface VeluVersionOption {
|
|
|
131
166
|
defaultPath: string;
|
|
132
167
|
}
|
|
133
168
|
|
|
169
|
+
interface VeluContextualCustomOption {
|
|
170
|
+
title: string;
|
|
171
|
+
description?: string;
|
|
172
|
+
icon?: string;
|
|
173
|
+
href: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
134
176
|
interface VeluConfig {
|
|
135
177
|
name?: string;
|
|
178
|
+
description?: string;
|
|
136
179
|
title?: string;
|
|
137
180
|
favicon?: string | VeluThemeAsset;
|
|
138
181
|
logo?: string | VeluThemeAsset;
|
|
@@ -150,6 +193,13 @@ interface VeluConfig {
|
|
|
150
193
|
asyncapi?: string | string[] | Record<string, unknown>;
|
|
151
194
|
api?: VeluApiConfig;
|
|
152
195
|
seo?: VeluSeoConfig;
|
|
196
|
+
metadata?: VeluMetadataConfig;
|
|
197
|
+
footer?: VeluFooterConfig;
|
|
198
|
+
footerSocials?: Record<string, unknown> | VeluFooterSocialInput[];
|
|
199
|
+
banner?: { content?: string; dismissible?: boolean };
|
|
200
|
+
contextual?: {
|
|
201
|
+
options?: Array<string | VeluContextualCustomOption>;
|
|
202
|
+
};
|
|
153
203
|
navigation: {
|
|
154
204
|
tabs?: VeluTab[];
|
|
155
205
|
languages?: VeluLanguageNav[];
|
|
@@ -243,6 +293,41 @@ function findFirstPageInTab(tab: VeluTab): string | undefined {
|
|
|
243
293
|
return undefined;
|
|
244
294
|
}
|
|
245
295
|
|
|
296
|
+
function findFirstRouteInGroup(group: VeluGroup, tabSlug: string, parentGroupSlugs: string[] = []): string | undefined {
|
|
297
|
+
const nextGroupSlugs = group.slug ? [...parentGroupSlugs, group.slug] : parentGroupSlugs;
|
|
298
|
+
|
|
299
|
+
for (const item of group.pages) {
|
|
300
|
+
if (typeof item === 'string') {
|
|
301
|
+
return `/${[tabSlug, ...nextGroupSlugs, pageBasename(item)].filter(Boolean).join('/')}`;
|
|
302
|
+
}
|
|
303
|
+
if (isGroup(item)) {
|
|
304
|
+
const nested = findFirstRouteInGroup(item, tabSlug, nextGroupSlugs);
|
|
305
|
+
if (nested) return nested;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function resolveFirstRouteInTab(tab: VeluTab): string | undefined {
|
|
312
|
+
if (!tab.slug) return undefined;
|
|
313
|
+
|
|
314
|
+
if (tab.pages) {
|
|
315
|
+
for (const item of tab.pages) {
|
|
316
|
+
if (typeof item === 'string') return `/${tab.slug}/${pageBasename(item)}`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (tab.groups) {
|
|
321
|
+
for (const group of tab.groups) {
|
|
322
|
+
const nested = findFirstRouteInGroup(group, tab.slug);
|
|
323
|
+
if (nested) return nested;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (typeof tab.href === 'string' && tab.href.trim().length > 0) return tab.href.trim();
|
|
328
|
+
return `/${tab.slug}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
246
331
|
function parseVersionParts(version: string): number[] {
|
|
247
332
|
const parts = version.match(/\d+/g);
|
|
248
333
|
return parts ? parts.map((n) => Number(n)) : [];
|
|
@@ -258,6 +343,109 @@ function compareVersionParts(a: number[], b: number[]): number {
|
|
|
258
343
|
return 0;
|
|
259
344
|
}
|
|
260
345
|
|
|
346
|
+
const FOOTER_SOCIAL_PRESETS: Record<string, { label: string; icon: string }> = {
|
|
347
|
+
x: { label: 'X', icon: 'x-twitter' },
|
|
348
|
+
twitter: { label: 'X', icon: 'x-twitter' },
|
|
349
|
+
github: { label: 'GitHub', icon: 'github' },
|
|
350
|
+
gitlab: { label: 'GitLab', icon: 'gitlab' },
|
|
351
|
+
linkedin: { label: 'LinkedIn', icon: 'linkedin' },
|
|
352
|
+
discord: { label: 'Discord', icon: 'discord' },
|
|
353
|
+
youtube: { label: 'YouTube', icon: 'youtube' },
|
|
354
|
+
facebook: { label: 'Facebook', icon: 'facebook' },
|
|
355
|
+
instagram: { label: 'Instagram', icon: 'instagram' },
|
|
356
|
+
slack: { label: 'Slack', icon: 'slack' },
|
|
357
|
+
medium: { label: 'Medium', icon: 'medium' },
|
|
358
|
+
reddit: { label: 'Reddit', icon: 'reddit' },
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
function normalizeSocialKey(value: string): string {
|
|
362
|
+
return value.trim().toLowerCase().replace(/[\s_]+/g, '-');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function titleCaseWords(value: string): string {
|
|
366
|
+
const normalized = value.trim().replace(/[_-]+/g, ' ');
|
|
367
|
+
if (!normalized) return 'Social';
|
|
368
|
+
return normalized
|
|
369
|
+
.split(/\s+/)
|
|
370
|
+
.filter(Boolean)
|
|
371
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
372
|
+
.join(' ');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function trimString(value: unknown): string | undefined {
|
|
376
|
+
if (typeof value !== 'string') return undefined;
|
|
377
|
+
const trimmed = value.trim();
|
|
378
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function normalizeFooterSocialMap(value: unknown): VeluFooterSocialLink[] {
|
|
382
|
+
if (!isRecord(value)) return [];
|
|
383
|
+
const links: VeluFooterSocialLink[] = [];
|
|
384
|
+
for (const [rawKey, rawHref] of Object.entries(value)) {
|
|
385
|
+
const href = trimString(rawHref);
|
|
386
|
+
if (!href) continue;
|
|
387
|
+
const key = normalizeSocialKey(rawKey);
|
|
388
|
+
if (!key) continue;
|
|
389
|
+
const preset = FOOTER_SOCIAL_PRESETS[key];
|
|
390
|
+
links.push({
|
|
391
|
+
key,
|
|
392
|
+
href,
|
|
393
|
+
label: preset?.label ?? titleCaseWords(key),
|
|
394
|
+
icon: preset?.icon ?? key,
|
|
395
|
+
iconType: 'brands',
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return links;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function normalizeFooterSocialList(value: unknown): VeluFooterSocialLink[] {
|
|
402
|
+
if (!Array.isArray(value)) return [];
|
|
403
|
+
const links: VeluFooterSocialLink[] = [];
|
|
404
|
+
for (const entry of value) {
|
|
405
|
+
if (!isRecord(entry)) continue;
|
|
406
|
+
const href = trimString(entry.href) ?? trimString(entry.url);
|
|
407
|
+
if (!href) continue;
|
|
408
|
+
const key = normalizeSocialKey(
|
|
409
|
+
trimString(entry.type) ?? trimString(entry.icon) ?? trimString(entry.label) ?? 'social',
|
|
410
|
+
);
|
|
411
|
+
const preset = FOOTER_SOCIAL_PRESETS[key];
|
|
412
|
+
links.push({
|
|
413
|
+
key,
|
|
414
|
+
href,
|
|
415
|
+
label: trimString(entry.label) ?? preset?.label ?? titleCaseWords(key),
|
|
416
|
+
icon: trimString(entry.icon) ?? preset?.icon ?? key,
|
|
417
|
+
iconType: trimString(entry.iconType) ?? 'brands',
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return links;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function dedupeFooterSocialLinks(links: VeluFooterSocialLink[]): VeluFooterSocialLink[] {
|
|
424
|
+
const seen = new Set<string>();
|
|
425
|
+
const unique: VeluFooterSocialLink[] = [];
|
|
426
|
+
for (const link of links) {
|
|
427
|
+
const dedupeKey = `${link.href}::${link.icon}`;
|
|
428
|
+
if (seen.has(dedupeKey)) continue;
|
|
429
|
+
seen.add(dedupeKey);
|
|
430
|
+
unique.push(link);
|
|
431
|
+
}
|
|
432
|
+
return unique;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export interface VeluBannerConfig {
|
|
436
|
+
content: string;
|
|
437
|
+
dismissible: boolean;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function getBannerConfig(): VeluBannerConfig | null {
|
|
441
|
+
const config = loadVeluConfig();
|
|
442
|
+
const banner = config.banner;
|
|
443
|
+
if (!banner) return null;
|
|
444
|
+
const content = typeof banner.content === 'string' ? banner.content.trim() : '';
|
|
445
|
+
if (!content) return null;
|
|
446
|
+
return { content, dismissible: banner.dismissible === true };
|
|
447
|
+
}
|
|
448
|
+
|
|
261
449
|
export function getExternalTabs(): Array<{ label: string; href: string }> {
|
|
262
450
|
const config = loadVeluConfig();
|
|
263
451
|
const tabs = config.navigation?.tabs ?? [];
|
|
@@ -294,6 +482,14 @@ export function getGlobalAnchors(): VeluAnchor[] {
|
|
|
294
482
|
);
|
|
295
483
|
}
|
|
296
484
|
|
|
485
|
+
export function getFooterSocials(): VeluFooterSocialLink[] {
|
|
486
|
+
const config = loadVeluConfig();
|
|
487
|
+
const source = config.footer?.socials ?? config.footerSocials;
|
|
488
|
+
const fromMap = normalizeFooterSocialMap(source);
|
|
489
|
+
const fromList = normalizeFooterSocialList(source);
|
|
490
|
+
return dedupeFooterSocialLinks([...fromMap, ...fromList]);
|
|
491
|
+
}
|
|
492
|
+
|
|
297
493
|
export interface VeluTabMenuDefinition {
|
|
298
494
|
tab: string;
|
|
299
495
|
items: Array<{
|
|
@@ -357,6 +553,46 @@ export function getLanguages(): string[] {
|
|
|
357
553
|
return config.languages ?? [];
|
|
358
554
|
}
|
|
359
555
|
|
|
556
|
+
export function getDropdownOptions(): VeluDropdownOption[] {
|
|
557
|
+
const raw = loadRawConfig();
|
|
558
|
+
const navigation = isRecord(raw.navigation) ? raw.navigation : {};
|
|
559
|
+
const dropdowns = Array.isArray(navigation.dropdowns) ? navigation.dropdowns : [];
|
|
560
|
+
if (dropdowns.length === 0) return [];
|
|
561
|
+
|
|
562
|
+
const allTabs = loadVeluConfig().navigation.tabs ?? [];
|
|
563
|
+
const out: VeluDropdownOption[] = [];
|
|
564
|
+
|
|
565
|
+
dropdowns.forEach((entry, index) => {
|
|
566
|
+
if (!isRecord(entry)) return;
|
|
567
|
+
const dropdown = trimString(entry.dropdown);
|
|
568
|
+
if (!dropdown) return;
|
|
569
|
+
|
|
570
|
+
const slug = slugify(trimString(entry.slug) ?? dropdown, `dropdown-${index + 1}`);
|
|
571
|
+
const scopedTabs = allTabs.filter((tab) => {
|
|
572
|
+
const tabSlug = tab.slug ?? '';
|
|
573
|
+
return tabSlug === slug || tabSlug.startsWith(`${slug}/`);
|
|
574
|
+
});
|
|
575
|
+
const firstTab = scopedTabs[0];
|
|
576
|
+
const firstRoute = firstTab ? resolveFirstRouteInTab(firstTab) : undefined;
|
|
577
|
+
const href = trimString(entry.href);
|
|
578
|
+
const defaultPath = withTrailingSlashPath(firstRoute ?? href ?? `/${slug}`);
|
|
579
|
+
|
|
580
|
+
out.push({
|
|
581
|
+
dropdown,
|
|
582
|
+
slug,
|
|
583
|
+
description: trimString(entry.description),
|
|
584
|
+
icon: trimString(entry.icon),
|
|
585
|
+
iconType: trimString(entry.iconType),
|
|
586
|
+
tabSlugs: scopedTabs
|
|
587
|
+
.map((tab) => tab.slug)
|
|
588
|
+
.filter((tabSlug): tabSlug is string => typeof tabSlug === 'string' && tabSlug.length > 0),
|
|
589
|
+
defaultPath,
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
return out;
|
|
594
|
+
}
|
|
595
|
+
|
|
360
596
|
export function getProductOptions(): VeluProductOption[] {
|
|
361
597
|
const config = loadVeluConfig();
|
|
362
598
|
const products = (config.navigation.products ?? []).filter((p) => !p.hidden);
|
|
@@ -484,6 +720,10 @@ export interface VeluResolvedSeoConfig {
|
|
|
484
720
|
indexing: 'navigable' | 'all';
|
|
485
721
|
}
|
|
486
722
|
|
|
723
|
+
export interface VeluResolvedMetadataConfig {
|
|
724
|
+
timestamp: boolean;
|
|
725
|
+
}
|
|
726
|
+
|
|
487
727
|
function normalizePlaygroundDisplay(api: VeluApiConfig | undefined): PlaygroundDisplayMode {
|
|
488
728
|
const display = api?.playground?.display;
|
|
489
729
|
if (display === 'interactive' || display === 'simple' || display === 'none') return display;
|
|
@@ -629,6 +869,13 @@ export function getSeoConfig(): VeluResolvedSeoConfig {
|
|
|
629
869
|
};
|
|
630
870
|
}
|
|
631
871
|
|
|
872
|
+
export function getMetadataConfig(): VeluResolvedMetadataConfig {
|
|
873
|
+
const config = loadVeluConfig();
|
|
874
|
+
return {
|
|
875
|
+
timestamp: config.metadata?.timestamp === true,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
632
879
|
export function getSiteName(): string {
|
|
633
880
|
const config = loadVeluConfig();
|
|
634
881
|
const fromName = normalizeAssetPath(config.name);
|
|
@@ -638,6 +885,11 @@ export function getSiteName(): string {
|
|
|
638
885
|
return 'Velu Docs';
|
|
639
886
|
}
|
|
640
887
|
|
|
888
|
+
export function getSiteDescription(): string | undefined {
|
|
889
|
+
const config = loadVeluConfig();
|
|
890
|
+
return normalizeAssetPath(config.description);
|
|
891
|
+
}
|
|
892
|
+
|
|
641
893
|
export function getSiteFavicon(): string | undefined {
|
|
642
894
|
const config = loadVeluConfig();
|
|
643
895
|
const asset = normalizeThemeAsset(config.favicon);
|
|
@@ -656,6 +908,51 @@ export function getSitePrimaryColor(): string | undefined {
|
|
|
656
908
|
return normalizeAssetPath(colors.primary) ?? normalizeAssetPath(colors.light) ?? normalizeAssetPath(colors.dark);
|
|
657
909
|
}
|
|
658
910
|
|
|
911
|
+
export interface VeluContextualOption {
|
|
912
|
+
id: string;
|
|
913
|
+
title: string;
|
|
914
|
+
description: string;
|
|
915
|
+
href?: string;
|
|
916
|
+
type: 'builtin' | 'custom';
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const CONTEXTUAL_PRESETS: Record<string, { title: string; description: string }> = {
|
|
920
|
+
copy: { title: 'Copy page', description: 'Copy page as Markdown for LLMs' },
|
|
921
|
+
view: { title: 'View as Markdown', description: 'View this page in Markdown format' },
|
|
922
|
+
chatgpt: { title: 'Open in ChatGPT', description: 'Ask questions about this page' },
|
|
923
|
+
claude: { title: 'Open in Claude', description: 'Ask questions about this page' },
|
|
924
|
+
perplexity: { title: 'Open in Perplexity', description: 'Ask questions about this page' },
|
|
925
|
+
grok: { title: 'Open in Grok', description: 'Ask questions about this page' },
|
|
926
|
+
mcp: { title: 'Copy MCP URL', description: 'Copy the MCP server URL' },
|
|
927
|
+
'add-mcp': { title: 'Copy MCP install command', description: 'Copy npx command to install MCP server' },
|
|
928
|
+
cursor: { title: 'Connect to Cursor', description: 'Install MCP Server on Cursor' },
|
|
929
|
+
vscode: { title: 'Connect to VS Code', description: 'Install MCP Server on VS Code' },
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const DEFAULT_CONTEXTUAL_OPTIONS = ['copy', 'chatgpt', 'claude', 'add-mcp', 'cursor', 'vscode'];
|
|
933
|
+
|
|
934
|
+
export function getContextualOptions(): VeluContextualOption[] {
|
|
935
|
+
const config = loadVeluConfig();
|
|
936
|
+
const raw = config.contextual?.options;
|
|
937
|
+
|
|
938
|
+
const ids = raw ?? DEFAULT_CONTEXTUAL_OPTIONS;
|
|
939
|
+
|
|
940
|
+
return ids.map((entry, index) => {
|
|
941
|
+
if (typeof entry === 'string') {
|
|
942
|
+
const preset = CONTEXTUAL_PRESETS[entry];
|
|
943
|
+
if (!preset) return null;
|
|
944
|
+
return { id: entry, title: preset.title, description: preset.description, type: 'builtin' as const };
|
|
945
|
+
}
|
|
946
|
+
return {
|
|
947
|
+
id: `custom-${index}`,
|
|
948
|
+
title: entry.title,
|
|
949
|
+
description: entry.description ?? '',
|
|
950
|
+
href: entry.href,
|
|
951
|
+
type: 'custom' as const,
|
|
952
|
+
};
|
|
953
|
+
}).filter((item): item is VeluContextualOption => item !== null);
|
|
954
|
+
}
|
|
955
|
+
|
|
659
956
|
export function getSiteOrigin(): string {
|
|
660
957
|
const seo = getSeoConfig();
|
|
661
958
|
const envCandidates = [
|
package/src/validate.ts
CHANGED
|
@@ -115,6 +115,7 @@ type VeluOpenApiSource = string | string[] | Record<string, unknown>;
|
|
|
115
115
|
|
|
116
116
|
interface VeluConfig {
|
|
117
117
|
$schema?: string;
|
|
118
|
+
variables?: Record<string, string>;
|
|
118
119
|
icons?: {
|
|
119
120
|
library?: "fontawesome" | "lucide" | "tabler";
|
|
120
121
|
};
|
|
@@ -145,6 +146,13 @@ interface VeluConfig {
|
|
|
145
146
|
};
|
|
146
147
|
};
|
|
147
148
|
};
|
|
149
|
+
metadata?: {
|
|
150
|
+
timestamp?: boolean;
|
|
151
|
+
};
|
|
152
|
+
footer?: {
|
|
153
|
+
socials?: Record<string, unknown>;
|
|
154
|
+
};
|
|
155
|
+
footerSocials?: Record<string, unknown>;
|
|
148
156
|
navigation: {
|
|
149
157
|
openapi?: VeluOpenApiSource;
|
|
150
158
|
asyncapi?: VeluOpenApiSource;
|