doccupine 0.0.122 → 0.0.124

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 (33) hide show
  1. package/dist/lib/structures.js +2 -0
  2. package/dist/templates/components/Chat.d.ts +1 -1
  3. package/dist/templates/components/Chat.js +32 -12
  4. package/dist/templates/components/MDXComponents.d.ts +1 -1
  5. package/dist/templates/components/MDXComponents.js +4 -2
  6. package/dist/templates/components/SearchDocs.d.ts +1 -1
  7. package/dist/templates/components/SearchDocs.js +56 -9
  8. package/dist/templates/components/SearchModalContent.d.ts +1 -1
  9. package/dist/templates/components/SearchModalContent.js +177 -14
  10. package/dist/templates/components/SideBar.d.ts +1 -1
  11. package/dist/templates/components/SideBar.js +1 -1
  12. package/dist/templates/components/layout/Card.d.ts +1 -1
  13. package/dist/templates/components/layout/Card.js +2 -2
  14. package/dist/templates/components/layout/SiteGate.d.ts +1 -1
  15. package/dist/templates/components/layout/SiteGate.js +1 -1
  16. package/dist/templates/components/layout/Slug.d.ts +1 -1
  17. package/dist/templates/components/layout/Slug.js +1 -1
  18. package/dist/templates/components/layout/Steps.d.ts +1 -1
  19. package/dist/templates/components/layout/Steps.js +7 -5
  20. package/dist/templates/components/layout/Update.d.ts +1 -1
  21. package/dist/templates/components/layout/Update.js +5 -3
  22. package/dist/templates/mdx/cards.mdx.d.ts +1 -1
  23. package/dist/templates/mdx/cards.mdx.js +2 -2
  24. package/dist/templates/mdx/platform/ai-assistant.mdx.d.ts +1 -1
  25. package/dist/templates/mdx/platform/ai-assistant.mdx.js +1 -7
  26. package/dist/templates/mdx/platform/mcp.mdx.d.ts +1 -0
  27. package/dist/templates/mdx/platform/mcp.mdx.js +53 -0
  28. package/dist/templates/mdx/steps.mdx.d.ts +1 -1
  29. package/dist/templates/mdx/steps.mdx.js +2 -2
  30. package/dist/templates/mdx/update.mdx.d.ts +1 -1
  31. package/dist/templates/mdx/update.mdx.js +2 -2
  32. package/dist/templates/package.js +3 -3
  33. package/package.json +1 -1
@@ -5,7 +5,10 @@ import { Search, X } from "lucide-react";
5
5
  import { IconButton } from "cherry-styled-components";
6
6
  import { mq, Theme } from "@/app/theme";
7
7
  import { Spinner } from "@/components/Spinner";
8
- import { thinScrollbar } from "@/components/layout/SharedStyled";
8
+ import {
9
+ interactiveStyles,
10
+ thinScrollbar,
11
+ } from "@/components/layout/SharedStyled";
9
12
 
