jamdesk 1.1.89 → 1.1.91

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 (85) hide show
  1. package/dist/__tests__/integration/validate.integration.test.js +2 -1
  2. package/dist/__tests__/integration/validate.integration.test.js.map +1 -1
  3. package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts +2 -0
  4. package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts.map +1 -0
  5. package/dist/__tests__/unit/dev-workspace-symlinks.test.js +112 -0
  6. package/dist/__tests__/unit/dev-workspace-symlinks.test.js.map +1 -0
  7. package/dist/__tests__/unit/docs-config-discovery.test.d.ts +2 -0
  8. package/dist/__tests__/unit/docs-config-discovery.test.d.ts.map +1 -0
  9. package/dist/__tests__/unit/docs-config-discovery.test.js +190 -0
  10. package/dist/__tests__/unit/docs-config-discovery.test.js.map +1 -0
  11. package/dist/__tests__/unit/docs-config.test.js +2 -1
  12. package/dist/__tests__/unit/docs-config.test.js.map +1 -1
  13. package/dist/__tests__/unit/language-filter.test.d.ts +2 -0
  14. package/dist/__tests__/unit/language-filter.test.d.ts.map +1 -0
  15. package/dist/__tests__/unit/language-filter.test.js +166 -0
  16. package/dist/__tests__/unit/language-filter.test.js.map +1 -0
  17. package/dist/__tests__/unit/output.test.d.ts +2 -0
  18. package/dist/__tests__/unit/output.test.d.ts.map +1 -0
  19. package/dist/__tests__/unit/output.test.js +61 -0
  20. package/dist/__tests__/unit/output.test.js.map +1 -0
  21. package/dist/commands/dev.d.ts.map +1 -1
  22. package/dist/commands/dev.js +4 -1
  23. package/dist/commands/dev.js.map +1 -1
  24. package/dist/commands/doctor.d.ts.map +1 -1
  25. package/dist/commands/doctor.js +14 -12
  26. package/dist/commands/doctor.js.map +1 -1
  27. package/dist/commands/validate.d.ts.map +1 -1
  28. package/dist/commands/validate.js +14 -2
  29. package/dist/commands/validate.js.map +1 -1
  30. package/dist/lib/docs-config.d.ts +54 -3
  31. package/dist/lib/docs-config.d.ts.map +1 -1
  32. package/dist/lib/docs-config.js +126 -8
  33. package/dist/lib/docs-config.js.map +1 -1
  34. package/dist/lib/language-filter.d.ts +31 -0
  35. package/dist/lib/language-filter.d.ts.map +1 -0
  36. package/dist/lib/language-filter.js +14 -0
  37. package/dist/lib/language-filter.js.map +1 -0
  38. package/package.json +1 -1
  39. package/vendored/app/api/r2/[project]/[...path]/route.ts +14 -9
  40. package/vendored/app/layout.tsx +2 -2
  41. package/vendored/components/HtmlLangSync.tsx +3 -2
  42. package/vendored/components/mdx/Accordion.tsx +1 -1
  43. package/vendored/components/mdx/Card.tsx +1 -1
  44. package/vendored/components/mdx/CodeGroup.tsx +18 -23
  45. package/vendored/components/mdx/Color.tsx +0 -1
  46. package/vendored/components/mdx/Icon.tsx +1 -1
  47. package/vendored/components/mdx/MDXComponents.tsx +92 -66
  48. package/vendored/components/mdx/OpenApiEndpoint.tsx +0 -1
  49. package/vendored/components/mdx/ParamField.tsx +0 -1
  50. package/vendored/components/mdx/RequestExample.tsx +0 -1
  51. package/vendored/components/mdx/ResponseExample.tsx +0 -1
  52. package/vendored/components/mdx/Steps.tsx +12 -3
  53. package/vendored/components/mdx/Table.tsx +8 -2
  54. package/vendored/components/mdx/Tabs.tsx +1 -1
  55. package/vendored/components/mdx/Tree.tsx +6 -4
  56. package/vendored/components/navigation/Header.tsx +7 -5
  57. package/vendored/components/navigation/LanguageSelector.tsx +32 -7
  58. package/vendored/components/navigation/TableOfContents.tsx +1 -1
  59. package/vendored/components/navigation/TabsNav.tsx +17 -5
  60. package/vendored/components/search/SearchModal.tsx +41 -36
  61. package/vendored/components/ui/CodePanel.tsx +2 -2
  62. package/vendored/hooks/useChat.ts +1 -1
  63. package/vendored/hooks/useShikiHighlight.ts +7 -1
  64. package/vendored/lib/build/error-parser.ts +38 -12
  65. package/vendored/lib/code-utils.ts +6 -2
  66. package/vendored/lib/health-checks.ts +2 -2
  67. package/vendored/lib/language-utils.ts +53 -2
  68. package/vendored/lib/layout-helpers.tsx +2 -1
  69. package/vendored/lib/mdx-inline-components.ts +1 -1
  70. package/vendored/lib/navigation-resolver.ts +0 -69
  71. package/vendored/lib/normalize-config.ts +1 -1
  72. package/vendored/lib/openapi/generator.ts +3 -3
  73. package/vendored/lib/openapi/parser.ts +14 -6
  74. package/vendored/lib/openapi/validator.ts +2 -2
  75. package/vendored/lib/openapi-isr.ts +4 -1
  76. package/vendored/lib/public-paths-resolver.ts +7 -6
  77. package/vendored/lib/redis.ts +2 -2
  78. package/vendored/lib/rehype-code-meta.ts +2 -2
  79. package/vendored/lib/render-doc-page.tsx +2 -2
  80. package/vendored/lib/seo.ts +21 -6
  81. package/vendored/lib/shiki-highlighter.ts +1 -1
  82. package/vendored/lib/snippet-loader-isr.ts +1 -1
  83. package/vendored/lib/validate-config.ts +136 -8
  84. package/vendored/shared/status-reporter.ts +12 -0
  85. package/vendored/workspace-package-lock.json +16 -16
