doccupine 0.0.63 → 0.0.64

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 (86) hide show
  1. package/README.md +4 -3
  2. package/dist/lib/structures.js +2 -0
  3. package/dist/templates/app/theme.d.ts +3 -1
  4. package/dist/templates/app/theme.js +17 -15
  5. package/dist/templates/components/Chat.d.ts +1 -1
  6. package/dist/templates/components/Chat.js +251 -158
  7. package/dist/templates/components/DocsSideBar.d.ts +1 -1
  8. package/dist/templates/components/DocsSideBar.js +34 -11
  9. package/dist/templates/components/SideBar.d.ts +1 -1
  10. package/dist/templates/components/SideBar.js +12 -2
  11. package/dist/templates/components/layout/Accordion.d.ts +1 -1
  12. package/dist/templates/components/layout/Accordion.js +1 -1
  13. package/dist/templates/components/layout/ActionBar.d.ts +1 -1
  14. package/dist/templates/components/layout/ActionBar.js +15 -60
  15. package/dist/templates/components/layout/Callout.d.ts +1 -1
  16. package/dist/templates/components/layout/Callout.js +1 -1
  17. package/dist/templates/components/layout/Card.d.ts +1 -1
  18. package/dist/templates/components/layout/Card.js +26 -7
  19. package/dist/templates/components/layout/Columns.d.ts +1 -1
  20. package/dist/templates/components/layout/Columns.js +1 -1
  21. package/dist/templates/components/layout/DocsComponents.d.ts +1 -1
  22. package/dist/templates/components/layout/DocsComponents.js +37 -11
  23. package/dist/templates/components/layout/DocsNavigation.d.ts +1 -1
  24. package/dist/templates/components/layout/DocsNavigation.js +3 -2
  25. package/dist/templates/components/layout/Field.d.ts +1 -1
  26. package/dist/templates/components/layout/Field.js +1 -1
  27. package/dist/templates/components/layout/Footer.d.ts +1 -1
  28. package/dist/templates/components/layout/Footer.js +28 -6
  29. package/dist/templates/components/layout/Header.d.ts +1 -1
  30. package/dist/templates/components/layout/Header.js +10 -12
  31. package/dist/templates/components/layout/SharedStyles.d.ts +1 -1
  32. package/dist/templates/components/layout/SharedStyles.js +26 -2
  33. package/dist/templates/components/layout/StaticLinks.d.ts +1 -1
  34. package/dist/templates/components/layout/StaticLinks.js +7 -3
  35. package/dist/templates/components/layout/Steps.d.ts +1 -1
  36. package/dist/templates/components/layout/Steps.js +7 -2
  37. package/dist/templates/components/layout/Tabs.d.ts +1 -1
  38. package/dist/templates/components/layout/Tabs.js +2 -2
  39. package/dist/templates/components/layout/Update.d.ts +1 -1
  40. package/dist/templates/components/layout/Update.js +1 -1
  41. package/dist/templates/mdx/ai-assistant.mdx.d.ts +1 -1
  42. package/dist/templates/mdx/ai-assistant.mdx.js +8 -0
  43. package/dist/templates/mdx/callouts.mdx.d.ts +1 -1
  44. package/dist/templates/mdx/callouts.mdx.js +6 -2
  45. package/dist/templates/mdx/cards.mdx.d.ts +1 -1
  46. package/dist/templates/mdx/cards.mdx.js +19 -3
  47. package/dist/templates/mdx/columns.mdx.d.ts +1 -1
  48. package/dist/templates/mdx/columns.mdx.js +2 -2
  49. package/dist/templates/mdx/commands.mdx.d.ts +1 -1
  50. package/dist/templates/mdx/commands.mdx.js +10 -2
  51. package/dist/templates/mdx/components.mdx.d.ts +1 -0
  52. package/dist/templates/mdx/components.mdx.js +56 -0
  53. package/dist/templates/mdx/deployment.mdx.d.ts +1 -1
  54. package/dist/templates/mdx/deployment.mdx.js +1 -1
  55. package/dist/templates/mdx/globals.mdx.d.ts +1 -1
  56. package/dist/templates/mdx/globals.mdx.js +5 -0
  57. package/dist/templates/mdx/index.mdx.d.ts +1 -1
  58. package/dist/templates/mdx/index.mdx.js +5 -5
  59. package/dist/templates/mdx/model-context-protocol.mdx.d.ts +1 -1
  60. package/dist/templates/mdx/model-context-protocol.mdx.js +2 -2
  61. package/dist/templates/mdx/navigation.mdx.d.ts +1 -1
  62. package/dist/templates/mdx/navigation.mdx.js +1 -1
  63. package/dist/templates/mdx/platform/external-links.mdx.d.ts +1 -1
  64. package/dist/templates/mdx/platform/external-links.mdx.js +2 -0
  65. package/dist/templates/mdx/platform/fonts-settings.mdx.d.ts +1 -1
  66. package/dist/templates/mdx/platform/fonts-settings.mdx.js +8 -5
  67. package/dist/templates/mdx/platform/index.mdx.d.ts +1 -1
  68. package/dist/templates/mdx/platform/index.mdx.js +10 -1
  69. package/dist/templates/mdx/platform/site-settings.mdx.d.ts +1 -1
  70. package/dist/templates/mdx/platform/site-settings.mdx.js +4 -4
  71. package/dist/templates/mdx/sections.mdx.d.ts +1 -1
  72. package/dist/templates/mdx/sections.mdx.js +2 -2
  73. package/dist/templates/mdx/steps.mdx.d.ts +1 -1
  74. package/dist/templates/mdx/steps.mdx.js +4 -0
  75. package/dist/templates/mdx/tabs.mdx.d.ts +1 -1
  76. package/dist/templates/mdx/tabs.mdx.js +1 -1
  77. package/dist/templates/package.js +4 -4
  78. package/dist/templates/services/llm/types.d.ts +1 -1
  79. package/dist/templates/services/llm/types.js +1 -1
  80. package/dist/templates/services/mcp/server.d.ts +1 -1
  81. package/dist/templates/services/mcp/server.js +1 -1
  82. package/dist/templates/services/mcp/tools.d.ts +1 -1
  83. package/dist/templates/services/mcp/tools.js +1 -5
  84. package/dist/templates/utils/config.d.ts +1 -1
  85. package/dist/templates/utils/config.js +1 -1
  86. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { CHAT_WIDTH } from "../app/theme.js";