10
13
  export interface PageItem {
11
14
  slug: string;
@@ -33,7 +36,12 @@ export interface SearchModalContentProps {
33
36
  merged: MergedResult[];
34
37
  sectionLabels: Record<string, string>;
35
38
  isSearching: boolean;
36
- navigate: (slug: string) => void;
39
+ navigate: (
40
+ slug: string,
41
+ modifiers?: { metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean },
42
+ ) => void;
43
+ canAskAssistant: boolean;
44
+ onAskAssistant: () => void;
37
45
  }
38
46
 
39
47
  const ANIMATION_MS = 300;
@@ -127,8 +135,74 @@ const StyledInput = styled.input<{ theme: Theme }>\`
127
135
  }
128
136
  \`;
129
137
 
138
+ // "Ask AI" affordance shown before the close button when the AI chat is
139
+ // enabled. Desktop-only (like the header's Cmd+K hint) since it advertises the
140
+ // Option+Enter shortcut; mobile users reach the assistant from the header CTA.
141
+ const StyledAskAssistant = styled.button<{ theme: Theme }>\`
142
+ \${interactiveStyles};
143
+ display: none;
144
+ align-items: center;
145
+ gap: 6px;
146
+ flex-shrink: 0;
147
+ border: solid 1px \${({ theme }) => theme.colors.grayLight};
148
+ background: \${({ theme }) => theme.colors.light};
149
+ color: \${({ theme }) => theme.colors.grayDark};
150
+ border-radius: \${({ theme }) => theme.spacing.radius.xs};
151
+ padding: 5px 8px;
152
+ font-family: inherit;
153
+ font-size: \${({ theme }) => theme.fontSizes.small.lg};
154
+ font-weight: 500;
155
+ line-height: 1;
156
+ white-space: nowrap;
157
+ cursor: pointer;
158
+
159
+ &:disabled {
160
+ opacity: 0.5;
161
+ cursor: default;
162
+
163
+ &:hover {
164
+ border-color: \${({ theme }) => theme.colors.grayLight};
165
+ }
166
+ }
167
+
168
+ \${mq("lg")} {
169
+ display: inline-flex;
170
+ }
171
+ \`;
172
+
173
+ const StyledAskKeys = styled.span\`
174
+ display: inline-flex;
175
+ align-items: center;
176
+ gap: 3px;
177
+ \`;
178
+
179
+ const StyledAskKbd = styled.kbd<{ theme: Theme }>\`
180
+ font-family: inherit;
181
+ font-size: 12px;
182
+ font-weight: 600;
183
+ line-height: 1;
184
+ min-width: 18px;
185
+ height: 18px;
186
+ padding: 0 4px;
187
+ display: inline-flex;
188
+ align-items: center;
189
+ justify-content: center;
190
+ background: \${({ theme }) => theme.colors.grayLight};
191
+ color: \${({ theme }) => theme.colors.grayDark};
192
+ border-radius: 4px;
193
+ \`;
194
+
195
+ // The return glyph's ink sits high in its em box, so it reads top-aligned even
196
+ // when the keycap flex-centers it. Nudge just the character (not the pill) down
197
+ // a pixel so it optically centers and lines up with the option glyph.
198
+ const StyledReturnGlyph = styled.span\`
199
+ display: inline-block;
200
+ transform: translateY(1.5px);
201
+ \`;
202
+
130
203
  const StyledResults = styled.ul<{ theme: Theme }>\`
131
204
  list-style: none;
205
+ position: relative;
132
206
  margin: 8px 0 0 0;
133
207
  padding: 0 8px;
134
208
  overflow-y: auto;
@@ -147,26 +221,44 @@ const StyledResults = styled.ul<{ theme: Theme }>\`
147
221
  }
148
222
  \`;
149
223
 
224
+ // Single indicator that slides between result items instead of each item
225
+ // fading its own background in and out. Positioned/sized inline from the active
226
+ // item's measured offset (see the layout effect below); the transition on
227
+ // transform + height produces the up/down glide as the active item changes.
228
+ const StyledHighlight = styled.div<{ theme: Theme }>\`
229
+ position: absolute;
230
+ left: 8px;
231
+ right: 8px;
232
+ top: 0;
233
+ z-index: 0;
234
+ pointer-events: none;
235
+ border-radius: \${({ theme }) => theme.spacing.radius.xs};
236
+ background: \${({ theme }) =>
237
+ \`color-mix(in srgb, \${theme.colors.primaryLight} 20%, transparent)\`};
238
+ transition:
239
+ transform 0.2s cubic-bezier(0.22, 1, 0.36, 1),
240
+ height 0.2s cubic-bezier(0.22, 1, 0.36, 1);
241
+ will-change: transform, height;
242
+
243
+ @media (prefers-reduced-motion: reduce) {
244
+ transition: none;
245
+ }
246
+ \`;
247
+
150
248
  const StyledResultItem = styled.li<{ theme: Theme; $isActive: boolean }>\`
249
+ position: relative;
250
+ z-index: 1;
151
251
  padding: 10px 12px;
152
252
  border-radius: \${({ theme }) => theme.spacing.radius.xs};
153
253
  cursor: pointer;
154
- transition: background 0.15s ease;
155
254
 
156
255
  \${({ $isActive, theme }) =>
157
256
  $isActive &&
158
257
  css\`
159
- background: color-mix(
160
- in srgb,
161
- \${theme.colors.primaryLight} 20%,
162
- transparent
163
- );
258
+ & > span:first-of-type {
259
+ color: \${theme.colors.primary};
260
+ }
164
261
  \`}
165
-
166
- &:hover {
167
- background: \${({ theme }) =>
168
- \`color-mix(in srgb, \${theme.colors.primaryLight} 15%, transparent)\`};
169
- }
170
262
  \`;
171
263
 
172
264
  const StyledResultTitle = styled.span<{ theme: Theme }>\`
@@ -174,6 +266,7 @@ const StyledResultTitle = styled.span<{ theme: Theme }>\`
174
266
  font-weight: 500;
175
267
  color: \${({ theme }) => theme.colors.dark};
176
268
  display: block;
269
+ transition: color 0.15s ease;
177
270
  \`;
178
271
 
179
272
  const StyledResultMeta = styled.span<{ theme: Theme }>\`
@@ -246,7 +339,51 @@ export function SearchModalContent({
246
339
  sectionLabels,
247
340
  isSearching,
248
341
  navigate,
342
+ canAskAssistant,
343
+ onAskAssistant,
249
344
  }: SearchModalContentProps) {
345
+ // Measure the active item so the sliding highlight can be placed over it.
346
+ // useLayoutEffect runs before paint, so the first position is committed
347
+ // without a transition (no slide-in on open); later activeIndex/hover
348
+ // changes glide because only the transform + height differ.
349
+ const [indicator, setIndicator] = React.useState<{
350
+ top: number;
351
+ height: number;
352
+ } | null>(null);
353
+
354
+ React.useLayoutEffect(() => {
355
+ const list = resultsRef.current;
356
+ if (!list) {
357
+ setIndicator(null);
358
+ return;
359
+ }
360
+ const items = list.querySelectorAll("li");
361
+ const active = items[activeIndex] as HTMLElement | undefined;
362
+ if (!active) {
363
+ setIndicator(null);
364
+ return;
365
+ }
366
+ setIndicator({ top: active.offsetTop, height: active.offsetHeight });
367
+
368
+ // Keep the active row inside the scroll viewport in the SAME pre-paint pass
369
+ // that positions the highlight, so the two never desync into a one-frame
370
+ // flicker. merged is a dependency, so re-filtering (a new query) also
371
+ // re-scrolls the reset active row into view even when its index is
372
+ // unchanged. This list is flat (no group headers), so no header inclusion
373
+ // is needed; a small margin keeps rows off the container's hard edges.
374
+ // Adjust scrollTop directly (never scrollIntoView, which can also pan
375
+ // ancestors).
376
+ const margin = 8;
377
+ const viewTop = active.offsetTop - margin;
378
+ const viewBottom = active.offsetTop + active.offsetHeight + margin;
379
+
380
+ if (viewTop < list.scrollTop) {
381
+ list.scrollTop = viewTop;
382
+ } else if (viewBottom > list.scrollTop + list.clientHeight) {
383
+ list.scrollTop = viewBottom - list.clientHeight;
384
+ }
385
+ }, [activeIndex, merged, resultsRef]);
386
+
250
387
  return (
251
388
  <StyledBackdrop
252
389
  $isClosing={isClosing}
@@ -275,6 +412,23 @@ export function SearchModalContent({
275
412
  autoComplete="off"
276
413
  spellCheck={false}
277
414
  />
415
+ {canAskAssistant && (
416
+ <StyledAskAssistant
417
+ type="button"
418
+ onClick={onAskAssistant}
419
+ disabled={!query.trim()}
420
+ aria-label="Ask AI"
421
+ title="Ask AI"
422
+ >
423
+ Ask AI
424
+ <StyledAskKeys aria-hidden="true">
425
+ <StyledAskKbd>&#8997;</StyledAskKbd>
426
+ <StyledAskKbd>
427
+ <StyledReturnGlyph>&#8629;</StyledReturnGlyph>
428
+ </StyledAskKbd>
429
+ </StyledAskKeys>
430
+ </StyledAskAssistant>
431
+ )}
278
432
  <IconButton
279
433
  onClick={closeSearch}
280
434
  aria-label="Close search"
@@ -285,11 +439,20 @@ export function SearchModalContent({
285
439
  </StyledInputWrapper>
286
440
  {merged.length > 0 ? (
287
441
  <StyledResults ref={resultsRef}>
442
+ {indicator && (
443
+ <StyledHighlight
444
+ aria-hidden="true"
445
+ style={{
446
+ transform: \`translateY(\${indicator.top}px)\`,
447
+ height: \`\${indicator.height}px\`,
448
+ }}
449
+ />
450
+ )}
288
451
  {merged.map((result, index) => (
289
452
  <StyledResultItem
290
453
  key={result.page.slug + result.page.section}
291
454
  $isActive={index === activeIndex}
292
- onClick={() => navigate(result.page.slug)}
455
+ onClick={(e) => navigate(result.page.slug, e)}
293
456
  onMouseEnter={() => setActiveIndex(index)}
294
457
  >
295
458
  <StyledResultTitle>{result.page.title}</StyledResultTitle>
@@ -1 +1 @@
1
- export declare const sideBarTemplate = "\"use client\";\nimport { useContext, useEffect, useRef, useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { Flex, Space, ThemeToggle } from \"cherry-styled-components\";\nimport {\n DocsSidebar,\n SectionBarContext,\n StyledSidebar,\n StyledSidebarList,\n StyledSidebarListItem,\n StyledStrong,\n StyledSidebarListItemLink,\n StyledSidebarGroupButton,\n StyledSidebarGroupRow,\n StyledSidebarGroupLink,\n StyledSidebarGroupChevron,\n StyledSidebarGroupContent,\n StyledSidebarFooter,\n StyleMobileBar,\n StyledMobileBurger,\n} from \"@/components/layout/DocsComponents\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { useLockBodyScroll } from \"@/components/LockBodyScroll\";\n\n// A link can be a leaf (slug + title) or a group with nested children. Both the\n// category icon and the per-link icon are optional Lucide names.\ntype NavItemLink = {\n slug?: string;\n title: string;\n icon?: string;\n links?: NavItemLink[];\n};\n\ntype NavItem = {\n label: string;\n icon?: string;\n links: NavItemLink[];\n};\n\ninterface SideBarProps {\n result: NavItem[];\n}\n\nfunction linkContainsActivePath(link: NavItemLink, pathname: string): boolean {\n if (link.slug !== undefined && pathname === `/${link.slug}`) {\n return true;\n }\n return (link.links ?? []).some((child) =>\n linkContainsActivePath(child, pathname),\n );\n}\n\nfunction SidebarNavLink({\n link,\n depth,\n pathname,\n onNavigate,\n}: {\n link: NavItemLink;\n depth: number;\n pathname: string;\n onNavigate: () => void;\n}) {\n const children = link.links ?? [];\n const hasChildren = children.length > 0;\n const href = link.slug !== undefined ? `/${link.slug}` : undefined;\n const isActive = href !== undefined && pathname === href;\n const indent = { paddingLeft: `${20 + depth * 14}px` };\n\n // Open collapsible groups that contain the active page so deep links land\n // with their ancestors already expanded.\n const [isOpen, setIsOpen] = useState(\n hasChildren\n ? children.some((child) => linkContainsActivePath(child, pathname))\n : false,\n );\n\n if (!hasChildren) {\n return (\n <StyledSidebarListItem>\n <StyledSidebarListItemLink\n href={href ?? \"#\"}\n $isActive={isActive}\n aria-current={isActive ? \"page\" : undefined}\n onClick={onNavigate}\n style={indent}\n >\n {link.icon && <Icon name={link.icon} size={16} />}\n {link.title}\n </StyledSidebarListItemLink>\n </StyledSidebarListItem>\n );\n }\n\n const toggle = () => setIsOpen((prev) => !prev);\n const toggleLabel = isOpen\n ? `Collapse ${link.title}`\n : `Expand ${link.title}`;\n const groupActive = linkContainsActivePath(link, pathname);\n\n return (\n <li>\n {href !== undefined ? (\n <StyledSidebarGroupRow\n $isActive={groupActive}\n $isOpen={isOpen}\n style={indent}\n >\n <StyledSidebarGroupLink\n href={href}\n aria-current={pathname === href ? \"page\" : undefined}\n onClick={onNavigate}\n >\n {link.icon && <Icon name={link.icon} size={16} />}\n {link.title}\n </StyledSidebarGroupLink>\n <StyledSidebarGroupChevron\n type=\"button\"\n onClick={toggle}\n aria-expanded={isOpen}\n aria-label={toggleLabel}\n >\n <Icon name=\"chevron-right\" size={16} />\n </StyledSidebarGroupChevron>\n </StyledSidebarGroupRow>\n ) : (\n <StyledSidebarGroupButton\n type=\"button\"\n onClick={toggle}\n $isActive={groupActive}\n $isOpen={isOpen}\n style={indent}\n aria-expanded={isOpen}\n aria-label={toggleLabel}\n >\n {link.icon && <Icon name={link.icon} size={16} />}\n {link.title}\n <Icon name=\"chevron-right\" size={16} />\n </StyledSidebarGroupButton>\n )}\n <StyledSidebarGroupContent $isOpen={isOpen}>\n {children.map((child: NavItemLink, index: number) => (\n <SidebarNavLink\n key={index}\n link={child}\n depth={depth + 1}\n pathname={pathname}\n onNavigate={onNavigate}\n />\n ))}\n </StyledSidebarGroupContent>\n </li>\n );\n}\n\nfunction SideBar({ result }: SideBarProps) {\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n const hasSectionBar = useContext(SectionBarContext);\n const pathname = usePathname();\n const navRef = useRef<HTMLElement>(null);\n const footerRef = useRef<HTMLDivElement>(null);\n\n useLockBodyScroll(isMobileMenuOpen);\n\n // Bring the current page's link into view within the sidebar's own scroll\n // area when it starts off-screen (deep pages, in-content links, search).\n // Scoped to the nav via scrollTo so the main document never jumps.\n useEffect(() => {\n const nav = navRef.current;\n if (!nav) return;\n const active = nav.querySelector<HTMLElement>('[aria-current=\"page\"]');\n if (!active) return;\n\n const navRect = nav.getBoundingClientRect();\n const activeRect = active.getBoundingClientRect();\n\n // The theme-toggle footer is sticky over the bottom of the scroll area\n // (~60px tall), so links underneath it are covered rather than visible.\n // Clamp the visible bottom to the footer's top edge when it's pinned.\n const footerRect = footerRef.current?.getBoundingClientRect();\n const visibleBottom = footerRect\n ? Math.min(navRect.bottom, footerRect.top)\n : navRect.bottom;\n\n const isOutOfView =\n activeRect.top < navRect.top || activeRect.bottom > visibleBottom;\n if (!isOutOfView) return;\n\n // Center within the visible band (nav top .. footer top), not the full\n // nav height, so the link never settles behind the footer.\n const visibleHeight = visibleBottom - navRect.top;\n const target =\n nav.scrollTop +\n (activeRect.top - navRect.top) -\n (visibleHeight - active.clientHeight) / 2;\n\n // Animate the scroll, but honor users who opt out of motion.\n const prefersReducedMotion = window.matchMedia(\n \"(prefers-reduced-motion: reduce)\",\n ).matches;\n nav.scrollTo({\n top: Math.max(0, target),\n behavior: prefersReducedMotion ? \"auto\" : \"smooth\",\n });\n }, [pathname]);\n\n return (\n <DocsSidebar>\n <StyleMobileBar\n onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}\n $isActive={isMobileMenuOpen}\n aria-label={\n isMobileMenuOpen ? \"Close navigation menu\" : \"Open navigation menu\"\n }\n aria-expanded={isMobileMenuOpen}\n >\n <StyledMobileBurger $isActive={isMobileMenuOpen} />\n </StyleMobileBar>\n\n <StyledSidebar\n ref={navRef}\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>\n {item.icon && <Icon name={item.icon} size={16} />}\n {item.label}\n </StyledStrong>{\" \"}\n </StyledSidebarListItem>\n <li>\n <Space $size={20} />\n </li>\n {item.links &&\n item.links.map((link: NavItemLink, indexChild: number) => (\n <SidebarNavLink\n key={indexChild}\n link={link}\n depth={0}\n pathname={pathname}\n onNavigate={() => setIsMobileMenuOpen(false)}\n />\n ))}\n <li aria-hidden=\"true\">\n <Space $size={20} />\n </li>\n </StyledSidebarList>\n );\n })}\n <StyledSidebarFooter ref={footerRef}>\n <Flex $xsJustifyContent=\"flex-start\" $lgJustifyContent=\"flex-end\">\n <ThemeToggle />\n </Flex>\n </StyledSidebarFooter>\n </StyledSidebar>\n </DocsSidebar>\n );\n}\n\nexport { SideBar };\n";
1
+ export declare const sideBarTemplate = "\"use client\";\nimport { useContext, useEffect, useRef, useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { Flex, Space, ThemeToggle } from \"cherry-styled-components\";\nimport {\n DocsSidebar,\n SectionBarContext,\n StyledSidebar,\n StyledSidebarList,\n StyledSidebarListItem,\n StyledStrong,\n StyledSidebarListItemLink,\n StyledSidebarGroupButton,\n StyledSidebarGroupRow,\n StyledSidebarGroupLink,\n StyledSidebarGroupChevron,\n StyledSidebarGroupContent,\n StyledSidebarFooter,\n StyleMobileBar,\n StyledMobileBurger,\n} from \"@/components/layout/DocsComponents\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { useLockBodyScroll } from \"@/components/LockBodyScroll\";\n\n// A link can be a leaf (slug + title) or a group with nested children. Both the\n// category icon and the per-link icon are optional Lucide names.\ntype NavItemLink = {\n slug?: string;\n title: string;\n icon?: string;\n links?: NavItemLink[];\n};\n\ntype NavItem = {\n label: string;\n icon?: string;\n links: NavItemLink[];\n};\n\ninterface SideBarProps {\n result: NavItem[];\n}\n\nfunction linkContainsActivePath(link: NavItemLink, pathname: string): boolean {\n if (link.slug !== undefined && pathname === `/${link.slug}`) {\n return true;\n }\n return (link.links ?? []).some((child) =>\n linkContainsActivePath(child, pathname),\n );\n}\n\nfunction SidebarNavLink({\n link,\n depth,\n pathname,\n onNavigate,\n}: {\n link: NavItemLink;\n depth: number;\n pathname: string;\n onNavigate: () => void;\n}) {\n const children = link.links ?? [];\n const hasChildren = children.length > 0;\n const href = link.slug !== undefined ? `/${link.slug}` : undefined;\n const isActive = href !== undefined && pathname === href;\n const indent = { paddingLeft: `${20 + depth * 14}px` };\n\n // Open collapsible groups that contain the active page so deep links land\n // with their ancestors already expanded.\n const [isOpen, setIsOpen] = useState(\n hasChildren\n ? children.some((child) => linkContainsActivePath(child, pathname))\n : false,\n );\n\n if (!hasChildren) {\n return (\n <StyledSidebarListItem>\n <StyledSidebarListItemLink\n href={href ?? \"#\"}\n $isActive={isActive}\n aria-current={isActive ? \"page\" : undefined}\n onClick={onNavigate}\n style={indent}\n >\n {link.icon && <Icon name={link.icon} size={16} />}\n {link.title}\n </StyledSidebarListItemLink>\n </StyledSidebarListItem>\n );\n }\n\n const toggle = () => setIsOpen((prev) => !prev);\n const toggleLabel = isOpen\n ? `Collapse ${link.title}`\n : `Expand ${link.title}`;\n const groupActive = linkContainsActivePath(link, pathname);\n\n return (\n <li>\n {href !== undefined ? (\n <StyledSidebarGroupRow\n $isActive={groupActive}\n $isOpen={isOpen}\n style={indent}\n >\n <StyledSidebarGroupLink\n href={href}\n aria-current={pathname === href ? \"page\" : undefined}\n onClick={onNavigate}\n >\n {link.icon && <Icon name={link.icon} size={16} />}\n {link.title}\n </StyledSidebarGroupLink>\n <StyledSidebarGroupChevron\n type=\"button\"\n onClick={toggle}\n aria-expanded={isOpen}\n aria-label={toggleLabel}\n >\n <Icon name=\"chevron-right\" size={16} />\n </StyledSidebarGroupChevron>\n </StyledSidebarGroupRow>\n ) : (\n <StyledSidebarGroupButton\n type=\"button\"\n onClick={toggle}\n $isActive={groupActive}\n $isOpen={isOpen}\n style={indent}\n aria-expanded={isOpen}\n aria-label={toggleLabel}\n >\n {link.icon && <Icon name={link.icon} size={16} />}\n {link.title}\n <Icon name=\"chevron-right\" size={16} />\n </StyledSidebarGroupButton>\n )}\n <StyledSidebarGroupContent $isOpen={isOpen}>\n {children.map((child: NavItemLink, index: number) => (\n <SidebarNavLink\n key={index}\n link={child}\n depth={depth + 1}\n pathname={pathname}\n onNavigate={onNavigate}\n />\n ))}\n </StyledSidebarGroupContent>\n </li>\n );\n}\n\nfunction SideBar({ result }: SideBarProps) {\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n const hasSectionBar = useContext(SectionBarContext);\n const pathname = usePathname();\n const navRef = useRef<HTMLElement>(null);\n const footerRef = useRef<HTMLDivElement>(null);\n\n useLockBodyScroll(isMobileMenuOpen);\n\n // Bring the current page's link into view within the sidebar's own scroll\n // area when it starts off-screen (deep pages, in-content links, search).\n // Scoped to the nav via scrollTo so the main document never jumps.\n useEffect(() => {\n const nav = navRef.current;\n if (!nav) return;\n const active = nav.querySelector<HTMLElement>('[aria-current=\"page\"]');\n if (!active) return;\n\n const navRect = nav.getBoundingClientRect();\n const activeRect = active.getBoundingClientRect();\n\n // The theme-toggle footer is sticky over the bottom of the scroll area\n // (~60px tall), so links underneath it are covered rather than visible.\n // Clamp the visible bottom to the footer's top edge when it's pinned.\n const footerRect = footerRef.current?.getBoundingClientRect();\n const visibleBottom = footerRect\n ? Math.min(navRect.bottom, footerRect.top)\n : navRect.bottom;\n\n const isOutOfView =\n activeRect.top < navRect.top || activeRect.bottom > visibleBottom;\n if (!isOutOfView) return;\n\n // Center within the visible band (nav top .. footer top), not the full\n // nav height, so the link never settles behind the footer.\n const visibleHeight = visibleBottom - navRect.top;\n const target =\n nav.scrollTop +\n (activeRect.top - navRect.top) -\n (visibleHeight - active.clientHeight) / 2;\n\n // Animate the scroll, but honor users who opt out of motion.\n const prefersReducedMotion = window.matchMedia(\n \"(prefers-reduced-motion: reduce)\",\n ).matches;\n nav.scrollTo({\n top: Math.max(0, target),\n behavior: prefersReducedMotion ? \"auto\" : \"smooth\",\n });\n }, [pathname]);\n\n return (\n <DocsSidebar>\n <StyleMobileBar\n onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}\n $isActive={isMobileMenuOpen}\n aria-label={\n isMobileMenuOpen ? \"Close navigation menu\" : \"Open navigation menu\"\n }\n aria-expanded={isMobileMenuOpen}\n >\n <StyledMobileBurger $isActive={isMobileMenuOpen} />\n </StyleMobileBar>\n\n <StyledSidebar\n ref={navRef}\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>\n {item.icon && <Icon name={item.icon} size={16} />}\n {item.label}\n </StyledStrong>{\" \"}\n </StyledSidebarListItem>\n <li>\n <Space $size={20} />\n </li>\n {item.links &&\n item.links.map((link: NavItemLink, indexChild: number) => (\n <SidebarNavLink\n key={indexChild}\n link={link}\n depth={0}\n pathname={pathname}\n onNavigate={() => setIsMobileMenuOpen(false)}\n />\n ))}\n <li aria-hidden=\"true\">\n <Space $size={20} />\n </li>\n </StyledSidebarList>\n );\n })}\n <StyledSidebarFooter ref={footerRef}>\n <Flex $xsJustifyContent=\"flex-start\" $lgJustifyContent=\"flex-end\">\n <ThemeToggle $shortcut />\n </Flex>\n </StyledSidebarFooter>\n </StyledSidebar>\n </DocsSidebar>\n );\n}\n\nexport { SideBar };\n";
@@ -253,7 +253,7 @@ function SideBar({ result }: SideBarProps) {
253
253
  })}
254
254
  <StyledSidebarFooter ref={footerRef}>
255
255
  <Flex $xsJustifyContent="flex-start" $lgJustifyContent="flex-end">
256
- <ThemeToggle />
256
+ <ThemeToggle $shortcut />
257
257
  </Flex>
258
258
  </StyledSidebarFooter>
259
259
  </StyledSidebar>
@@ -1 +1 @@
1
- export declare const cardTemplate = "\"use client\";\nimport Link from \"next/link\";\nimport styled, { css, useTheme } from \"styled-components\";\nimport { styledText } from \"cherry-styled-components\";\nimport { Theme } from \"@/app/theme\";\nimport { Icon, IconProps } from \"@/components/layout/Icon\";\nimport { interactiveStyles } from \"@/components/layout/SharedStyled\";\n\nconst cardStyles = css<{ theme: Theme }>`\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n padding: 20px;\n margin: 0;\n ${({ theme }) => styledText(theme)}\n color: ${({ theme }) => theme.colors.grayDark};\n`;\n\nconst StyledCard = styled.div<{ theme: Theme }>`\n ${cardStyles}\n`;\n\nconst StyledCardLink = styled(Link)<{ theme: Theme }>`\n ${interactiveStyles};\n ${cardStyles}\n text-decoration: none;\n`;\n\nconst StyledCardTitle = styled.p<{ theme: Theme }>`\n font-weight: bold;\n margin: 5px 0;\n color: ${({ theme }) => theme.colors.dark};\n ${({ theme }) => styledText(theme)};\n`;\n\ninterface CardProps extends React.HTMLAttributes<HTMLDivElement> {\n children: React.ReactNode;\n title: string;\n icon?: IconProps;\n href?: string;\n}\n\nfunction Card({ children, title, icon, href }: CardProps) {\n const theme = useTheme() as Theme;\n\n const content = (\n <>\n {icon && <Icon name={icon} color={theme.colors.primary} />}\n <StyledCardTitle>{title}</StyledCardTitle>\n {children}\n </>\n );\n\n if (href) {\n const isExternal = /^(https?:)?\\/\\//i.test(href);\n return (\n <StyledCardLink\n href={href}\n target={isExternal ? \"_blank\" : undefined}\n rel={isExternal ? \"noopener noreferrer\" : undefined}\n >\n {content}\n </StyledCardLink>\n );\n }\n\n return <StyledCard>{content}</StyledCard>;\n}\n\nexport { Card };\n";
1
+ export declare const cardTemplate = "\"use client\";\nimport Link from \"next/link\";\nimport styled, { css, useTheme } from \"styled-components\";\nimport { styledText } from \"cherry-styled-components\";\nimport { Theme } from \"@/app/theme\";\nimport { Icon, IconProps } from \"@/components/layout/Icon\";\nimport { interactiveStyles } from \"@/components/layout/SharedStyled\";\n\nconst cardStyles = css<{ theme: Theme }>`\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n padding: 20px;\n margin: 0;\n ${({ theme }) => styledText(theme)}\n color: ${({ theme }) => theme.colors.grayDark};\n`;\n\nconst StyledCard = styled.div<{ theme: Theme }>`\n ${cardStyles}\n`;\n\nconst StyledCardLink = styled(Link)<{ theme: Theme }>`\n ${interactiveStyles};\n ${cardStyles}\n text-decoration: none;\n`;\n\nconst StyledCardTitle = styled.p<{ theme: Theme }>`\n font-weight: bold;\n margin: 5px 0;\n color: ${({ theme }) => theme.colors.dark};\n ${({ theme }) => styledText(theme)};\n`;\n\ninterface CardProps extends React.HTMLAttributes<HTMLDivElement> {\n children: React.ReactNode;\n title?: string;\n icon?: IconProps;\n href?: string;\n}\n\nfunction Card({ children, title, icon, href }: CardProps) {\n const theme = useTheme() as Theme;\n\n const content = (\n <>\n {icon && <Icon name={icon} color={theme.colors.primary} />}\n {title && <StyledCardTitle>{title}</StyledCardTitle>}\n {children}\n </>\n );\n\n if (href) {\n const isExternal = /^(https?:)?\\/\\//i.test(href);\n return (\n <StyledCardLink\n href={href}\n target={isExternal ? \"_blank\" : undefined}\n rel={isExternal ? \"noopener noreferrer\" : undefined}\n >\n {content}\n </StyledCardLink>\n );\n }\n\n return <StyledCard>{content}</StyledCard>;\n}\n\nexport { Card };\n";
@@ -35,7 +35,7 @@ const StyledCardTitle = styled.p<{ theme: Theme }>\`
35
35
 
36
36
  interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
37
37
  children: React.ReactNode;
38
- title: string;
38
+ title?: string;
39
39
  icon?: IconProps;
40
40
  href?: string;
41
41
  }
@@ -46,7 +46,7 @@ function Card({ children, title, icon, href }: CardProps) {
46
46
  const content = (
47
47
  <>
48
48
  {icon && <Icon name={icon} color={theme.colors.primary} />}
49
- <StyledCardTitle>{title}</StyledCardTitle>
49
+ {title && <StyledCardTitle>{title}</StyledCardTitle>}
50
50
  {children}
51
51
  </>
52
52
  );
@@ -1 +1 @@
1
- export declare const siteGateComponentTemplate = "\"use client\";\nimport { useState, type FormEvent } from \"react\";\nimport styled from \"styled-components\";\nimport { Input, ThemeToggle } from \"cherry-styled-components\";\nimport { Lock } from \"lucide-react\";\nimport { Theme } from \"@/app/theme\";\nimport { Button } from \"@/components/layout/Button\";\nimport { Callout } from \"@/components/layout/Callout\";\nimport { config } from \"@/utils/config\";\n\nconst StyledWrapper = styled.div<{ theme: Theme }>`\n min-height: 100dvh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n background: ${({ theme }) => theme.colors.light};\n`;\n\nconst StyledInner = styled.div`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n width: 100%;\n max-width: 380px;\n`;\n\nconst StyledFooter = styled.div`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n`;\n\nconst StyledBranding = styled.p<{ theme: Theme }>`\n margin: 0;\n font-size: 14px;\n color: ${({ theme }) => theme.colors.grayDark};\n\n & a {\n color: ${({ theme }) => theme.colors.accent};\n font-weight: 700;\n text-decoration: none;\n transition: all 0.3s ease;\n }\n\n & a:hover {\n color: ${({ theme }) => theme.colors.primary};\n }\n`;\n\nconst StyledCard = styled.div<{ theme: Theme }>`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n width: 100%;\n padding: 40px 32px;\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n box-shadow: ${({ theme }) => theme.shadows.sm};\n text-align: center;\n`;\n\nconst StyledIcon = styled.div<{ theme: Theme }>`\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 48px;\n height: 48px;\n border-radius: 50%;\n color: ${({ theme }) => theme.colors.accent};\n background: ${({ theme }) => theme.colors.grayLight};\n\n & svg {\n width: 22px;\n height: 22px;\n }\n`;\n\nconst StyledTitle = styled.h1<{ theme: Theme }>`\n margin: 0;\n font-size: 22px;\n color: ${({ theme }) => theme.colors.accent};\n`;\n\nconst StyledLede = styled.p<{ theme: Theme }>`\n margin: 0;\n font-size: 15px;\n color: ${({ theme }) => theme.colors.grayDark};\n`;\n\nconst StyledForm = styled.form`\n display: flex;\n flex-direction: column;\n gap: 20px;\n width: 100%;\n`;\n\nconst StyledAlert = styled.div`\n width: 100%;\n text-align: left;\n`;\n\nexport function SiteGate({ hideBranding }: { hideBranding?: boolean }) {\n const [password, setPassword] = useState(\"\");\n const [status, setStatus] = useState<\"idle\" | \"loading\" | \"error\">(\"idle\");\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n if (!password || status === \"loading\") return;\n\n setStatus(\"loading\");\n try {\n const res = await fetch(\"/api/gate\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ password }),\n });\n if (res.ok) {\n window.location.reload();\n return;\n }\n setStatus(\"error\");\n } catch {\n setStatus(\"error\");\n }\n }\n\n return (\n <StyledWrapper>\n <StyledInner>\n <StyledCard>\n <StyledIcon>\n <Lock />\n </StyledIcon>\n <StyledTitle>{config.name || \"Documentation\"}</StyledTitle>\n <StyledLede>This site is password protected.</StyledLede>\n <StyledForm onSubmit={handleSubmit} noValidate>\n <Input\n type=\"password\"\n name=\"password\"\n autoComplete=\"current-password\"\n placeholder=\"Enter password\"\n aria-label=\"Password\"\n autoFocus\n value={password}\n $error={status === \"error\"}\n $fullWidth\n onChange={(e) => {\n setPassword(e.target.value);\n if (status === \"error\") setStatus(\"idle\");\n }}\n />\n <Button\n type=\"submit\"\n fullWidth\n disabled={status === \"loading\" || !password}\n >\n {status === \"loading\" ? \"Unlocking...\" : \"Enter\"}\n </Button>\n </StyledForm>\n {status === \"error\" && (\n <StyledAlert>\n <Callout type=\"danger\">\n <p>Incorrect password. Try again.</p>\n </Callout>\n </StyledAlert>\n )}\n </StyledCard>\n <StyledFooter>\n <ThemeToggle />\n {!hideBranding && (\n <StyledBranding>\n Powered by{\" \"}\n <a\n href=\"https://doccupine.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Doccupine\n </a>\n </StyledBranding>\n )}\n </StyledFooter>\n </StyledInner>\n </StyledWrapper>\n );\n}\n";
1
+ export declare const siteGateComponentTemplate = "\"use client\";\nimport { useState, type FormEvent } from \"react\";\nimport styled from \"styled-components\";\nimport { Input, ThemeToggle } from \"cherry-styled-components\";\nimport { Lock } from \"lucide-react\";\nimport { Theme } from \"@/app/theme\";\nimport { Button } from \"@/components/layout/Button\";\nimport { Callout } from \"@/components/layout/Callout\";\nimport { config } from \"@/utils/config\";\n\nconst StyledWrapper = styled.div<{ theme: Theme }>`\n min-height: 100dvh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n background: ${({ theme }) => theme.colors.light};\n`;\n\nconst StyledInner = styled.div`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n width: 100%;\n max-width: 380px;\n`;\n\nconst StyledFooter = styled.div`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n`;\n\nconst StyledBranding = styled.p<{ theme: Theme }>`\n margin: 0;\n font-size: 14px;\n color: ${({ theme }) => theme.colors.grayDark};\n\n & a {\n color: ${({ theme }) => theme.colors.accent};\n font-weight: 700;\n text-decoration: none;\n transition: all 0.3s ease;\n }\n\n & a:hover {\n color: ${({ theme }) => theme.colors.primary};\n }\n`;\n\nconst StyledCard = styled.div<{ theme: Theme }>`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n width: 100%;\n padding: 40px 32px;\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n box-shadow: ${({ theme }) => theme.shadows.sm};\n text-align: center;\n`;\n\nconst StyledIcon = styled.div<{ theme: Theme }>`\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 48px;\n height: 48px;\n border-radius: 50%;\n color: ${({ theme }) => theme.colors.accent};\n background: ${({ theme }) => theme.colors.grayLight};\n\n & svg {\n width: 22px;\n height: 22px;\n }\n`;\n\nconst StyledTitle = styled.h1<{ theme: Theme }>`\n margin: 0;\n font-size: 22px;\n color: ${({ theme }) => theme.colors.accent};\n`;\n\nconst StyledLede = styled.p<{ theme: Theme }>`\n margin: 0;\n font-size: 15px;\n color: ${({ theme }) => theme.colors.grayDark};\n`;\n\nconst StyledForm = styled.form`\n display: flex;\n flex-direction: column;\n gap: 20px;\n width: 100%;\n`;\n\nconst StyledAlert = styled.div`\n width: 100%;\n text-align: left;\n`;\n\nexport function SiteGate({ hideBranding }: { hideBranding?: boolean }) {\n const [password, setPassword] = useState(\"\");\n const [status, setStatus] = useState<\"idle\" | \"loading\" | \"error\">(\"idle\");\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n if (!password || status === \"loading\") return;\n\n setStatus(\"loading\");\n try {\n const res = await fetch(\"/api/gate\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ password }),\n });\n if (res.ok) {\n window.location.reload();\n return;\n }\n setStatus(\"error\");\n } catch {\n setStatus(\"error\");\n }\n }\n\n return (\n <StyledWrapper>\n <StyledInner>\n <StyledCard>\n <StyledIcon>\n <Lock />\n </StyledIcon>\n <StyledTitle>{config.name || \"Documentation\"}</StyledTitle>\n <StyledLede>This site is password protected.</StyledLede>\n <StyledForm onSubmit={handleSubmit} noValidate>\n <Input\n type=\"password\"\n name=\"password\"\n autoComplete=\"current-password\"\n placeholder=\"Enter password\"\n aria-label=\"Password\"\n autoFocus\n value={password}\n $error={status === \"error\"}\n $fullWidth\n onChange={(e) => {\n setPassword(e.target.value);\n if (status === \"error\") setStatus(\"idle\");\n }}\n />\n <Button\n type=\"submit\"\n fullWidth\n disabled={status === \"loading\" || !password}\n >\n {status === \"loading\" ? \"Unlocking...\" : \"Enter\"}\n </Button>\n </StyledForm>\n {status === \"error\" && (\n <StyledAlert>\n <Callout type=\"danger\">\n <p>Incorrect password. Try again.</p>\n </Callout>\n </StyledAlert>\n )}\n </StyledCard>\n <StyledFooter>\n <ThemeToggle $shortcut />\n {!hideBranding && (\n <StyledBranding>\n Powered by{\" \"}\n <a\n href=\"https://doccupine.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Doccupine\n </a>\n </StyledBranding>\n )}\n </StyledFooter>\n </StyledInner>\n </StyledWrapper>\n );\n}\n";
@@ -171,7 +171,7 @@ export function SiteGate({ hideBranding }: { hideBranding?: boolean }) {
171
171
  )}
172
172
  </StyledCard>
173
173
  <StyledFooter>
174
- <ThemeToggle />
174
+ <ThemeToggle $shortcut />
175
175
  {!hideBranding && (
176
176
  <StyledBranding>
177
177
  Powered by{" "}
@@ -1 +1 @@
1
- export declare const slugTemplate = "// Shared heading/anchor id helpers.\n//\n// `slugify` turns heading text into a URL-safe base slug. `createSlugger`\n// wraps it with occurrence-based de-duplication so repeated heading text\n// yields stable, unique ids (\"setup\", \"setup-1\", \"setup-2\", ...) matching\n// the scheme used by GitHub and rehype-slug.\n//\n// The index sidebar (components/Docs.tsx) and the rendered headings\n// (components/MDXComponents.tsx, components/layout/Update.tsx) must walk the\n// document in the same order with a shared slugger so every sidebar link\n// resolves to exactly one heading.\n\nexport function slugify(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/\\s+/g, \"-\")\n .trim();\n}\n\nexport type Slugger = (text: string) => string;\n\nexport function createSlugger(): Slugger {\n const seen = new Map<string, number>();\n return (text: string) => {\n const base = slugify(text);\n const count = seen.get(base) ?? 0;\n seen.set(base, count + 1);\n return count === 0 ? base : `${base}-${count}`;\n };\n}\n";
1
+ export declare const slugTemplate = "// Shared heading/anchor id helpers.\n//\n// `slugify` turns heading text into a URL-safe base slug. `createSlugger`\n// wraps it with occurrence-based de-duplication so repeated heading text\n// yields stable, unique ids (\"setup\", \"setup-1\", \"setup-2\", ...) matching\n// the scheme used by GitHub and rehype-slug.\n//\n// The index sidebar (components/Docs.tsx) and the rendered headings\n// (components/MDXComponents.tsx, components/layout/Update.tsx) must walk the\n// document in the same order with a shared slugger so every sidebar link\n// resolves to exactly one heading.\n\nexport function slugify(text: string): string {\n return (text ?? \"\")\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/\\s+/g, \"-\")\n .trim();\n}\n\nexport type Slugger = (text: string) => string;\n\nexport function createSlugger(): Slugger {\n const seen = new Map<string, number>();\n return (text: string) => {\n const base = slugify(text);\n const count = seen.get(base) ?? 0;\n seen.set(base, count + 1);\n return count === 0 ? base : `${base}-${count}`;\n };\n}\n";
@@ -11,7 +11,7 @@ export const slugTemplate = `// Shared heading/anchor id helpers.
11
11
  // resolves to exactly one heading.
12
12
 
13
13
  export function slugify(text: string): string {
14
- return text
14
+ return (text ?? "")
15
15
  .toLowerCase()
16
16
  .replace(/[^\\w\\s-]/g, "")
17
17
  .replace(/\\s+/g, "-")
@@ -1 +1 @@
1
- export declare const stepsTemplate = "\"use client\";\nimport React from \"react\";\nimport styled, { useTheme } from \"styled-components\";\nimport { styledText } from \"cherry-styled-components\";\nimport { Theme } from \"@/app/theme\";\nimport { Icon, IconProps } from \"@/components/layout/Icon\";\n\nconst StyledStepsContainer = styled.div<{ theme: Theme }>`\n position: relative;\n width: 100%;\n`;\n\nconst StyledStep = styled.div<{ theme: Theme }>`\n background: ${({ theme }) => theme.colors.light};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n padding: 0 0 20px 52px;\n margin: 0;\n position: relative;\n ${({ theme }) => styledText(theme)}\n color: ${({ theme }) => theme.colors.grayDark};\n\n &::after {\n content: \"\";\n position: absolute;\n left: 16px;\n top: 0;\n width: 1px;\n height: 100%;\n background: ${({ theme }) => theme.colors.primary};\n background: linear-gradient(\n 180deg,\n ${({ theme }) => theme.colors.primary},\n transparent\n );\n border-radius: 4px;\n }\n`;\n\nconst StepNumber = styled.div<{ theme: Theme }>`\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: ${({ theme }) => theme.colors.primary};\n color: ${({ theme }) => theme.colors.light};\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 700;\n margin-bottom: 12px;\n ${({ theme }) => styledText(theme)};\n position: absolute;\n left: 0;\n top: 0;\n z-index: 1;\n`;\n\nconst StyledStepTitle = styled.div<{ theme: Theme }>`\n margin: 0 0 10px 0;\n padding: 2px 0 0 0;\n color: ${({ theme }) => theme.colors.dark};\n ${({ theme }) => styledText(theme)};\n font-weight: 700;\n display: flex;\n align-items: center;\n gap: 10px;\n`;\n\nconst StepContent = styled.div<{ theme: Theme }>`\n color: ${({ theme }) => theme.colors.grayDark};\n ${({ theme }) => styledText(theme)};\n\n & > .code-wrapper {\n margin: 10px 0;\n }\n`;\n\ninterface StepProps extends React.HTMLAttributes<HTMLDivElement> {\n title: string;\n children: React.ReactNode;\n icon?: IconProps;\n}\n\nfunction Step(_props: StepProps) {\n return null;\n}\n\ninterface StepsProps extends React.HTMLAttributes<HTMLDivElement> {\n children: React.ReactNode;\n}\n\nfunction Steps({ children }: StepsProps) {\n const theme = useTheme() as Theme;\n\n const steps = React.Children.toArray(children).filter(\n (child): child is React.ReactElement<StepProps> =>\n React.isValidElement(child),\n );\n\n return (\n <StyledStepsContainer theme={theme}>\n {steps.map((step, index) => {\n const { title, children: stepContent, icon } = step.props;\n\n return (\n <StyledStep key={index} theme={theme}>\n <StepNumber theme={theme}>{index + 1}</StepNumber>\n <StyledStepTitle theme={theme}>\n {icon && <Icon name={icon} color={theme.colors.primary} />}\n {title}\n </StyledStepTitle>\n <StepContent theme={theme}>{stepContent}</StepContent>\n </StyledStep>\n );\n })}\n </StyledStepsContainer>\n );\n}\n\nexport { Steps, Step };\n";
1
+ export declare const stepsTemplate = "\"use client\";\nimport React from \"react\";\nimport styled, { useTheme } from \"styled-components\";\nimport { styledText } from \"cherry-styled-components\";\nimport { Theme } from \"@/app/theme\";\nimport { Icon, IconProps } from \"@/components/layout/Icon\";\n\nconst StyledStepsContainer = styled.div<{ theme: Theme }>`\n position: relative;\n width: 100%;\n`;\n\nconst StyledStep = styled.div<{ theme: Theme }>`\n background: ${({ theme }) => theme.colors.light};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n padding: 0 0 20px 52px;\n margin: 0;\n position: relative;\n ${({ theme }) => styledText(theme)}\n color: ${({ theme }) => theme.colors.grayDark};\n\n &::after {\n content: \"\";\n position: absolute;\n left: 16px;\n top: 0;\n width: 1px;\n height: 100%;\n background: ${({ theme }) => theme.colors.primary};\n background: linear-gradient(\n 180deg,\n ${({ theme }) => theme.colors.primary},\n transparent\n );\n border-radius: 4px;\n }\n`;\n\nconst StepNumber = styled.div<{ theme: Theme }>`\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: ${({ theme }) => theme.colors.primary};\n color: ${({ theme }) => theme.colors.light};\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 700;\n margin-bottom: 12px;\n ${({ theme }) => styledText(theme)};\n position: absolute;\n left: 0;\n top: 0;\n z-index: 1;\n`;\n\nconst StyledStepTitle = styled.div<{ theme: Theme }>`\n margin: 0 0 10px 0;\n padding: 2px 0 0 0;\n color: ${({ theme }) => theme.colors.dark};\n ${({ theme }) => styledText(theme)};\n font-weight: 700;\n display: flex;\n align-items: center;\n gap: 10px;\n`;\n\nconst StepContent = styled.div<{ theme: Theme }>`\n color: ${({ theme }) => theme.colors.grayDark};\n ${({ theme }) => styledText(theme)};\n\n & > .code-wrapper {\n margin: 10px 0;\n }\n`;\n\ninterface StepProps extends React.HTMLAttributes<HTMLDivElement> {\n title?: string;\n children: React.ReactNode;\n icon?: IconProps;\n}\n\nfunction Step(_props: StepProps) {\n return null;\n}\n\ninterface StepsProps extends React.HTMLAttributes<HTMLDivElement> {\n children: React.ReactNode;\n}\n\nfunction Steps({ children }: StepsProps) {\n const theme = useTheme() as Theme;\n\n const steps = React.Children.toArray(children).filter(\n (child): child is React.ReactElement<StepProps> =>\n React.isValidElement(child),\n );\n\n return (\n <StyledStepsContainer theme={theme}>\n {steps.map((step, index) => {\n const { title, children: stepContent, icon } = step.props;\n\n return (\n <StyledStep key={index} theme={theme}>\n <StepNumber theme={theme}>{index + 1}</StepNumber>\n {(title || icon) && (\n <StyledStepTitle theme={theme}>\n {icon && <Icon name={icon} color={theme.colors.primary} />}\n {title}\n </StyledStepTitle>\n )}\n <StepContent theme={theme}>{stepContent}</StepContent>\n </StyledStep>\n );\n })}\n </StyledStepsContainer>\n );\n}\n\nexport { Steps, Step };\n";
@@ -75,7 +75,7 @@ const StepContent = styled.div<{ theme: Theme }>\`
75
75
  \`;
76
76
 
77
77
  interface StepProps extends React.HTMLAttributes<HTMLDivElement> {
78
- title: string;
78
+ title?: string;
79
79
  children: React.ReactNode;
80
80
  icon?: IconProps;
81
81
  }
@@ -104,10 +104,12 @@ function Steps({ children }: StepsProps) {
104
104
  return (
105
105
  <StyledStep key={index} theme={theme}>
106
106
  <StepNumber theme={theme}>{index + 1}</StepNumber>
107
- <StyledStepTitle theme={theme}>
108
- {icon && <Icon name={icon} color={theme.colors.primary} />}
109
- {title}
110
- </StyledStepTitle>
107
+ {(title || icon) && (
108
+ <StyledStepTitle theme={theme}>
109
+ {icon && <Icon name={icon} color={theme.colors.primary} />}
110
+ {title}
111
+ </StyledStepTitle>
112
+ )}
111
113
  <StepContent theme={theme}>{stepContent}</StepContent>
112
114
  </StyledStep>
113
115
  );
@@ -1 +1 @@
1
- export declare const updateTemplate = "\"use client\";\nimport styled from \"styled-components\";\nimport { styledSmall } from \"cherry-styled-components\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { slugify } from \"@/components/layout/Slug\";\n\nconst StyledUpdate = styled.div<{ theme: Theme; $columns?: number }>`\n position: relative;\n display: flex;\n gap: 20px;\n flex-direction: column;\n\n ${mq(\"lg\")} {\n margin: 20px 0;\n flex-direction: row;\n }\n`;\n\nconst StyledUpdateLabel = styled.a<{ theme: Theme }>`\n display: inline-block;\n width: fit-content;\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 20%, transparent)`};\n color: ${({ theme }) => theme.colors.primary};\n padding: 2px 4px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n font-weight: 600;\n text-decoration: none;\n ${({ theme }) => styledSmall(theme)};\n\n &:hover {\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 35%, transparent)`};\n }\n`;\n\nconst StyledUpdateDescription = styled.div<{ theme: Theme }>`\n ${({ theme }) => styledSmall(theme)};\n color: ${({ theme }) => theme.colors.gray};\n`;\n\nconst StyledUpdateSidebar = styled.div`\n display: flex;\n flex-direction: column;\n gap: 10px;\n\n ${mq(\"lg\")} {\n min-width: 160px;\n }\n`;\n\nconst StyledUpdateChildren = styled.div`\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 20px;\n`;\n\ninterface UpdateProps extends React.HTMLAttributes<HTMLDivElement> {\n children: React.ReactNode;\n label: string;\n description: string;\n}\n\nfunction Update({ children, label, description, id }: UpdateProps) {\n // MDXComponents injects a de-duplicated id from the shared slugger so the\n // index sidebar anchor resolves here; fall back to a plain slug when used\n // outside the MDX pipeline. The label doubles as an anchor so readers can\n // right-click to copy the deep link (native #anchor jump is offset by the\n // global scroll-padding-top).\n const anchor = id ?? slugify(label);\n return (\n <StyledUpdate id={anchor}>\n <StyledUpdateSidebar>\n <div>\n <StyledUpdateLabel href={`#${anchor}`}>{label}</StyledUpdateLabel>\n </div>\n <StyledUpdateDescription>{description}</StyledUpdateDescription>\n </StyledUpdateSidebar>\n <StyledUpdateChildren>{children}</StyledUpdateChildren>\n </StyledUpdate>\n );\n}\n\nexport { Update };\n";
1
+ export declare const updateTemplate = "\"use client\";\nimport styled from \"styled-components\";\nimport { styledSmall } from \"cherry-styled-components\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { slugify } from \"@/components/layout/Slug\";\n\nconst StyledUpdate = styled.div<{ theme: Theme; $columns?: number }>`\n position: relative;\n display: flex;\n gap: 20px;\n flex-direction: column;\n\n ${mq(\"lg\")} {\n margin: 20px 0;\n flex-direction: row;\n }\n`;\n\nconst StyledUpdateLabel = styled.a<{ theme: Theme }>`\n display: inline-block;\n width: fit-content;\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 20%, transparent)`};\n color: ${({ theme }) => theme.colors.primary};\n padding: 2px 4px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n font-weight: 600;\n text-decoration: none;\n ${({ theme }) => styledSmall(theme)};\n\n &:hover {\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 35%, transparent)`};\n }\n`;\n\nconst StyledUpdateDescription = styled.div<{ theme: Theme }>`\n ${({ theme }) => styledSmall(theme)};\n color: ${({ theme }) => theme.colors.gray};\n`;\n\nconst StyledUpdateSidebar = styled.div`\n display: flex;\n flex-direction: column;\n gap: 10px;\n\n ${mq(\"lg\")} {\n min-width: 160px;\n }\n`;\n\nconst StyledUpdateChildren = styled.div`\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 20px;\n`;\n\ninterface UpdateProps extends React.HTMLAttributes<HTMLDivElement> {\n children: React.ReactNode;\n label: string;\n description?: string;\n}\n\nfunction Update({ children, label = \"Label\", description, id }: UpdateProps) {\n // MDXComponents injects a de-duplicated id from the shared slugger so the\n // index sidebar anchor resolves here; fall back to a plain slug when used\n // outside the MDX pipeline. The label doubles as an anchor so readers can\n // right-click to copy the deep link (native #anchor jump is offset by the\n // global scroll-padding-top).\n const anchor = id ?? slugify(label);\n return (\n <StyledUpdate id={anchor}>\n <StyledUpdateSidebar>\n <div>\n <StyledUpdateLabel href={`#${anchor}`}>{label}</StyledUpdateLabel>\n </div>\n {description && (\n <StyledUpdateDescription>{description}</StyledUpdateDescription>\n )}\n </StyledUpdateSidebar>\n <StyledUpdateChildren>{children}</StyledUpdateChildren>\n </StyledUpdate>\n );\n}\n\nexport { Update };\n";
@@ -59,10 +59,10 @@ const StyledUpdateChildren = styled.div\`
59
59
  interface UpdateProps extends React.HTMLAttributes<HTMLDivElement> {
60
60
  children: React.ReactNode;
61
61
  label: string;
62
- description: string;
62
+ description?: string;
63
63
  }
64
64
 
65
- function Update({ children, label, description, id }: UpdateProps) {
65
+ function Update({ children, label = "Label", description, id }: UpdateProps) {
66
66
  // MDXComponents injects a de-duplicated id from the shared slugger so the
67
67
  // index sidebar anchor resolves here; fall back to a plain slug when used
68
68
  // outside the MDX pipeline. The label doubles as an anchor so readers can
@@ -75,7 +75,9 @@ function Update({ children, label, description, id }: UpdateProps) {
75
75
  <div>
76
76
  <StyledUpdateLabel href={\`#\${anchor}\`}>{label}</StyledUpdateLabel>
77
77
  </div>
78
- <StyledUpdateDescription>{description}</StyledUpdateDescription>
78
+ {description && (
79
+ <StyledUpdateDescription>{description}</StyledUpdateDescription>
80
+ )}
79
81
  </StyledUpdateSidebar>
80
82
  <StyledUpdateChildren>{children}</StyledUpdateChildren>
81
83
  </StyledUpdate>
@@ -1 +1 @@
1
- export declare const cardsMdxTemplate = "---\ntitle: \"Cards\"\ndescription: \"Cards act as visual containers for your content, giving you flexibility to combine text, icons, images, and links in a clean and organized way.\"\ndate: \"2026-02-19\"\ncategory: \"Components\"\ncategoryOrder: 1\norder: 6\n---\n\n# Cards\n\nDuplicate a page or section with ease, then emphasize important information or links using customizable layouts and icons.\n\nCards act as visual containers for your content, giving you flexibility to combine text, icons, images, and links in a clean and organized way.\n\n## Cards Usage\n\nYou can use the Cards component directly within your MDX files without any import. The following example shows a basic usage:\n\n```html\n<Card title=\"Note\" icon=\"badge-info\">\n Doccupine CLI is a command-line tool that helps you create and manage your\n Doccupine project. It provides a simple and intuitive interface for creating\n and configuring your project.\n</Card>\n```\n\n<Card title=\"Note\" icon=\"badge-info\">\n Doccupine CLI is a command-line tool that helps you create and manage your Doccupine project. It provides a simple and intuitive interface for creating and configuring your project.\n</Card>\n\n## Link Card\n\nPass a `href` prop to turn the card into a clickable link. The card will display interactive hover and focus styles automatically.\n\n```html\n<Card title=\"Getting Started\" icon=\"rocket\" href=\"/cards\">\n Learn how to set up Doccupine and create your first documentation site.\n</Card>\n```\n\n<Card title=\"Getting Started\" icon=\"rocket\" href=\"/cards\">\n Learn how to set up Doccupine and create your first documentation site.\n</Card>\n\n## Properties\n\n<Field value=\"title\" type=\"string\" required>\n The title of the card.\n</Field>\n\n<Field value=\"icon\" type=\"string\">\n The [Lucide](https://lucide.dev/icons) icon name to display in the card.\n</Field>\n\n<Field value=\"href\" type=\"string\">\n A URL or path to link to. When provided, the card becomes a clickable link with interactive styles.\n</Field>\n\n<Field value=\"children\" type=\"node\" required>\n The content of the card.\n</Field>";
1
+ export declare const cardsMdxTemplate = "---\ntitle: \"Cards\"\ndescription: \"Cards act as visual containers for your content, giving you flexibility to combine text, icons, images, and links in a clean and organized way.\"\ndate: \"2026-02-19\"\ncategory: \"Components\"\ncategoryOrder: 1\norder: 6\n---\n\n# Cards\n\nDuplicate a page or section with ease, then emphasize important information or links using customizable layouts and icons.\n\nCards act as visual containers for your content, giving you flexibility to combine text, icons, images, and links in a clean and organized way.\n\n## Cards Usage\n\nYou can use the Cards component directly within your MDX files without any import. The following example shows a basic usage:\n\n```html\n<Card title=\"Note\" icon=\"badge-info\">\n Doccupine CLI is a command-line tool that helps you create and manage your\n Doccupine project. It provides a simple and intuitive interface for creating\n and configuring your project.\n</Card>\n```\n\n<Card title=\"Note\" icon=\"badge-info\">\n Doccupine CLI is a command-line tool that helps you create and manage your Doccupine project. It provides a simple and intuitive interface for creating and configuring your project.\n</Card>\n\n## Link Card\n\nPass a `href` prop to turn the card into a clickable link. The card will display interactive hover and focus styles automatically.\n\n```html\n<Card title=\"Getting Started\" icon=\"rocket\" href=\"/cards\">\n Learn how to set up Doccupine and create your first documentation site.\n</Card>\n```\n\n<Card title=\"Getting Started\" icon=\"rocket\" href=\"/cards\">\n Learn how to set up Doccupine and create your first documentation site.\n</Card>\n\n## Properties\n\n<Field value=\"title\" type=\"string\">\n An optional title for the card. When omitted, the card renders without a title.\n</Field>\n\n<Field value=\"icon\" type=\"string\">\n The [Lucide](https://lucide.dev/icons) icon name to display in the card.\n</Field>\n\n<Field value=\"href\" type=\"string\">\n A URL or path to link to. When provided, the card becomes a clickable link with interactive styles.\n</Field>\n\n<Field value=\"children\" type=\"node\" required>\n The content of the card.\n</Field>";
@@ -45,8 +45,8 @@ Pass a \`href\` prop to turn the card into a clickable link. The card will displ
45
45
 
46
46
  ## Properties
47
47
 
48
- <Field value="title" type="string" required>
49
- The title of the card.
48
+ <Field value="title" type="string">
49
+ An optional title for the card. When omitted, the card renders without a title.
50
50
  </Field>
51
51
 
52
52
  <Field value="icon" type="string">
@@ -1 +1 @@
1
- export declare const platformAiAssistantMdxTemplate = "---\ntitle: \"AI Assistant\"\ndescription: \"Configure the built-in AI assistant that ships with every Doccupine documentation site.\"\ndate: \"2026-02-19\"\ncategory: \"Configuration\"\ncategoryOrder: 2\norder: 6\nsection: \"Platform\"\n---\n\n# AI Assistant\n\nEvery Doccupine site ships with a built-in AI assistant that helps visitors find answers across your documentation. The AI settings page lets you choose how it's powered.\n\n## Modes\n\n### Platform (default)\n\nUses Doccupine's built-in integration. Zero configuration needed - the AI assistant works out of the box with no API keys or setup.\n\nEach plan includes a monthly AI usage budget:\n\n| Plan | Monthly Budget |\n| ---------- | -------------- |\n| Trial | $2 |\n| Pro | $20 |\n| Enterprise | $50 |\n\nThe AI settings page shows a usage dashboard with your current spending and remaining budget. Usage resets automatically with your billing cycle.\n\n#### AI credit top-ups\n\nIf you run out of AI credits before your billing cycle resets, you can purchase a one-time top-up to increase your monthly limit. Available tiers:\n\n- **$5**\n- **$10**\n- **$20**\n\nTop-ups are added to your current cycle's budget immediately after purchase and reset when your billing cycle renews. You can purchase multiple top-ups in the same cycle.\n\n### Custom\n\nBring your own API key for full control over the AI model. Supported providers:\n\n- **OpenAI**\n- **Anthropic**\n- **Google**\n\nIn Custom mode, you can also configure:\n\n- **Embedding model** - the model used to index your documentation content\n- **Temperature** - controls response creativity (0.0 for focused answers, up to 1.0 for more varied responses)\n\nFor a complete list of available models, refer to the official documentation of your chosen provider.\n\n### Off\n\nCompletely disables the AI assistant on your site.\n\n<Callout type=\"warning\">\n AI settings are stored as environment variables on your deployment, not in a JSON file. After saving, a redeploy is triggered automatically to apply the changes.\n</Callout>\n\n## MCP server authentication\n\nEvery Doccupine site exposes an MCP (Model Context Protocol) endpoint at `/api/mcp`. This lets external AI tools query your documentation programmatically.\n\nYou can set an optional **API key** to restrict access to the MCP endpoint. When set, requests must include the key in their authorization header.\n\n<Callout type=\"note\">\n For more details on how the MCP endpoint works and how to connect it to AI tools, see the [Model Context Protocol documentation](/model-context-protocol).\n</Callout>";
1
+ export declare const platformAiAssistantMdxTemplate = "---\ntitle: \"AI Assistant\"\ndescription: \"Configure the built-in AI assistant that ships with every Doccupine documentation site.\"\ndate: \"2026-02-19\"\ncategory: \"Configuration\"\ncategoryOrder: 2\norder: 6\nsection: \"Platform\"\n---\n\n# AI Assistant\n\nEvery Doccupine site ships with a built-in AI assistant that helps visitors find answers across your documentation. The AI settings page lets you choose how it's powered.\n\n## Modes\n\n### Platform (default)\n\nUses Doccupine's built-in integration. Zero configuration needed - the AI assistant works out of the box with no API keys or setup.\n\nEach plan includes a monthly AI usage budget:\n\n| Plan | Monthly Budget |\n| ---------- | -------------- |\n| Trial | $2 |\n| Pro | $20 |\n| Enterprise | $50 |\n\nThe AI settings page shows a usage dashboard with your current spending and remaining budget. Usage resets automatically with your billing cycle.\n\n#### AI credit top-ups\n\nIf you run out of AI credits before your billing cycle resets, you can purchase a one-time top-up to increase your monthly limit. Available tiers:\n\n- **$5**\n- **$10**\n- **$20**\n\nTop-ups are added to your current cycle's budget immediately after purchase and reset when your billing cycle renews. You can purchase multiple top-ups in the same cycle.\n\n### Custom\n\nBring your own API key for full control over the AI model. Supported providers:\n\n- **OpenAI**\n- **Anthropic**\n- **Google**\n\nIn Custom mode, you can also configure:\n\n- **Embedding model** - the model used to index your documentation content\n- **Temperature** - controls response creativity (0.0 for focused answers, up to 1.0 for more varied responses)\n\nFor a complete list of available models, refer to the official documentation of your chosen provider.\n\n### Off\n\nCompletely disables the AI assistant on your site.\n\n<Callout type=\"warning\">\n AI settings are stored as environment variables on your deployment, not in a JSON file. After saving, a redeploy is triggered automatically to apply the changes.\n</Callout>\n\n<Callout type=\"note\">\n Connecting external AI tools (Claude, Cursor, and others) to your site is configured on the dedicated [MCP](/platform/mcp) page, which also covers MCP server authentication.\n</Callout>";
@@ -61,12 +61,6 @@ Completely disables the AI assistant on your site.
61
61
  AI settings are stored as environment variables on your deployment, not in a JSON file. After saving, a redeploy is triggered automatically to apply the changes.
62
62
  </Callout>
63
63
 
64
- ## MCP server authentication
65
-
66
- Every Doccupine site exposes an MCP (Model Context Protocol) endpoint at \`/api/mcp\`. This lets external AI tools query your documentation programmatically.
67
-
68
- You can set an optional **API key** to restrict access to the MCP endpoint. When set, requests must include the key in their authorization header.
69
-
70
64
  <Callout type="note">
71
- For more details on how the MCP endpoint works and how to connect it to AI tools, see the [Model Context Protocol documentation](/model-context-protocol).
65
+ Connecting external AI tools (Claude, Cursor, and others) to your site is configured on the dedicated [MCP](/platform/mcp) page, which also covers MCP server authentication.
72
66
  </Callout>`;
@@ -0,0 +1 @@
1
+ export declare const platformMcpMdxTemplate = "---\ntitle: \"MCP\"\ndescription: \"Connect external AI apps like Claude and Cursor to your documentation with a Model Context Protocol server, and protect it with an API key.\"\ndate: \"2026-07-12\"\ncategory: \"Configuration\"\ncategoryOrder: 2\norder: 7\nsection: \"Platform\"\n---\n\n# MCP\n\nThe **MCP** settings page lets external AI applications connect to your documentation through a hosted Model Context Protocol (MCP) server. Every Doccupine site exposes an MCP endpoint at `/api/mcp` that AI tools can query to search and read your content.\n\n## Connect your site with AI apps\n\nThe **Connect your site with AI apps** card gives you a ready-to-paste MCP configuration for popular AI tools. Pick a tab, copy the snippet, and add it to that tool's MCP config:\n\n- **Claude** - add the configuration to Claude Desktop's `claude_desktop_config.json`, or add the server URL as a connector from Claude's settings.\n- **Cursor** - add the configuration to Cursor's `~/.cursor/mcp.json`, or connect it from Cursor's MCP settings.\n- **Others** - a generic `mcpServers` block for any MCP-compatible application.\n\nEvery snippet points to your site's own MCP endpoint, for example:\n\n```json\n{\n \"mcpServers\": {\n \"Your Project\": { \"url\": \"https://your-site.com/api/mcp\" }\n }\n}\n```\n\n<Callout type=\"note\">\n The connection snippet appears once your site has been deployed. If you haven't published yet, deploy your site first to get its MCP server URL.\n</Callout>\n\n## MCP server authentication\n\nBy default your MCP endpoint is publicly accessible. You can restrict access with an API key:\n\n1. Enable **Require API key for MCP access**.\n2. Enter an API key.\n3. Save.\n\nOnce enabled, clients must include the key as a Bearer token in the `Authorization` header of every request to `/api/mcp`.\n\n<Callout type=\"warning\">\n The API key is stored as an environment variable (`DOCS_API_KEY`) on your deployment, not in a JSON file. After saving, a redeploy is triggered automatically to apply the change.\n</Callout>\n\n<Callout type=\"note\">\n For the full reference on how the MCP server works - available tools, rate limits, and endpoint details - see the [Model Context Protocol documentation](/model-context-protocol).\n</Callout>";
@@ -0,0 +1,53 @@
1
+ export const platformMcpMdxTemplate = `---
2
+ title: "MCP"
3
+ description: "Connect external AI apps like Claude and Cursor to your documentation with a Model Context Protocol server, and protect it with an API key."
4
+ date: "2026-07-12"
5
+ category: "Configuration"
6
+ categoryOrder: 2
7
+ order: 7
8
+ section: "Platform"
9
+ ---
10
+
11
+ # MCP
12
+
13
+ The **MCP** settings page lets external AI applications connect to your documentation through a hosted Model Context Protocol (MCP) server. Every Doccupine site exposes an MCP endpoint at \`/api/mcp\` that AI tools can query to search and read your content.
14
+
15
+ ## Connect your site with AI apps
16
+
17
+ The **Connect your site with AI apps** card gives you a ready-to-paste MCP configuration for popular AI tools. Pick a tab, copy the snippet, and add it to that tool's MCP config:
18
+
19
+ - **Claude** - add the configuration to Claude Desktop's \`claude_desktop_config.json\`, or add the server URL as a connector from Claude's settings.
20
+ - **Cursor** - add the configuration to Cursor's \`~/.cursor/mcp.json\`, or connect it from Cursor's MCP settings.
21
+ - **Others** - a generic \`mcpServers\` block for any MCP-compatible application.
22
+
23
+ Every snippet points to your site's own MCP endpoint, for example:
24
+
25
+ \`\`\`json
26
+ {
27
+ "mcpServers": {
28
+ "Your Project": { "url": "https://your-site.com/api/mcp" }
29
+ }
30
+ }
31
+ \`\`\`
32
+
33
+ <Callout type="note">
34
+ The connection snippet appears once your site has been deployed. If you haven't published yet, deploy your site first to get its MCP server URL.
35
+ </Callout>
36
+
37
+ ## MCP server authentication
38
+
39
+ By default your MCP endpoint is publicly accessible. You can restrict access with an API key:
40
+
41
+ 1. Enable **Require API key for MCP access**.
42
+ 2. Enter an API key.
43
+ 3. Save.
44
+
45
+ Once enabled, clients must include the key as a Bearer token in the \`Authorization\` header of every request to \`/api/mcp\`.
46
+
47
+ <Callout type="warning">
48
+ The API key is stored as an environment variable (\`DOCS_API_KEY\`) on your deployment, not in a JSON file. After saving, a redeploy is triggered automatically to apply the change.
49
+ </Callout>
50
+
51
+ <Callout type="note">
52
+ For the full reference on how the MCP server works - available tools, rate limits, and endpoint details - see the [Model Context Protocol documentation](/model-context-protocol).
53
+ </Callout>`;
@@ -1 +1 @@
1
- export declare const stepsMdxTemplate = "---\ntitle: \"Steps\"\ndescription: \"Guide readers step-by-step using the Steps component.\"\ndate: \"2026-02-19\"\ncategory: \"Components\"\ncategoryOrder: 1\norder: 14\n---\n\n# Steps\n\nGuide readers step-by-step using the Steps component.\n\nThe Steps component is perfect for organizing procedures or workflows in a clear sequence. Include as many individual steps as necessary to outline your process.\n\n## Steps Usage\n\nYou can use the `Steps` component to create a step-by-step guide. Each step is represented by a `Step` component, which includes a title and content.\n\n```html\n<Steps>\n <Step title=\"Step 1\">\n Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Step>\n <Step title=\"Step 2\">\n Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Step>\n <Step title=\"Step 3\">\n Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Step>\n</Steps>\n```\n\n<Steps>\n <Step title=\"Step 1\">\n Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Step>\n <Step title=\"Step 2\">\n Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Step>\n <Step title=\"Step 3\">\n Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Step>\n</Steps>\n\n## Properties\n\n<Field value=\"title\" type=\"string\" required>\n The title of the step.\n</Field>\n\n<Field value=\"icon\" type=\"string\">\n A [Lucide](https://lucide.dev/icons) icon name shown next to the step title.\n</Field>\n\n<Field value=\"children\" type=\"node\" required>\n The content of the step.\n</Field>";
1
+ export declare const stepsMdxTemplate = "---\ntitle: \"Steps\"\ndescription: \"Guide readers step-by-step using the Steps component.\"\ndate: \"2026-02-19\"\ncategory: \"Components\"\ncategoryOrder: 1\norder: 14\n---\n\n# Steps\n\nGuide readers step-by-step using the Steps component.\n\nThe Steps component is perfect for organizing procedures or workflows in a clear sequence. Include as many individual steps as necessary to outline your process.\n\n## Steps Usage\n\nYou can use the `Steps` component to create a step-by-step guide. Each step is represented by a `Step` component, which includes a title and content.\n\n```html\n<Steps>\n <Step title=\"Step 1\">\n Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Step>\n <Step title=\"Step 2\">\n Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Step>\n <Step title=\"Step 3\">\n Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Step>\n</Steps>\n```\n\n<Steps>\n <Step title=\"Step 1\">\n Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Step>\n <Step title=\"Step 2\">\n Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Step>\n <Step title=\"Step 3\">\n Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n </Step>\n</Steps>\n\n## Properties\n\n<Field value=\"title\" type=\"string\">\n An optional title for the step. When omitted, the step renders without a title row.\n</Field>\n\n<Field value=\"icon\" type=\"string\">\n A [Lucide](https://lucide.dev/icons) icon name shown next to the step title.\n</Field>\n\n<Field value=\"children\" type=\"node\" required>\n The content of the step.\n</Field>";