@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.
@@ -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;