1
2
  export const chatTemplate = `"use client";
2
3
  import React, {
3
4
  createContext,
@@ -20,6 +21,7 @@ import {
20
21
  styledAnchor,
21
22
  styledTable,
22
23
  stylesLists,
24
+ StyledSmallButton,
23
25
  } from "@/components/layout/SharedStyled";
24
26
 
25
27
  const mdxComponents = getMDXComponents({});
@@ -49,6 +51,7 @@ const StyledChat = styled.div<{ theme: Theme; $isVisible: boolean }>\`
49
51
  transform: translateX(0);
50
52
  background: \${({ theme }) => theme.colors.light};
51
53
  -webkit-overflow-scrolling: touch;
54
+ opacity: 1;
52
55
 
53
56
  &::-webkit-scrollbar {
54
57
  display: none;
@@ -58,10 +61,11 @@ const StyledChat = styled.div<{ theme: Theme; $isVisible: boolean }>\`
58
61
  !$isVisible &&
59
62
  css\`
60
63
  transform: translateX(100%);
64
+ opacity: 0;
61
65
  \`}
62
66
 
63
67
  \${mq("lg")} {
64
- width: 420px;
68
+ width: ${CHAT_WIDTH}px;
65
69
  border-left: solid 1px \${({ theme }) => theme.colors.grayLight};
66
70
  }
67
71
  \`;
@@ -352,15 +356,17 @@ const StyledChatForm = styled.form<{ theme: Theme; $isVisible: boolean }>\`
352
356
  border-top: solid 1px \${({ theme }) => theme.colors.grayLight};
353
357
  transition: all 0.3s ease;
354
358
  transform: translateX(100%);
359
+ opacity: 0;
355
360
 
356
361
  \${mq("lg")} {
357
- width: 420px;
362
+ width: ${CHAT_WIDTH}px;
358
363
  border-left: solid 1px \${({ theme }) => theme.colors.grayLight};
359
364
  }
360
365
 
361
366
  \${({ $isVisible }) =>
362
367
  $isVisible &&
363
368
  css\`
369
+ opacity: 1;
364
370
  transform: translateX(0);
365
371
  \`}
366
372
 
@@ -369,53 +375,94 @@ const StyledChatForm = styled.form<{ theme: Theme; $isVisible: boolean }>\`
369
375
  }
370
376
  \`;
371
377
 
372
- const StyledChatFixedForm = styled.form<{
378
+ const StyledGlowSmallButton = styled(StyledSmallButton)<{
373
379
  theme: Theme;
374
- $hide: boolean;
380
+ $hasContent: boolean;
375
381
  }>\`
376
- transition: all 0.3s ease;
377
- position: fixed;
378
- bottom: 20px;
379
- left: 20px;
380
- width: calc(100% - 115px);
381
- z-index: 998;
382
-
383
- \${mq("lg")} {
384
- left: 50%;
385
- transform: translateX(-50%) translateY(0);
386
- bottom: initial;
387
- position: absolute;
388
- top: 153px;
389
- width: calc(100% - 320px * 2 - 40px);
390
- opacity: 1;
382
+ @property --gradient-angle {
383
+ syntax: "<angle>";
384
+ initial-value: 0deg;
385
+ inherits: false;
391
386
  }
392
387
 
393
- \${({ $hide }) =>
394
- $hide &&
395
- css\`
396
- transform: translateX(-100px);
388
+ position: relative;
389
+ isolation: isolate;
390
+ margin-right: 0;
391
+ background: \${({ theme }) => theme.colors.light};
392
+ padding: 0;
397
393
 
398
- \${mq("lg")} {
399
- opacity: 0;
400
- transform: translateX(-50%) translateY(-20px);
401
- }
402
- \`}
394
+ &::before {
395
+ content: "";
396
+ inset: -2px;
397
+ border-radius: 8px;
398
+ background: conic-gradient(
399
+ from var(--gradient-angle),
400
+ #cc5555,
401
+ #d9a745,
402
+ #3ab0cc,
403
+ #cc7fc2,
404
+ #4380cc,
405
+ #4c1fa3,
406
+ #cc5555
407
+ );
408
+ opacity: 0;
409
+ transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);
410
+ animation: \${rotateGradient} 3s linear infinite;
411
+ z-index: -1;
412
+ position: absolute;
413
+ top: -2px;
414
+ left: -2px;
415
+ width: calc(100% + 4px);
416
+ height: calc(100% + 4px);
417
+ }
403
418
 
404
- & .loading {
405
- animation: \${loadingAnimation} 1s linear infinite;
419
+ &::after {
420
+ content: "";
421
+ position: absolute;
422
+ inset: -8px;
423
+ border-radius: 14px;
424
+ background: conic-gradient(
425
+ from var(--gradient-angle),
426
+ \${rgba("#ff6b6b", 0.4)},
427
+ \${rgba("#feca57", 0.4)},
428
+ \${rgba("#48dbfb", 0.4)},
429
+ \${rgba("#ff9ff3", 0.4)},
430
+ \${rgba("#54a0ff", 0.4)},
431
+ \${rgba("#5f27cd", 0.4)},
432
+ \${rgba("#ff6b6b", 0.4)}
433
+ );
434
+ opacity: 0;
435
+ transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);
436
+ animation:
437
+ \${rotateGradient} 3s linear infinite,
438
+ \${pulseGlow} 2s ease-in-out infinite;
439
+ z-index: -2;
440
+ pointer-events: none;
406
441
  }
407
- \`;
408
442
 
409
- const StyledChatFixedInner = styled.div\`
410
- margin: auto;
411
- display: flex;
412
- gap: 10px;
413
- justify-content: center;
414
- align-items: center;
443
+ &:hover::before,
444
+ &:hover::after {
445
+ opacity: 1;
446
+ }
415
447
 
416
- \${mq("lg")} {
417
- max-width: 640px;
448
+ & span {
449
+ padding: 6px 8px;
450
+ display: flex;
451
+ background: \${({ theme }) => theme.colors.light};
452
+ border-radius: \${({ theme }) => theme.spacing.radius.xs};
453
+ gap: 6px;
418
454
  }
455
+
456
+ \${({ $hasContent }) =>
457
+ $hasContent &&
458
+ css\`
459
+ &::before {
460
+ opacity: 1;
461
+ }
462
+ &::after {
463
+ opacity: 1;
464
+ }
465
+ \`}
419
466
  \`;
420
467
 
421
468
  const StyledError = styled.div<{ theme: Theme }>\`
@@ -636,6 +683,7 @@ interface RainbowInputProps {
636
683
  placeholder?: string;
637
684
  autoComplete?: string;
638
685
  "aria-label"?: string;
686
+ inputRef?: React.Ref<HTMLInputElement>;
639
687
  }
640
688
 
641
689
  function RainbowInput({
@@ -645,6 +693,7 @@ function RainbowInput({
645
693
  placeholder,
646
694
  autoComplete,
647
695
  "aria-label": ariaLabel,
696
+ inputRef,
648
697
  }: RainbowInputProps) {
649
698
  const [isFocused, setIsFocused] = useState(false);
650
699
  const [isHovered, setIsHovered] = useState(false);
@@ -675,6 +724,7 @@ function RainbowInput({
675
724
  ))}
676
725
  </StyledSparkleContainer>
677
726
  <StyledRainbowInput
727
+ ref={inputRef}
678
728
  id={id}
679
729
  value={value}
680
730
  onChange={onChange}
@@ -688,14 +738,51 @@ function RainbowInput({
688
738
  );
689
739
  }
690
740
 
741
+ function ChatButtonCTA() {
742
+ const { setIsOpen, isOpen, answer, setAnswer, chatInputRef } =
743
+ useContext(ChatContext);
744
+
745
+ return (
746
+ <StyledGlowSmallButton
747
+ onClick={() => {
748
+ const next = !isOpen;
749
+ setIsOpen(next);
750
+ if (next) {
751
+ if (answer.length === 0) {
752
+ setAnswer([
753
+ { text: "Hey there, how can I assist you?", answer: true },
754
+ ]);
755
+ }
756
+ setTimeout(() => {
757
+ chatInputRef.current?.focus();
758
+ }, 350);
759
+ }
760
+ }}
761
+ aria-label="Ask AI Assistant"
762
+ $hasContent={isOpen}
763
+ type="button"
764
+ >
765
+ <span>
766
+ <Sparkles size={16} />
767
+ Ask AI
768
+ </span>
769
+ </StyledGlowSmallButton>
770
+ );
771
+ }
772
+
691
773
  function Chat() {
692
- const [question, setQuestion] = useState("");
693
- const [loading, setLoading] = useState(false);
694
- const [error, setError] = useState<string | null>(null);
695
- const [answer, setAnswer] = useState<Answer[]>([]);
774
+ const {
775
+ isOpen,
776
+ question,
777
+ setQuestion,
778
+ loading,
779
+ error,
780
+ answer,
781
+ ask,
782
+ clearChat,
783
+ chatInputRef,
784
+ } = useContext(ChatContext);
696
785
  const endRef = useRef<HTMLDivElement | null>(null);
697
- const abortRef = useRef<AbortController | null>(null);
698
- const { isOpen, setIsOpen } = useContext(ChatContext);
699
786
 
700
787
  useEffect(() => {
701
788
  endRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
@@ -703,12 +790,111 @@ function Chat() {
703
790
 
704
791
  useEffect(() => {
705
792
  if (answer?.length > 0) {
706
- const el = document.getElementById(
707
- "chat-bottom-input",
708
- ) as HTMLInputElement | null;
709
- el?.focus();
793
+ chatInputRef.current?.focus();
710
794
  }
711
- }, [answer]);
795
+ }, [answer, chatInputRef]);
796
+
797
+ return (
798
+ <>
799
+ <StyledChat $isVisible={isOpen}>
800
+ <StyledChatTitle>
801
+ <StyledChatTitleIconWrapper>
802
+ <Sparkles />
803
+ <h3>AI Assistant</h3>
804
+ </StyledChatTitleIconWrapper>
805
+ <StyledChatCloseButton onClick={clearChat} aria-label="Close chat">
806
+ <X />
807
+ </StyledChatCloseButton>
808
+ </StyledChatTitle>
809
+ {answer &&
810
+ answer.map((a, i) => (
811
+ <StyledAnswer key={i} $isAnswer={a.answer ?? false}>
812
+ {a.answer && a.mdx ? (
813
+ <MDXRemote {...a.mdx} components={mdxComponents} />
814
+ ) : (
815
+ a.text
816
+ )}
817
+ </StyledAnswer>
818
+ ))}
819
+ {loading && (
820
+ <StyledLoading>
821
+ Answering<span>.</span>
822
+ <span>.</span>
823
+ <span>.</span>
824
+ </StyledLoading>
825
+ )}
826
+ {error && (
827
+ <StyledError>
828
+ <strong>Error:</strong> {error}
829
+ </StyledError>
830
+ )}
831
+ <div ref={endRef} />
832
+ </StyledChat>
833
+
834
+ <StyledChatForm onSubmit={ask} $isVisible={isOpen}>
835
+ <RainbowInput
836
+ id="chat-bottom-input"
837
+ inputRef={chatInputRef}
838
+ value={question}
839
+ onChange={(e) => setQuestion(e.target.value)}
840
+ placeholder="Ask AI Assistant..."
841
+ autoComplete="off"
842
+ aria-label="Ask a follow-up question"
843
+ />
844
+ <StyledRainbowButton
845
+ type="submit"
846
+ disabled={loading || question.trim() === ""}
847
+ $hasContent={question.trim().length > 0}
848
+ aria-label={loading ? "Loading response" : "Submit question"}
849
+ >
850
+ {loading ? <LoaderPinwheel className="loading" /> : <ArrowUp />}
851
+ </StyledRainbowButton>
852
+ </StyledChatForm>
853
+ </>
854
+ );
855
+ }
856
+
857
+ const ChatContext = createContext<{
858
+ isOpen: boolean;
859
+ setIsOpen: (isOpen: boolean) => void;
860
+ isChatActive: boolean;
861
+ question: string;
862
+ setQuestion: (q: string) => void;
863
+ loading: boolean;
864
+ error: string | null;
865
+ answer: Answer[];
866
+ setAnswer: (answers: Answer[]) => void;
867
+ ask: (e: React.FormEvent) => void;
868
+ clearChat: () => void;
869
+ chatInputRef: React.RefObject<HTMLInputElement | null>;
870
+ }>({
871
+ isOpen: false,
872
+ setIsOpen: () => {},
873
+ isChatActive: false,
874
+ question: "",
875
+ setQuestion: () => {},
876
+ loading: false,
877
+ error: null,
878
+ answer: [],
879
+ setAnswer: () => {},
880
+ ask: () => {},
881
+ clearChat: () => {},
882
+ chatInputRef: { current: null },
883
+ });
884
+
885
+ interface ChatContextProviderProps {
886
+ children: React.ReactNode;
887
+ isChatActive: boolean;
888
+ }
889
+
890
+ const ChtProvider = ({ children, isChatActive }: ChatContextProviderProps) => {
891
+ const [isOpen, setIsOpen] = useState(false);
892
+ const [question, setQuestion] = useState("");
893
+ const [loading, setLoading] = useState(false);
894
+ const [error, setError] = useState<string | null>(null);
895
+ const [answer, setAnswer] = useState<Answer[]>([]);
896
+ const abortRef = useRef<AbortController | null>(null);
897
+ const chatInputRef = useRef<HTMLInputElement | null>(null);
712
898
 
713
899
  async function ask(e: React.FormEvent) {
714
900
  e.preventDefault();
@@ -730,7 +916,6 @@ function Chat() {
730
916
  abortRef.current = controller;
731
917
 
732
918
  try {
733
- // Build conversation history from previous Q&A pairs
734
919
  const history = answer
735
920
  .filter((a) => a.text.trim() !== "")
736
921
  .map((a) => ({
@@ -757,7 +942,6 @@ function Chat() {
757
942
  throw new Error("Failed to get response reader");
758
943
  }
759
944
 
760
- // Add a placeholder for the streaming answer
761
945
  const streamingAnswerIndex = mergedQuestions.length;
762
946
  setAnswer([...mergedQuestions, { text: "", answer: true }]);
763
947
 
@@ -768,7 +952,6 @@ function Chat() {
768
952
 
769
953
  buffer += decoder.decode(value, { stream: true });
770
954
  const parts = buffer.split("\\n");
771
- // Keep the last (potentially incomplete) part in the buffer
772
955
  buffer = parts.pop() ?? "";
773
956
 
774
957
  for (const line of parts) {
@@ -792,7 +975,6 @@ function Chat() {
792
975
  throw new Error(data.data);
793
976
  } else if (data.type === "done") {
794
977
  const streamedContent = contentParts.join("");
795
- // Finalize with MDX serialization
796
978
  let mdxSource: MDXRemoteSerializeResult | null = null;
797
979
  try {
798
980
  mdxSource = await serialize(streamedContent, {
@@ -838,109 +1020,11 @@ function Chat() {
838
1020
  }
839
1021
  }
840
1022
 
841
- return (
842
- <>
843
- <StyledChatFixedForm onSubmit={ask} $hide={answer?.length > 0}>
844
- <StyledChatFixedInner>
845
- <RainbowInput
846
- value={question}
847
- onChange={(e) => setQuestion(e.target.value)}
848
- placeholder="Ask AI Assistant..."
849
- autoComplete="off"
850
- aria-label="Ask a question about the documentation"
851
- />
852
- <StyledRainbowButton
853
- type="submit"
854
- disabled={loading}
855
- $hasContent={question.trim().length > 0}
856
- aria-label={loading ? "Loading response" : "Submit question"}
857
- >
858
- {loading ? <LoaderPinwheel className="loading" /> : <ArrowUp />}
859
- </StyledRainbowButton>
860
- </StyledChatFixedInner>
861
- </StyledChatFixedForm>
862
-
863
- <StyledChat $isVisible={isOpen}>
864
- <StyledChatTitle>
865
- <StyledChatTitleIconWrapper>
866
- <Sparkles />
867
- <h3>AI Assistant</h3>
868
- </StyledChatTitleIconWrapper>
869
- <StyledChatCloseButton
870
- onClick={() => {
871
- abortRef.current?.abort();
872
- setAnswer([]);
873
- setIsOpen(false);
874
- }}
875
- aria-label="Close chat"
876
- >
877
- <X />
878
- </StyledChatCloseButton>
879
- </StyledChatTitle>
880
- {answer &&
881
- answer.map((a, i) => (
882
- <StyledAnswer key={i} $isAnswer={a.answer ?? false}>
883
- {a.answer && a.mdx ? (
884
- <MDXRemote {...a.mdx} components={mdxComponents} />
885
- ) : (
886
- a.text
887
- )}
888
- </StyledAnswer>
889
- ))}
890
- {loading && (
891
- <StyledLoading>
892
- Answering<span>.</span>
893
- <span>.</span>
894
- <span>.</span>
895
- </StyledLoading>
896
- )}
897
- {error && (
898
- <StyledError>
899
- <strong>Error:</strong> {error}
900
- </StyledError>
901
- )}
902
- <div ref={endRef} />
903
- </StyledChat>
904
-
905
- <StyledChatForm onSubmit={ask} $isVisible={isOpen}>
906
- <RainbowInput
907
- id="chat-bottom-input"
908
- value={question}
909
- onChange={(e) => setQuestion(e.target.value)}
910
- placeholder="Ask AI Assistant..."
911
- autoComplete="off"
912
- aria-label="Ask a follow-up question"
913
- />
914
- <StyledRainbowButton
915
- type="submit"
916
- disabled={loading || question.trim() === ""}
917
- $hasContent={question.trim().length > 0}
918
- aria-label={loading ? "Loading response" : "Submit question"}
919
- >
920
- {loading ? <LoaderPinwheel className="loading" /> : <ArrowUp />}
921
- </StyledRainbowButton>
922
- </StyledChatForm>
923
- </>
924
- );
925
- }
926
-
927
- const ChatContext = createContext<{
928
- isOpen: boolean;
929
- setIsOpen: (isOpen: boolean) => void;
930
- isChatActive: boolean;
931
- }>({
932
- isOpen: false,
933
- setIsOpen: () => {},
934
- isChatActive: false,
935
- });
936
-
937
- interface ChatContextProviderProps {
938
- children: React.ReactNode;
939
- isChatActive: boolean;
940
- }
941
-
942
- const ChtProvider = ({ children, isChatActive }: ChatContextProviderProps) => {
943
- const [isOpen, setIsOpen] = useState(false);
1023
+ function clearChat() {
1024
+ abortRef.current?.abort();
1025
+ setAnswer([]);
1026
+ setIsOpen(false);
1027
+ }
944
1028
 
945
1029
  return (
946
1030
  <ChatContext.Provider
@@ -948,6 +1032,15 @@ const ChtProvider = ({ children, isChatActive }: ChatContextProviderProps) => {
948
1032
  isOpen,
949
1033
  setIsOpen,
950
1034
  isChatActive,
1035
+ question,
1036
+ setQuestion,
1037
+ loading,
1038
+ error,
1039
+ answer,
1040
+ setAnswer,
1041
+ ask,
1042
+ clearChat,
1043
+ chatInputRef,
951
1044
  }}
952
1045
  >
953
1046
  {children}
@@ -955,5 +1048,5 @@ const ChtProvider = ({ children, isChatActive }: ChatContextProviderProps) => {
955
1048
  );
956
1049
  };
957
1050
 
958
- export { Chat, ChtProvider, ChatContext };
1051
+ export { Chat, ChtProvider, ChatContext, ChatButtonCTA };
959
1052
  `;