@@ -1,4 +1,15 @@
1
- import { isValidElement, type ReactNode } from 'react';
1
+ import {
2
+ isValidElement,
3
+ type AnchorHTMLAttributes,
4
+ type BlockquoteHTMLAttributes,
5
+ type HTMLAttributes,
6
+ type ImgHTMLAttributes,
7
+ type OlHTMLAttributes,
8
+ type ReactNode,
9
+ type TableHTMLAttributes,
10
+ type TdHTMLAttributes,
11
+ type ThHTMLAttributes,
12
+ } from 'react';
2
13
  import { Card } from './Card';
3
14
  import { Note, Info, Warning, Tip, Check, Danger, Callout } from './Callouts';
4
15
  import { Accordion, AccordionGroup } from './Accordion';
@@ -41,14 +52,20 @@ import { Visibility } from './Visibility';
41
52
  * 2. language-* class on code element (standard markdown)
42
53
  * 3. language-* class on pre element (fallback for edge cases)
43
54
  */
44
- function getCodeLanguage(preProps: any, children: ReactNode): string {
55
+ type PreLikeProps = {
56
+ 'data-language'?: string;
57
+ className?: string;
58
+ };
59
+ type CodeLikeProps = { className?: string };
60
+
61
+ function getCodeLanguage(preProps: PreLikeProps, children: ReactNode): string {
45
62
  // Check data-language on pre element (added by Shiki transformer)
46
63
  if (preProps['data-language']) {
47
64
  return preProps['data-language'];
48
65
  }
49
66
  // Check code element's className for language-* class
50
67
  if (isValidElement(children)) {
51
- const codeProps = children.props as any;
68
+ const codeProps = children.props as CodeLikeProps;
52
69
  const className = codeProps?.className || '';
53
70
  const match = className.match(/language-(\w+)/);
54
71
  if (match) return match[1];
@@ -70,7 +87,7 @@ function extractTextFromChildren(children: ReactNode): string {
70
87
  if (!children) return '';
71
88
 
72
89
  if (isValidElement(children)) {
73
- const props = children.props as any;
90
+ const props = children.props as { children?: ReactNode };
74
91
  return extractTextFromChildren(props?.children);
75
92
  }
76
93
 
@@ -239,9 +256,9 @@ export const MDXComponents = {
239
256
  // Sized images from preprocess-mdx (![alt](url =WIDTHx) syntax).
240
257
  // These are output as <SizedImage> JSX so they go through component mapping
241
258
  // (raw <img> JSX in MDX bypasses the components provider).
242
- SizedImage: ({ src, alt, width, height, className, ...rest }: any) => (
259
+ SizedImage: ({ src, alt, width, height, className, ..._rest }: Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> & { src?: string }) => (
243
260
  <ZoomableImage
244
- src={src}
261
+ src={src ?? ''}
245
262
  alt={alt || ''}
246
263
  width={width}
247
264
  height={height}
@@ -269,11 +286,20 @@ export const MDXComponents = {
269
286
  noStyle, // Explicitly destructure to prevent React warning about unknown DOM prop
270
287
  nostyle, // Also handle lowercase version from HTML parsing
271
288
  // Destructure and ignore any other props to prevent them from being passed to DOM
272
- ...rest
273
- }: any) => {
289
+ ..._rest
290
+ }: Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> & {
291
+ src?: string;
292
+ class?: string;
293
+ 'data-no-zoom'?: string | boolean;
294
+ 'data-no-style'?: string | boolean;
295
+ noZoom?: string | boolean;
296
+ nozoom?: string | boolean;
297
+ noStyle?: string | boolean;
298
+ nostyle?: string | boolean;
299
+ }) => {
274
300
  // Check if this is a video file — render <Video> instead of <ZoomableImage>
275
301
  if (isVideoUrl(src)) {
276
- return <Video src={src} title={alt || undefined} />;
302
+ return <Video src={src ?? ''} title={alt || undefined} />;
277
303
  }
278
304
 
279
305
  // Check for data-no-zoom attribute or noZoom prop (handle both camelCase and lowercase)
@@ -295,7 +321,7 @@ export const MDXComponents = {
295
321
 
296
322
  return (
297
323
  <ZoomableImage
298
- src={src}
324
+ src={src ?? ''}
299
325
  alt={alt || ''}
300
326
  width={width}
301
327
  height={height}
@@ -308,64 +334,64 @@ export const MDXComponents = {
308
334
  />
309
335
  );
310
336
  },
311
- h1: (props: any) => (
312
- <h1
313
- className="text-3xl font-bold text-theme-text-primary mt-8 mb-4 tracking-tight"
314
- {...props}
337
+ h1: (props: HTMLAttributes<HTMLHeadingElement>) => (
338
+ <h1
339
+ className="text-3xl font-bold text-theme-text-primary mt-8 mb-4 tracking-tight"
340
+ {...props}
315
341
  />
316
342
  ),
317
- h2: (props: any) => (
318
- <h2
319
- className="text-2xl font-semibold text-theme-text-primary mt-10 mb-4 tracking-tight scroll-mt-20"
320
- {...props}
343
+ h2: (props: HTMLAttributes<HTMLHeadingElement>) => (
344
+ <h2
345
+ className="text-2xl font-semibold text-theme-text-primary mt-10 mb-4 tracking-tight scroll-mt-20"
346
+ {...props}
321
347
  />
322
348
  ),
323
- h3: (props: any) => (
324
- <h3
325
- className="text-xl font-semibold text-theme-text-primary mt-8 mb-3 scroll-mt-20"
326
- {...props}
349
+ h3: (props: HTMLAttributes<HTMLHeadingElement>) => (
350
+ <h3
351
+ className="text-xl font-semibold text-theme-text-primary mt-8 mb-3 scroll-mt-20"
352
+ {...props}
327
353
  />
328
354
  ),
329
- h4: (props: any) => (
330
- <h4
331
- className="text-lg font-semibold text-theme-text-primary mt-6 mb-2 scroll-mt-20"
332
- {...props}
355
+ h4: (props: HTMLAttributes<HTMLHeadingElement>) => (
356
+ <h4
357
+ className="text-lg font-semibold text-theme-text-primary mt-6 mb-2 scroll-mt-20"
358
+ {...props}
333
359
  />
334
360
  ),
335
- p: (props: any) => (
336
- <p
337
- className="text-theme-text-secondary leading-7 mb-4"
338
- {...props}
361
+ p: (props: HTMLAttributes<HTMLParagraphElement>) => (
362
+ <p
363
+ className="text-theme-text-secondary leading-7 mb-4"
364
+ {...props}
339
365
  />
340
366
  ),
341
- ul: ({ class: htmlClass, className, ...props }: any) => (
342
- <ul
343
- className={`list-disc list-inside text-theme-text-secondary space-y-2 mb-4 ml-4 ${htmlClass || ''} ${className || ''}`.trim()}
344
- {...props}
367
+ ul: ({ class: htmlClass, className, ...props }: HTMLAttributes<HTMLUListElement> & { class?: string }) => (
368
+ <ul
369
+ className={`list-disc list-inside text-theme-text-secondary space-y-2 mb-4 ml-4 ${htmlClass || ''} ${className || ''}`.trim()}
370
+ {...props}
345
371
  />
346
372
  ),
347
- ol: ({ class: htmlClass, className, ...props }: any) => (
348
- <ol
349
- className={`list-decimal list-inside text-theme-text-secondary space-y-2 mb-4 ml-4 ${htmlClass || ''} ${className || ''}`.trim()}
350
- {...props}
373
+ ol: ({ class: htmlClass, className, ...props }: OlHTMLAttributes<HTMLOListElement> & { class?: string }) => (
374
+ <ol
375
+ className={`list-decimal list-inside text-theme-text-secondary space-y-2 mb-4 ml-4 ${htmlClass || ''} ${className || ''}`.trim()}
376
+ {...props}
351
377
  />
352
378
  ),
353
- li: (props: any) => (
354
- <li
355
- className="text-theme-text-secondary marker:text-theme-marker"
356
- {...props}
379
+ li: (props: HTMLAttributes<HTMLLIElement>) => (
380
+ <li
381
+ className="text-theme-text-secondary marker:text-theme-marker"
382
+ {...props}
357
383
  />
358
384
  ),
359
385
  // NOTE: page.tsx overrides this for hostAtDocs sites to auto-prefix links.
360
386
  // If you change styling here, update the override in app/[[...slug]]/page.tsx too.
361
- a: ({ ariaLabel, ...props }: any) => (
387
+ a: ({ ariaLabel, ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { ariaLabel?: string }) => (
362
388
  <a
363
389
  className="text-theme-accent hover:text-theme-accent-hover transition-colors"
364
390
  aria-label={ariaLabel}
365
391
  {...props}
366
392
  />
367
393
  ),
368
- blockquote: (props: any) => (
394
+ blockquote: (props: BlockquoteHTMLAttributes<HTMLQuoteElement>) => (
369
395
  <blockquote
370
396
  className="border-l-4 pl-4 text-theme-text-tertiary my-6 not-italic"
371
397
  style={{ borderColor: 'var(--color-border)' }}
@@ -373,7 +399,7 @@ export const MDXComponents = {
373
399
  />
374
400
  ),
375
401
  // Custom pre component to wrap code blocks with titles in CodePanel
376
- pre: ({ children, 'data-title': dataTitle, ...props }: any) => {
402
+ pre: ({ children, 'data-title': dataTitle, ...props }: HTMLAttributes<HTMLPreElement> & { 'data-title'?: string }) => {
377
403
  const language = getCodeLanguage(props, children);
378
404
 
379
405
  // Check for mermaid diagrams - render with Mermaid component
@@ -411,41 +437,41 @@ export const MDXComponents = {
411
437
  );
412
438
  },
413
439
  // Inline code styling is done via CSS in base.css (.prose :not(pre) > code)
414
- table: (props: any) => (
440
+ table: (props: TableHTMLAttributes<HTMLTableElement>) => (
415
441
  <div className="overflow-x-auto my-6">
416
- <table
417
- className="w-full text-sm border-collapse"
418
- {...props}
442
+ <table
443
+ className="w-full text-sm border-collapse"
444
+ {...props}
419
445
  />
420
446
  </div>
421
447
  ),
422
- th: (props: any) => (
423
- <th
424
- className="text-left font-semibold text-theme-text-primary p-3 border-b-2 border-theme-border bg-theme-bg-secondary"
425
- {...props}
448
+ th: (props: ThHTMLAttributes<HTMLTableHeaderCellElement>) => (
449
+ <th
450
+ className="text-left font-semibold text-theme-text-primary p-3 border-b-2 border-theme-border bg-theme-bg-secondary"
451
+ {...props}
426
452
  />
427
453
  ),
428
- td: (props: any) => (
429
- <td
430
- className="p-3 border-b border-theme-border text-theme-text-secondary"
431
- {...props}
454
+ td: (props: TdHTMLAttributes<HTMLTableDataCellElement>) => (
455
+ <td
456
+ className="p-3 border-b border-theme-border text-theme-text-secondary"
457
+ {...props}
432
458
  />
433
459
  ),
434
- hr: (props: any) => (
435
- <hr
436
- className="border-theme-border my-8"
437
- {...props}
460
+ hr: (props: HTMLAttributes<HTMLHRElement>) => (
461
+ <hr
462
+ className="border-theme-border my-8"
463
+ {...props}
438
464
  />
439
465
  ),
440
466
  // Generic div handler to convert class to className
441
- div: ({ class: htmlClass, className, ...props }: any) => (
442
- <div
443
- className={`${htmlClass || ''} ${className || ''}`.trim() || undefined}
444
- {...props}
467
+ div: ({ class: htmlClass, className, ...props }: HTMLAttributes<HTMLDivElement> & { class?: string }) => (
468
+ <div
469
+ className={`${htmlClass || ''} ${className || ''}`.trim() || undefined}
470
+ {...props}
445
471
  />
446
472
  ),
447
473
  // Generic span handler to convert class to className
448
- span: ({ class: htmlClass, className, ...props }: any) => (
474
+ span: ({ class: htmlClass, className, ...props }: HTMLAttributes<HTMLSpanElement> & { class?: string }) => (
449
475
  <span
450
476
  className={`${htmlClass || ''} ${className || ''}`.trim() || undefined}
451
477
  {...props}
@@ -77,7 +77,6 @@ function renderSchemaType(schema: JsonSchema): string {
77
77
  // Shiki HTML comes from server-side highlighting of trusted code strings;
78
78
  // sanitization is unnecessary and would break the syntax-color spans.
79
79
  function renderHighlightedHtml(html: string) {
80
- // eslint-disable-next-line react/no-danger
81
80
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
82
81
  }
83
82
 
@@ -30,7 +30,6 @@ export function ParamField({
30
30
  }: ParamFieldProps) {
31
31
  // Determine the parameter name from the prop
32
32
  const paramName = body || header || query || path || '';
33
- const location = body ? 'body' : header ? 'header' : query ? 'query' : path ? 'path' : '';
34
33
 
35
34
  return (
36
35
  <div className="group param-divider py-2.5 first:pt-0 not-prose">
@@ -3,7 +3,6 @@
3
3
  import { ReactNode, Children, isValidElement } from 'react';
4
4
  import { CodePanel, CodePanelTab } from '../ui/CodePanel';
5
5
  import { formatLanguage, getElementProps } from '@/lib/code-utils';
6
- import { getLanguageIcon } from '@/lib/language-icons';
7
6
 
8
7
  interface RequestExampleProps {
9
8
  children: ReactNode;
@@ -3,7 +3,6 @@
3
3
  import { ReactNode, Children, isValidElement } from 'react';
4
4
  import { CodePanel, CodePanelTab } from '../ui/CodePanel';
5
5
  import { getElementProps } from '@/lib/code-utils';
6
- import { getLanguageIcon } from '@/lib/language-icons';
7
6
 
8
7
  interface ResponseExampleProps {
9
8
  children: ReactNode;
@@ -1,6 +1,14 @@
1
1
  'use client';
2
2
 
3
- import { ReactNode, Children, Fragment, cloneElement, isValidElement, memo } from 'react';
3
+ import {
4
+ ReactNode,
5
+ Children,
6
+ Fragment,
7
+ cloneElement,
8
+ isValidElement,
9
+ memo,
10
+ type ReactElement,
11
+ } from 'react';
4
12
  import { getIconClass } from '@/lib/icon-utils';
5
13
  import { useStepSlug } from './StepSlugContext';
6
14
 
@@ -56,10 +64,11 @@ export const Steps = memo(function Steps({ children, titleSize = 'p' }: StepsPro
56
64
  <div className="relative my-8 space-y-0 not-prose">
57
65
  {childrenArray.map((child) => {
58
66
  if (isStepComponent(child)) {
59
- const childProps = (child as any).props as StepProps;
67
+ const stepEl = child as ReactElement<StepProps>;
68
+ const childProps = stepEl.props;
60
69
  const currentStepIndex = stepIndex;
61
70
  stepIndex++;
62
- return cloneElement(child as any, {
71
+ return cloneElement(stepEl, {
63
72
  ...childProps,
64
73
  stepNumber: childProps.stepNumber ?? currentStepIndex + 1,
65
74
  isLast: currentStepIndex === totalSteps - 1,
@@ -170,7 +170,10 @@ export function Table({
170
170
  <tbody>
171
171
  {sortedBodyRows.map((row, index) => {
172
172
  if (isValidElement(row)) {
173
- return cloneElement(row, { key: row.key ?? index, _rowIndex: index } as any);
173
+ return cloneElement(
174
+ row as React.ReactElement<InternalRowProps>,
175
+ { key: row.key ?? index, _rowIndex: index }
176
+ );
174
177
  }
175
178
  return row;
176
179
  })}
@@ -223,7 +226,10 @@ export function Row({
223
226
  <tr className={classes.join(' ')}>
224
227
  {Children.map(children, (child, index) => {
225
228
  if (isValidElement(child)) {
226
- return cloneElement(child, { _isHeaderRow: header, _columnIndex: index } as any);
229
+ return cloneElement(
230
+ child as React.ReactElement<InternalCellProps>,
231
+ { _isHeaderRow: header, _columnIndex: index }
232
+ );
227
233
  }
228
234
  return child;
229
235
  })}
@@ -34,7 +34,7 @@ interface TabProps {
34
34
  * Tab component - represents a single tab panel
35
35
  * Must be used inside a Tabs component
36
36
  */
37
- export const Tab = memo(function Tab({ title, icon, iconType, children }: TabProps) {
37
+ export const Tab = memo(function Tab({ title: _title, icon: _icon, iconType: _iconType, children }: TabProps) {
38
38
  // This component is rendered by Tabs, not directly
39
39
  // The props are extracted by Tabs to build the tab bar
40
40
  return <>{children}</>;
@@ -10,8 +10,6 @@ import {
10
10
  useMemo,
11
11
  useId,
12
12
  ReactNode,
13
- Children,
14
- isValidElement,
15
13
  } from 'react';
16
14
 
17
15
  // =============================================================================
@@ -158,7 +156,11 @@ function TreeRoot({ children }: TreeProps) {
158
156
 
159
157
  traverse(null);
160
158
  return items;
161
- }, [expandedIds, itemsRef.current.size]); // eslint-disable-line react-hooks/exhaustive-deps
159
+ // itemsRef.current.size is intentional — refs don't trigger re-renders, but
160
+ // size changes correlate with register/unregister so this is the best
161
+ // available recompute signal without lifting items into state.
162
+ // eslint-disable-next-line react-hooks/exhaustive-deps
163
+ }, [expandedIds, itemsRef.current.size]);
162
164
 
163
165
  // Initialize expanded state from defaultOpen props
164
166
  useEffect(() => {
@@ -174,7 +176,7 @@ function TreeRoot({ children }: TreeProps) {
174
176
  }
175
177
  setInitialized(true);
176
178
  }
177
- }, [initialized, itemsRef.current.size]); // eslint-disable-line react-hooks/exhaustive-deps
179
+ }, [initialized, itemsRef.current.size]);
178
180
 
179
181
  // Type-ahead search handler
180
182
  const handleTypeAhead = useCallback(
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useMemo, useRef } from 'react';
3
+ import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
4
4
  import { useOnClickOutside } from '@/hooks/useOnClickOutside';
5
5
  import Link from 'next/link';
6
6
  import { usePathname } from 'next/navigation';
@@ -95,8 +95,10 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
95
95
  return resolveNavigation(config, pathname);
96
96
  }, [config, pathname, hasTabs, layout, hasMultipleLanguages]);
97
97
 
98
- // Simple check if current path belongs to a tab (without full navigation resolution)
99
- const pathBelongsToTab = (tab: typeof navigationTabs[0], currentPath: string): boolean => {
98
+ // Simple check if current path belongs to a tab (without full navigation resolution).
99
+ // Pure over its arguments wrapped in useCallback so it's stable for the
100
+ // tab list useMemo below.
101
+ const pathBelongsToTab = useCallback((tab: typeof navigationTabs[0], currentPath: string): boolean => {
100
102
  const path = currentPath.replace(/^\/docs\/?/, '');
101
103
 
102
104
  // Check tab's groups
@@ -124,7 +126,7 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
124
126
  }
125
127
 
126
128
  return false;
127
- };
129
+ }, []);
128
130
 
129
131
  // Build tabs data for header
130
132
  const headerTabs = useMemo(() => {
@@ -175,7 +177,7 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
175
177
  isExternal: false,
176
178
  };
177
179
  });
178
- }, [navigationTabs, resolved, hasTabs, pathname, linkPrefix]);
180
+ }, [navigationTabs, resolved, hasTabs, pathname, linkPrefix, pathBelongsToTab]);
179
181
 
180
182
  // Detect dark mode for logo switching
181
183
  useEffect(() => {
@@ -11,6 +11,7 @@ import {
11
11
  saveLanguagePreference,
12
12
  getLanguagePreference,
13
13
  extractLanguageFromPath,
14
+ toHreflang,
14
15
  } from '@/lib/language-utils';
15
16
  import { useLinkPrefix } from '@/lib/link-prefix-context';
16
17
  import { getUiStrings } from '@/lib/ui-strings';
@@ -51,13 +52,36 @@ export function LanguageSelector({
51
52
  const isNavigating = pendingPathname !== null;
52
53
  const showSpinner = spinnerPathname !== null;
53
54
 
55
+ // Dedupe entries that collapse onto the same BCP 47 tag (e.g. customer
56
+ // declared both `cn` and `zh-Hans`, which would render two identical
57
+ // "简体中文" rows). Prefer the entry whose code already IS the BCP 47 form;
58
+ // fall back to the first occurrence. The server-side warning in
59
+ // buildHreflangAlternates is the authoritative signal — this is the UX
60
+ // safety net so users don't see duplicate rows in the meantime.
61
+ const displayedLanguages = (() => {
62
+ const byTag = new Map<string, ResolvedLanguage>();
63
+ for (const lang of languages) {
64
+ const tag = toHreflang(lang.code);
65
+ const existing = byTag.get(tag);
66
+ if (!existing) {
67
+ byTag.set(tag, lang);
68
+ continue;
69
+ }
70
+ if (existing.code !== tag && lang.code === tag) byTag.set(tag, lang);
71
+ }
72
+ return [...byTag.values()];
73
+ })();
74
+
54
75
  // Find current language
55
- const currentLanguage = languages.find((l) => l.isActive) || languages[0];
76
+ const currentLanguage = displayedLanguages.find((l) => l.isActive) || displayedLanguages[0];
56
77
 
57
78
  // Find actual default language from config
58
- const actualDefault = languages.find((l) => l.isDefault)?.code || defaultLanguage;
79
+ const actualDefault = displayedLanguages.find((l) => l.isDefault)?.code || defaultLanguage;
59
80
 
60
- // Check localStorage preference on mount and redirect if needed
81
+ // Check localStorage preference on mount and redirect if needed.
82
+ // Intentionally mount-only: re-running on pathname/language changes would
83
+ // fight the user's in-session navigation choices (they may have explicitly
84
+ // switched languages this session, which we don't want to revert).
61
85
  useEffect(() => {
62
86
  const savedPref = getLanguagePreference();
63
87
  if (savedPref && currentLanguage && savedPref !== currentLanguage.code) {
@@ -68,18 +92,20 @@ export function LanguageSelector({
68
92
  if (!hasLangInPath) {
69
93
  const basePath = transformLanguagePath(
70
94
  pathname || '/docs',
71
- actualDefault,
72
95
  savedPref,
73
96
  actualDefault
74
97
  );
75
98
  router.replace(`${linkPrefix}${basePath}`);
76
99
  }
77
100
  }
78
- }, []); // Only run once on mount
101
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only by design; see comment above the effect
102
+ }, []);
79
103
 
80
104
  // Handle click outside to close
81
105
  useOnClickOutside(containerRef, () => setIsOpen(false), isOpen);
82
106
 
107
+ // Defined before handleKeyDown so handleKeyDown can include it in its
108
+ // dependency array without TDZ issues.
83
109
  const handleSelectLanguage = useCallback(
84
110
  (lang: ResolvedLanguage) => {
85
111
  // Block double-fire while a previous switch is still navigating —
@@ -95,7 +121,6 @@ export function LanguageSelector({
95
121
 
96
122
  const basePath = transformLanguagePath(
97
123
  pathname || '/docs',
98
- currentLanguage?.code || actualDefault,
99
124
  lang.code,
100
125
  actualDefault
101
126
  );
@@ -228,7 +253,7 @@ export function LanguageSelector({
228
253
  `}
229
254
  style={{ boxShadow: 'var(--shadow-lg)' }}
230
255
  >
231
- {languages.map((lang, index) => (
256
+ {displayedLanguages.map((lang, index) => (
232
257
  <li key={lang.code}>
233
258
  <button
234
259
  role="option"
@@ -494,7 +494,7 @@ export function TableOfContents({ content, className = '' }: TableOfContentsProp
494
494
  return () => {
495
495
  scrollTarget.removeEventListener('scroll', computeActive);
496
496
  };
497
- // eslint-disable-next-line react-hooks/exhaustive-deps
497
+
498
498
  }, [headings, isHidden]);
499
499
 
500
500
  if (headings.length === 0 || isHidden) {
@@ -4,7 +4,12 @@ import { useMemo } from 'react';
4
4
  import Link from 'next/link';
5
5
  import { usePathname } from 'next/navigation';
6
6
  // Icons use Font Awesome CSS classes for lightweight rendering
7
- import type { DocsConfig, TabsPosition } from '@/lib/docs-types';
7
+ import type {
8
+ DocsConfig,
9
+ GroupConfig,
10
+ NavigationPage,
11
+ TabsPosition,
12
+ } from '@/lib/docs-types';
8
13
  import { resolveNavigation } from '@/lib/navigation-resolver';
9
14
  import { getIconClass } from '@/lib/icon-utils';
10
15
  import { getTheme } from '@/themes';
@@ -87,14 +92,21 @@ export function TabsNav({ config, className = '' }: TabsNavProps) {
87
92
  const currentPath = pathname.replace(/^\/docs\/?/, '').replace(/^\//, '');
88
93
  let isActive = false;
89
94
 
90
- const checkPages = (pages: any[]): boolean => {
95
+ const checkPages = (pages: (NavigationPage | GroupConfig)[]): boolean => {
91
96
  for (const page of pages) {
92
- const pagePath = typeof page === 'string' ? page : page.page || page.group;
93
- if (pagePath === currentPath || currentPath.startsWith(pagePath + '/')) {
97
+ const pagePath =
98
+ typeof page === 'string'
99
+ ? page
100
+ : 'page' in page
101
+ ? page.page
102
+ : 'group' in page
103
+ ? page.group
104
+ : undefined;
105
+ if (pagePath && (pagePath === currentPath || currentPath.startsWith(pagePath + '/'))) {
94
106
  return true;
95
107
  }
96
108
  // Check nested groups
97
- if (page.pages) {
109
+ if (typeof page !== 'string' && 'pages' in page && page.pages) {
98
110
  if (checkPages(page.pages)) return true;
99
111
  }
100
112
  }