@@ -1 +1 @@
1
- export declare const docsSideBarTemplate = "\"use client\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Space } from \"cherry-styled-components\";\nimport {\n StyledIndexSidebar,\n StyledIndexSidebarLink,\n StyledIndexSidebarLabel,\n} from \"@/components/layout/DocsComponents\";\n\nexport interface Heading {\n id: string;\n text: string;\n level: number;\n}\n\nconst OFFSET = 80;\n\nexport function DocsSideBar({ headings }: { headings: Heading[] }) {\n const [activeId, setActiveId] = useState<string>(\"\");\n\n const handleScroll = useCallback(() => {\n if (headings.length === 0) return;\n\n const headingElements = headings\n .map((heading) => document.getElementById(heading.id))\n .filter((el): el is HTMLElement => el !== null);\n\n if (headingElements.length === 0) return;\n\n const windowHeight = window.innerHeight;\n\n const visibleHeadings = headingElements.filter((element) => {\n const rect = element.getBoundingClientRect();\n const elementTop = rect.top;\n const elementBottom = rect.bottom;\n return elementTop < windowHeight && elementBottom > -50;\n });\n\n if (visibleHeadings.length > 0) {\n let closestHeading = visibleHeadings[0];\n let closestDistance = Math.abs(\n closestHeading.getBoundingClientRect().top - OFFSET,\n );\n for (const heading of visibleHeadings) {\n const distance = Math.abs(heading.getBoundingClientRect().top - OFFSET);\n if (\n distance < closestDistance &&\n heading.getBoundingClientRect().top <= windowHeight * 0.3\n ) {\n closestDistance = distance;\n closestHeading = heading;\n }\n }\n setActiveId(closestHeading.id);\n return;\n }\n\n let currentActiveId = headings[0].id;\n for (const element of headingElements) {\n const rect = element.getBoundingClientRect();\n if (rect.top <= OFFSET) {\n currentActiveId = element.id;\n } else {\n break;\n }\n }\n setActiveId(currentActiveId);\n }, [headings]);\n\n useEffect(() => {\n if (headings.length === 0) return;\n // Set active heading from URL hash immediately on mount\n if (window.location.hash) {\n setActiveId(window.location.hash.slice(1));\n }\n // Run initial scroll check on next frame to avoid synchronous setState in effect\n const rafId = requestAnimationFrame(handleScroll);\n // Re-check after browser finishes scrolling to hash target on new tab/page load\n const delayedId = setTimeout(handleScroll, 300);\n let timeoutId: NodeJS.Timeout;\n const throttledHandleScroll = () => {\n clearTimeout(timeoutId);\n timeoutId = setTimeout(handleScroll, 50);\n };\n window.addEventListener(\"scroll\", throttledHandleScroll);\n window.addEventListener(\"resize\", handleScroll);\n return () => {\n window.removeEventListener(\"scroll\", throttledHandleScroll);\n window.removeEventListener(\"resize\", handleScroll);\n cancelAnimationFrame(rafId);\n clearTimeout(delayedId);\n clearTimeout(timeoutId);\n };\n }, [handleScroll, headings]);\n\n const handleHeadingClick = (headingId: string) => {\n const element = document.getElementById(headingId);\n if (element) {\n const elementPosition =\n element.getBoundingClientRect().top + window.scrollY;\n window.scrollTo({ top: elementPosition - OFFSET, behavior: \"smooth\" });\n }\n };\n\n return (\n <StyledIndexSidebar>\n {headings?.length > 0 && (\n <>\n <StyledIndexSidebarLabel>On this page</StyledIndexSidebarLabel>\n <Space $size={20} />\n </>\n )}\n {headings.map((heading, index) => (\n <li\n key={index}\n style={{ paddingLeft: `${(heading.level - 1) * 16}px` }}\n >\n <StyledIndexSidebarLink\n href={`#${heading.id}`}\n onClick={(e) => {\n e.preventDefault();\n handleHeadingClick(heading.id);\n }}\n $isActive={activeId === heading.id}\n >\n {heading.text}\n </StyledIndexSidebarLink>\n </li>\n ))}\n </StyledIndexSidebar>\n );\n}\n";
1
+ export declare const docsSideBarTemplate = "\"use client\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { Space } from \"cherry-styled-components\";\nimport {\n StyledIndexSidebar,\n StyledIndexSidebarLink,\n StyledIndexSidebarLabel,\n StyledIndexSidebarLi,\n} from \"@/components/layout/DocsComponents\";\n\ninterface Heading {\n id: string;\n text: string;\n level: number;\n}\n\nconst FALLBACK_OFFSET = 60;\n\nfunction getOffset() {\n const header = document.getElementById(\"header\");\n return (header ? header.offsetHeight : FALLBACK_OFFSET) + 20;\n}\n\nexport function DocsSideBar({ headings }: { headings: Heading[] }) {\n const [activeId, setActiveId] = useState<string>(\"\");\n const activeRef = useRef<HTMLLIElement>(null);\n\n const handleScroll = useCallback(() => {\n if (headings.length === 0) return;\n\n const headingElements = headings\n .map((heading) => document.getElementById(heading.id))\n .filter((el): el is HTMLElement => el !== null);\n\n if (headingElements.length === 0) return;\n\n const windowHeight = window.innerHeight;\n\n const visibleHeadings = headingElements.filter((element) => {\n const rect = element.getBoundingClientRect();\n const elementTop = rect.top;\n const elementBottom = rect.bottom;\n return elementTop < windowHeight && elementBottom > -50;\n });\n\n if (visibleHeadings.length > 0) {\n let closestHeading = visibleHeadings[0];\n let closestDistance = Math.abs(\n closestHeading.getBoundingClientRect().top - getOffset(),\n );\n for (const heading of visibleHeadings) {\n const distance = Math.abs(heading.getBoundingClientRect().top - getOffset());\n if (\n distance < closestDistance &&\n heading.getBoundingClientRect().top <= windowHeight * 0.3\n ) {\n closestDistance = distance;\n closestHeading = heading;\n }\n }\n setActiveId(closestHeading.id);\n return;\n }\n\n let currentActiveId = headings[0].id;\n for (const element of headingElements) {\n const rect = element.getBoundingClientRect();\n if (rect.top <= getOffset()) {\n currentActiveId = element.id;\n } else {\n break;\n }\n }\n setActiveId(currentActiveId);\n }, [headings]);\n\n useEffect(() => {\n if (headings.length === 0) return;\n // Set active heading from URL hash immediately on mount\n if (window.location.hash) {\n setActiveId(window.location.hash.slice(1));\n }\n // Run initial scroll check on next frame to avoid synchronous setState in effect\n const rafId = requestAnimationFrame(handleScroll);\n // Re-check after browser finishes scrolling to hash target on new tab/page load\n const delayedId = setTimeout(handleScroll, 300);\n let timeoutId: NodeJS.Timeout;\n const throttledHandleScroll = () => {\n clearTimeout(timeoutId);\n timeoutId = setTimeout(handleScroll, 50);\n };\n window.addEventListener(\"scroll\", throttledHandleScroll);\n window.addEventListener(\"resize\", handleScroll);\n return () => {\n window.removeEventListener(\"scroll\", throttledHandleScroll);\n window.removeEventListener(\"resize\", handleScroll);\n cancelAnimationFrame(rafId);\n clearTimeout(delayedId);\n clearTimeout(timeoutId);\n };\n }, [handleScroll, headings]);\n\n useEffect(() => {\n const el = activeRef.current;\n const container = el?.closest(\"[data-sidebar]\") as HTMLElement | null;\n if (!el || !container) return;\n const elRect = el.getBoundingClientRect();\n const cRect = container.getBoundingClientRect();\n const pad = 140;\n if (elRect.bottom + pad > cRect.bottom) {\n container.scrollBy({ top: elRect.bottom - cRect.bottom + pad, behavior: \"smooth\" });\n } else if (elRect.top - pad < cRect.top) {\n container.scrollBy({ top: elRect.top - cRect.top - pad, behavior: \"smooth\" });\n }\n }, [activeId]);\n\n const handleHeadingClick = (headingId: string) => {\n const element = document.getElementById(headingId);\n if (element) {\n const elementPosition =\n element.getBoundingClientRect().top + window.scrollY;\n window.scrollTo({ top: elementPosition - getOffset(), behavior: \"smooth\" });\n }\n };\n\n return (\n <StyledIndexSidebar data-sidebar>\n {headings?.length > 0 && (\n <>\n <StyledIndexSidebarLabel>On this page</StyledIndexSidebarLabel>\n <Space $size={15} />\n </>\n )}\n {headings.map((heading, index) => (\n <StyledIndexSidebarLi\n key={index}\n ref={activeId === heading.id ? activeRef : null}\n $isActive={activeId === heading.id}\n style={{ paddingLeft: `${(heading.level - 1) * 16}px` }}\n >\n <StyledIndexSidebarLink\n href={`#${heading.id}`}\n onClick={(e) => {\n e.preventDefault();\n handleHeadingClick(heading.id);\n }}\n $isActive={activeId === heading.id}\n >\n {heading.text}\n </StyledIndexSidebarLink>\n </StyledIndexSidebarLi>\n ))}\n </StyledIndexSidebar>\n );\n}\n";
@@ -1,22 +1,29 @@
1
1
  export const docsSideBarTemplate = `"use client";
2
- import { useCallback, useEffect, useState } from "react";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { Space } from "cherry-styled-components";
4
4
  import {
5
5
  StyledIndexSidebar,
6
6
  StyledIndexSidebarLink,
7
7
  StyledIndexSidebarLabel,
8
+ StyledIndexSidebarLi,
8
9
  } from "@/components/layout/DocsComponents";
9
10
 
10
- export interface Heading {
11
+ interface Heading {
11
12
  id: string;
12
13
  text: string;
13
14
  level: number;
14
15
  }
15
16
 
16
- const OFFSET = 80;
17
+ const FALLBACK_OFFSET = 60;
18
+
19
+ function getOffset() {
20
+ const header = document.getElementById("header");
21
+ return (header ? header.offsetHeight : FALLBACK_OFFSET) + 20;
22
+ }
17
23
 
18
24
  export function DocsSideBar({ headings }: { headings: Heading[] }) {
19
25
  const [activeId, setActiveId] = useState<string>("");
26
+ const activeRef = useRef<HTMLLIElement>(null);
20
27
 
21
28
  const handleScroll = useCallback(() => {
22
29
  if (headings.length === 0) return;
@@ -39,10 +46,10 @@ export function DocsSideBar({ headings }: { headings: Heading[] }) {
39
46
  if (visibleHeadings.length > 0) {
40
47
  let closestHeading = visibleHeadings[0];
41
48
  let closestDistance = Math.abs(
42
- closestHeading.getBoundingClientRect().top - OFFSET,
49
+ closestHeading.getBoundingClientRect().top - getOffset(),
43
50
  );
44
51
  for (const heading of visibleHeadings) {
45
- const distance = Math.abs(heading.getBoundingClientRect().top - OFFSET);
52
+ const distance = Math.abs(heading.getBoundingClientRect().top - getOffset());
46
53
  if (
47
54
  distance < closestDistance &&
48
55
  heading.getBoundingClientRect().top <= windowHeight * 0.3
@@ -58,7 +65,7 @@ export function DocsSideBar({ headings }: { headings: Heading[] }) {
58
65
  let currentActiveId = headings[0].id;
59
66
  for (const element of headingElements) {
60
67
  const rect = element.getBoundingClientRect();
61
- if (rect.top <= OFFSET) {
68
+ if (rect.top <= getOffset()) {
62
69
  currentActiveId = element.id;
63
70
  } else {
64
71
  break;
@@ -93,26 +100,42 @@ export function DocsSideBar({ headings }: { headings: Heading[] }) {
93
100
  };
94
101
  }, [handleScroll, headings]);
95
102
 
103
+ useEffect(() => {
104
+ const el = activeRef.current;
105
+ const container = el?.closest("[data-sidebar]") as HTMLElement | null;
106
+ if (!el || !container) return;
107
+ const elRect = el.getBoundingClientRect();
108
+ const cRect = container.getBoundingClientRect();
109
+ const pad = 140;
110
+ if (elRect.bottom + pad > cRect.bottom) {
111
+ container.scrollBy({ top: elRect.bottom - cRect.bottom + pad, behavior: "smooth" });
112
+ } else if (elRect.top - pad < cRect.top) {
113
+ container.scrollBy({ top: elRect.top - cRect.top - pad, behavior: "smooth" });
114
+ }
115
+ }, [activeId]);
116
+
96
117
  const handleHeadingClick = (headingId: string) => {
97
118
  const element = document.getElementById(headingId);
98
119
  if (element) {
99
120
  const elementPosition =
100
121
  element.getBoundingClientRect().top + window.scrollY;
101
- window.scrollTo({ top: elementPosition - OFFSET, behavior: "smooth" });
122
+ window.scrollTo({ top: elementPosition - getOffset(), behavior: "smooth" });
102
123
  }
103
124
  };
104
125
 
105
126
  return (
106
- <StyledIndexSidebar>
127
+ <StyledIndexSidebar data-sidebar>
107
128
  {headings?.length > 0 && (
108
129
  <>
109
130
  <StyledIndexSidebarLabel>On this page</StyledIndexSidebarLabel>
110
- <Space $size={20} />
131
+ <Space $size={15} />
111
132
  </>
112
133
  )}
113
134
  {headings.map((heading, index) => (
114
- <li
135
+ <StyledIndexSidebarLi
115
136
  key={index}
137
+ ref={activeId === heading.id ? activeRef : null}
138
+ $isActive={activeId === heading.id}
116
139
  style={{ paddingLeft: \`\${(heading.level - 1) * 16}px\` }}
117
140
  >
118
141
  <StyledIndexSidebarLink
@@ -125,7 +148,7 @@ export function DocsSideBar({ headings }: { headings: Heading[] }) {
125
148
  >
126
149
  {heading.text}
127
150
  </StyledIndexSidebarLink>
128
- </li>
151
+ </StyledIndexSidebarLi>
129
152
  ))}
130
153
  </StyledIndexSidebar>
131
154
  );
@@ -1 +1 @@
1
- export declare const sideBarTemplate = "\"use client\";\nimport { useContext, useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { Space } from \"cherry-styled-components\";\nimport {\n DocsSidebar,\n SectionBarContext,\n StyledSidebar,\n StyledSidebarList,\n StyledSidebarListItem,\n StyledStrong,\n StyledSidebarListItemLink,\n StyleMobileBar,\n StyledMobileBurger,\n} from \"@/components/layout/DocsComponents\";\n\ntype NavItem = {\n label: string;\n links: NavItemLink[];\n};\n\ntype NavItemLink = {\n slug: string;\n title: string;\n};\n\ninterface SideBarProps {\n result: NavItem[];\n}\n\nfunction SideBar({ result }: SideBarProps) {\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n const hasSectionBar = useContext(SectionBarContext);\n const pathname = usePathname();\n\n return (\n <DocsSidebar>\n <StyleMobileBar\n onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}\n $isActive={isMobileMenuOpen}\n >\n <StyledMobileBurger $isActive={isMobileMenuOpen} />\n </StyleMobileBar>\n\n <StyledSidebar\n $isActive={isMobileMenuOpen}\n $hasSectionBar={hasSectionBar}\n >\n {result &&\n result.map((item: NavItem, index: number) => {\n return (\n <StyledSidebarList key={index}>\n <StyledSidebarListItem>\n <StyledStrong>{item.label}</StyledStrong>{\" \"}\n </StyledSidebarListItem>\n <li>\n <Space $size={20} />\n </li>\n {item.links &&\n item.links.map((link: NavItemLink, indexChild: number) => {\n return (\n <StyledSidebarListItem key={indexChild}>\n <StyledSidebarListItemLink\n href={`/${link.slug}`}\n $isActive={pathname === `/${link.slug}`}\n onClick={() => setIsMobileMenuOpen(false)}\n >\n {link.title}\n </StyledSidebarListItemLink>\n </StyledSidebarListItem>\n );\n })}\n <Space $size={20} />\n </StyledSidebarList>\n );\n })}\n </StyledSidebar>\n </DocsSidebar>\n );\n}\n\nexport { SideBar };\n";
1
+ export declare const sideBarTemplate = "\"use client\";\nimport { useContext, useState, Suspense } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { Flex, Space } from \"cherry-styled-components\";\nimport {\n DocsSidebar,\n SectionBarContext,\n StyledSidebar,\n StyledSidebarList,\n StyledSidebarListItem,\n StyledStrong,\n StyledSidebarListItemLink,\n StyleMobileBar,\n StyledMobileBurger,\n} from \"@/components/layout/DocsComponents\";\nimport {\n ToggleTheme,\n ToggleThemeLoading,\n} from \"@/components/layout/ThemeToggle\";\n\ntype NavItem = {\n label: string;\n links: NavItemLink[];\n};\n\ntype NavItemLink = {\n slug: string;\n title: string;\n};\n\ninterface SideBarProps {\n result: NavItem[];\n}\n\nfunction SideBar({ result }: SideBarProps) {\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n const hasSectionBar = useContext(SectionBarContext);\n const pathname = usePathname();\n\n return (\n <DocsSidebar>\n <StyleMobileBar\n onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}\n $isActive={isMobileMenuOpen}\n >\n <StyledMobileBurger $isActive={isMobileMenuOpen} />\n </StyleMobileBar>\n\n <StyledSidebar\n $isActive={isMobileMenuOpen}\n $hasSectionBar={hasSectionBar}\n >\n {result &&\n result.map((item: NavItem, index: number) => {\n return (\n <StyledSidebarList key={index}>\n <StyledSidebarListItem>\n <StyledStrong>{item.label}</StyledStrong>{\" \"}\n </StyledSidebarListItem>\n <li>\n <Space $size={20} />\n </li>\n {item.links &&\n item.links.map((link: NavItemLink, indexChild: number) => {\n return (\n <StyledSidebarListItem key={indexChild}>\n <StyledSidebarListItemLink\n href={`/${link.slug}`}\n $isActive={pathname === `/${link.slug}`}\n onClick={() => setIsMobileMenuOpen(false)}\n >\n {link.title}\n </StyledSidebarListItemLink>\n </StyledSidebarListItem>\n );\n })}\n <Space $size={20} />\n </StyledSidebarList>\n );\n })}\n <Space $xs={40} $lg={20} />\n <Flex $xsJustifyContent=\"flex-start\" $lgJustifyContent=\"flex-end\">\n <Suspense fallback={<ToggleThemeLoading />}>\n <ToggleTheme />\n </Suspense>\n </Flex>\n </StyledSidebar>\n </DocsSidebar>\n );\n}\n\nexport { SideBar };\n";