doccupine 0.0.102 → 0.0.104

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 (59) hide show
  1. package/dist/index.js +7 -0
  2. package/dist/lib/layout.js +1 -0
  3. package/dist/lib/structures.js +5 -1
  4. package/dist/lib/types.d.ts +2 -0
  5. package/dist/templates/app/not-found.d.ts +1 -1
  6. package/dist/templates/app/not-found.js +2 -6
  7. package/dist/templates/components/Docs.d.ts +1 -1
  8. package/dist/templates/components/Docs.js +10 -14
  9. package/dist/templates/components/MDXComponents.d.ts +1 -1
  10. package/dist/templates/components/MDXComponents.js +16 -15
  11. package/dist/templates/components/SectionNavProvider.d.ts +1 -1
  12. package/dist/templates/components/SectionNavProvider.js +8 -1
  13. package/dist/templates/components/SideBar.d.ts +1 -1
  14. package/dist/templates/components/SideBar.js +136 -19
  15. package/dist/templates/components/layout/DocsComponents.d.ts +1 -1
  16. package/dist/templates/components/layout/DocsComponents.js +124 -6
  17. package/dist/templates/components/layout/DocsNavigation.d.ts +1 -1
  18. package/dist/templates/components/layout/DocsNavigation.js +27 -12
  19. package/dist/templates/components/layout/Footer.d.ts +1 -1
  20. package/dist/templates/components/layout/Footer.js +7 -0
  21. package/dist/templates/components/layout/Header.d.ts +1 -1
  22. package/dist/templates/components/layout/Header.js +3 -0
  23. package/dist/templates/components/layout/NotFound.d.ts +1 -0
  24. package/dist/templates/components/layout/NotFound.js +76 -0
  25. package/dist/templates/components/layout/SectionBar.d.ts +1 -1
  26. package/dist/templates/components/layout/SectionBar.js +9 -0
  27. package/dist/templates/components/layout/SharedStyles.d.ts +1 -1
  28. package/dist/templates/components/layout/SharedStyles.js +24 -0
  29. package/dist/templates/components/layout/Slug.d.ts +1 -0
  30. package/dist/templates/components/layout/Slug.js +32 -0
  31. package/dist/templates/components/layout/Steps.d.ts +1 -1
  32. package/dist/templates/components/layout/Steps.js +2 -1
  33. package/dist/templates/components/layout/Tabs.d.ts +1 -1
  34. package/dist/templates/components/layout/Tabs.js +11 -0
  35. package/dist/templates/components/layout/Update.d.ts +1 -1
  36. package/dist/templates/components/layout/Update.js +19 -14
  37. package/dist/templates/mdx/components.mdx.d.ts +1 -1
  38. package/dist/templates/mdx/components.mdx.js +1 -0
  39. package/dist/templates/mdx/globals.mdx.d.ts +1 -1
  40. package/dist/templates/mdx/globals.mdx.js +1 -0
  41. package/dist/templates/mdx/index.mdx.d.ts +1 -1
  42. package/dist/templates/mdx/index.mdx.js +1 -0
  43. package/dist/templates/mdx/navigation.mdx.d.ts +1 -1
  44. package/dist/templates/mdx/navigation.mdx.js +69 -1
  45. package/dist/templates/mdx/platform/build-and-deploy.mdx.d.ts +1 -1
  46. package/dist/templates/mdx/platform/build-and-deploy.mdx.js +1 -0
  47. package/dist/templates/mdx/platform/file-editor.mdx.d.ts +1 -1
  48. package/dist/templates/mdx/platform/file-editor.mdx.js +1 -0
  49. package/dist/templates/mdx/platform/index.mdx.d.ts +1 -1
  50. package/dist/templates/mdx/platform/index.mdx.js +1 -0
  51. package/dist/templates/mdx/platform/project-settings.mdx.d.ts +1 -1
  52. package/dist/templates/mdx/platform/project-settings.mdx.js +1 -0
  53. package/dist/templates/mdx/platform/site-settings.mdx.d.ts +1 -1
  54. package/dist/templates/mdx/platform/site-settings.mdx.js +1 -0
  55. package/dist/templates/mdx/tabs.mdx.d.ts +1 -1
  56. package/dist/templates/mdx/tabs.mdx.js +13 -6
  57. package/dist/templates/utils/orderNavItems.d.ts +1 -1
  58. package/dist/templates/utils/orderNavItems.js +11 -0
  59. package/package.json +1 -1
@@ -11,6 +11,7 @@ import {
11
11
  import Link from "next/link";
12
12
  import { mq, Theme } from "@/app/theme";
13
13
  import {
14
+ focusRing,
14
15
  styledAnchor,
15
16
  styledTable,
16
17
  stylesLists,
@@ -114,6 +115,7 @@ export const StyledMarkdownContainer = styled.div\`
114
115
  interface Props {
115
116
  theme?: Theme;
116
117
  $isActive?: boolean;
118
+ $isOpen?: boolean;
117
119
  $hasSectionBar?: boolean;
118
120
  }
119
121
 
@@ -251,6 +253,7 @@ export const StyledIndexSidebarLink = styled.a<{
251
253
  font-weight: \${({ $isActive }) => ($isActive ? "600" : "400")};
252
254
  text-decoration: none;
253
255
  transition: all 0.3s ease;
256
+ \${focusRing};
254
257
 
255
258
  &:hover {
256
259
  color: \${({ theme }) => theme.colors.primary};
@@ -271,6 +274,13 @@ export const StyledStrong = styled.strong<{ theme: Theme }>\`
271
274
  font-weight: 600;
272
275
  \${({ theme }) => styledStrong(theme)};
273
276
  color: \${({ theme }) => theme.colors.accentStrong};
277
+ display: inline-flex;
278
+ align-items: center;
279
+ gap: 8px;
280
+
281
+ & svg {
282
+ flex-shrink: 0;
283
+ }
274
284
  \`;
275
285
 
276
286
  export const StyledSidebarListItem = styled.li\`
@@ -279,28 +289,136 @@ export const StyledSidebarListItem = styled.li\`
279
289
  clear: both;
280
290
  \`;
281
291
 
282
- export const StyledSidebarListItemLink = styled(Link)<Props>\`
292
+ // Shared appearance for every sidebar row - leaf links AND nested group
293
+ // headers - so hover, active state, and the left rail read identically.
294
+ const sidebarRowStyles = css<Props>\`
283
295
  text-decoration: none;
284
296
  font-size: \${({ theme }) => theme.fontSizes.small.lg};
285
297
  line-height: 1.6;
286
298
  color: \${({ theme }) => theme.colors.accentMuted};
287
299
  padding: 5px 0 5px 20px;
288
300
  display: flex;
301
+ align-items: center;
302
+ gap: 8px;
303
+ flex: 1;
304
+ min-width: 0;
289
305
  transition: all 0.3s ease;
290
306
  border-left: solid 1px \${({ theme }) => theme.colors.grayLight};
291
307
 
308
+ & svg {
309
+ flex-shrink: 0;
310
+ }
311
+
292
312
  &:hover {
293
313
  color: \${({ theme }) => theme.colors.accent};
294
314
  border-color: \${({ theme }) => theme.colors.primary};
295
315
  }
296
316
 
317
+ \${focusRing};
318
+
297
319
  \${({ $isActive, theme }) =>
298
320
  $isActive &&
299
- \`
300
- color: \${theme.colors.accentStrong};
301
- border-color: \${theme.colors.primary};
302
- font-weight: 600;
303
- \`};
321
+ css\`
322
+ color: \${theme.colors.accentStrong};
323
+ border-color: \${theme.colors.primary};
324
+ font-weight: 600;
325
+ \`};
326
+ \`;
327
+
328
+ // The collapse chevron points right when closed and rotates to point down
329
+ // when the group is open.
330
+ const sidebarChevron = css<Props>\`
331
+ & .lucide-chevron-right {
332
+ margin-left: auto;
333
+ transition: transform 0.3s ease;
334
+
335
+ \${({ $isOpen }) =>
336
+ $isOpen &&
337
+ css\`
338
+ transform: rotate(90deg);
339
+ \`}
340
+ }
341
+ \`;
342
+
343
+ export const StyledSidebarListItemLink = styled(Link)<Props>\`
344
+ \${sidebarRowStyles};
345
+ \`;
346
+
347
+ // Nested navigation group. The header shares the link appearance/hover; a
348
+ // non-navigable group is a single toggle button, while a group that is also a
349
+ // page pairs a link with a chevron toggle. Children live in
350
+ // StyledSidebarGroupContent, which animates open/closed via height 0 <-> auto
351
+ // (enabled by interpolate-size in GlobalStyles, same as the Accordion).
352
+ export const StyledSidebarGroupButton = styled.button<Props>\`
353
+ \${resetButton};
354
+ \${sidebarRowStyles};
355
+ \${sidebarChevron};
356
+ width: 100%;
357
+ text-align: left;
358
+ \`;
359
+
360
+ export const StyledSidebarGroupRow = styled.div<Props>\`
361
+ \${sidebarRowStyles};
362
+ \${sidebarChevron};
363
+
364
+ /* The row is a plain div, so it never matches :focus-visible itself. Ring the
365
+ whole row when its inner link or chevron takes keyboard focus instead, to
366
+ match the leaf-row focus treatment. */
367
+ &:has(:focus-visible) {
368
+ border-radius: \${({ theme }) => theme.spacing.radius.xs};
369
+ box-shadow: 0 0 0 4px \${({ theme }) => theme.colors.primaryLight};
370
+ }
371
+ \`;
372
+
373
+ export const StyledSidebarGroupLink = styled(Link)\`
374
+ display: flex;
375
+ align-items: center;
376
+ gap: 8px;
377
+ flex: 1;
378
+ min-width: 0;
379
+ color: inherit;
380
+ text-decoration: none;
381
+
382
+ & svg {
383
+ flex-shrink: 0;
384
+ }
385
+
386
+ /* Ring is drawn on the parent row via :has(:focus-visible). */
387
+ &:focus-visible {
388
+ outline: none;
389
+ }
390
+ \`;
391
+
392
+ export const StyledSidebarGroupChevron = styled.button\`
393
+ display: inline-flex;
394
+ align-items: center;
395
+ justify-content: center;
396
+ flex-shrink: 0;
397
+ padding: 0;
398
+ border: 0;
399
+ background: none;
400
+ color: inherit;
401
+ cursor: pointer;
402
+
403
+ /* Ring is drawn on the parent row via :has(:focus-visible). */
404
+ &:focus-visible {
405
+ outline: none;
406
+ }
407
+ \`;
408
+
409
+ export const StyledSidebarGroupContent = styled.ul<Props>\`
410
+ list-style: none;
411
+ margin: 0;
412
+ padding: 0;
413
+ height: 0;
414
+ overflow: clip;
415
+ transition: all 0.3s ease;
416
+
417
+ \${({ $isOpen }) =>
418
+ $isOpen &&
419
+ css\`
420
+ height: auto;
421
+ \`}
304
422
  \`;
305
423
 
306
424
  export const StyleMobileBar = styled.button<Props>\`
@@ -1 +1 @@
1
- export declare const docsNavigationTemplate = "\"use client\";\nimport { useContext } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport Link from \"next/link\";\nimport styled, { css } from \"styled-components\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { interactiveStyles } from \"@/components/layout/SharedStyled\";\nimport { ChatContext } from \"@/components/Chat\";\n\nconst StyledNavigationWrapper = styled.div<{\n $isChatOpen?: boolean;\n}>`\n transition: all 0.3s ease;\n padding: 0 20px 100px 20px;\n ${mq(\"lg\")} {\n padding: 0 300px 80px 300px;\n ${({ $isChatOpen }) =>\n $isChatOpen &&\n css`\n padding: 0 440px 80px 300px;\n `}\n }\n`;\n\nconst StyledNavigationInner = styled.div`\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 20px;\n max-width: 640px;\n margin: auto;\n`;\n\nconst StyledNavButton = styled(Link)<{ theme: Theme }>`\n ${interactiveStyles};\n display: flex;\n flex-direction: column;\n text-decoration: none;\n padding: 20px;\n flex: 50%;\n max-width: 50%;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n color: ${({ theme }) => theme.colors.dark};\n\n &:hover {\n border-color: ${({ theme }) => theme.colors.primary};\n }\n\n &[data-direction=\"prev\"] {\n align-items: flex-start;\n }\n\n &[data-direction=\"next\"] {\n align-items: flex-end;\n margin-left: auto;\n text-align: right;\n }\n`;\n\nconst StyledNavLabel = styled.span<{ theme: Theme }>`\n color: ${({ theme }) => theme.colors.grayDark};\n display: flex;\n flex-direction: row;\n gap: 4px;\n\n & svg {\n margin: auto 0;\n }\n`;\n\nconst StyledNavTitle = styled.span<{ theme: Theme }>`\n color: ${({ theme }) => theme.colors.dark};\n font-weight: 600;\n margin: 0 0 4px 0;\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n max-width: 100%;\n`;\n\nconst StyledSpacer = styled.div`\n flex: 1;\n`;\n\ninterface Page {\n slug: string;\n title: string;\n category?: string;\n [key: string]: unknown;\n}\n\ninterface NavigationItem {\n category?: string;\n slug?: string;\n title?: string;\n links?: Page[];\n items?: Page[];\n [key: string]: unknown;\n}\n\ninterface DocsNavigationProps {\n result: NavigationItem[];\n}\n\nfunction DocsNavigation({ result }: DocsNavigationProps) {\n const { isOpen } = useContext(ChatContext);\n const pathname = usePathname();\n const allPages: Page[] = result.flatMap((item) => {\n if (item.links && Array.isArray(item.links)) {\n return item.links;\n }\n if (item.items && Array.isArray(item.items)) {\n return item.items;\n }\n if (item.slug !== undefined) {\n return [item as Page];\n }\n return [];\n });\n const currentSlug = pathname.replace(/^\\//, \"\").replace(/\\/$/, \"\");\n const currentIndex = allPages.findIndex((page) => page.slug === currentSlug);\n const prevPage = currentIndex > 0 ? allPages[currentIndex - 1] : null;\n const nextPage =\n currentIndex < allPages.length - 1 ? allPages[currentIndex + 1] : null;\n if (currentIndex === -1 || allPages.length === 0) {\n return null;\n }\n if (!prevPage && !nextPage) {\n return null;\n }\n return (\n <StyledNavigationWrapper $isChatOpen={isOpen}>\n <StyledNavigationInner>\n {prevPage ? (\n <StyledNavButton href={`/${prevPage.slug}`} data-direction=\"prev\">\n <StyledNavTitle>{prevPage.title}</StyledNavTitle>\n <StyledNavLabel>\n <Icon name=\"arrow-left\" size={16} /> Previous\n </StyledNavLabel>\n </StyledNavButton>\n ) : (\n <StyledSpacer />\n )}\n {nextPage && (\n <StyledNavButton href={`/${nextPage.slug}`} data-direction=\"next\">\n <StyledNavTitle>{nextPage.title}</StyledNavTitle>\n <StyledNavLabel>\n Next <Icon name=\"arrow-right\" size={16} />\n </StyledNavLabel>\n </StyledNavButton>\n )}\n </StyledNavigationInner>\n </StyledNavigationWrapper>\n );\n}\n\nexport { DocsNavigation };\n";
1
+ export declare const docsNavigationTemplate = "\"use client\";\nimport { useContext } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport Link from \"next/link\";\nimport styled, { css } from \"styled-components\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { interactiveStyles } from \"@/components/layout/SharedStyled\";\nimport { ChatContext } from \"@/components/Chat\";\n\nconst StyledNavigationWrapper = styled.div<{\n $isChatOpen?: boolean;\n}>`\n transition: all 0.3s ease;\n padding: 0 20px 100px 20px;\n ${mq(\"lg\")} {\n padding: 0 300px 80px 300px;\n ${({ $isChatOpen }) =>\n $isChatOpen &&\n css`\n padding: 0 440px 80px 300px;\n `}\n }\n`;\n\nconst StyledNavigationInner = styled.div`\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 20px;\n max-width: 640px;\n margin: auto;\n`;\n\nconst StyledNavButton = styled(Link)<{ theme: Theme }>`\n ${interactiveStyles};\n display: flex;\n flex-direction: column;\n text-decoration: none;\n padding: 20px;\n flex: 50%;\n max-width: 50%;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n color: ${({ theme }) => theme.colors.dark};\n\n &:hover {\n border-color: ${({ theme }) => theme.colors.primary};\n }\n\n &[data-direction=\"prev\"] {\n align-items: flex-start;\n }\n\n &[data-direction=\"next\"] {\n align-items: flex-end;\n margin-left: auto;\n text-align: right;\n }\n`;\n\nconst StyledNavLabel = styled.span<{ theme: Theme }>`\n color: ${({ theme }) => theme.colors.grayDark};\n display: flex;\n flex-direction: row;\n gap: 4px;\n\n & svg {\n margin: auto 0;\n }\n`;\n\nconst StyledNavTitle = styled.span<{ theme: Theme }>`\n color: ${({ theme }) => theme.colors.dark};\n font-weight: 600;\n margin: 0 0 4px 0;\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n max-width: 100%;\n`;\n\nconst StyledSpacer = styled.div`\n flex: 1;\n`;\n\ninterface Page {\n // Optional: nested group nodes may act as headers without their own page.\n // collectPages() only pushes nodes that actually have a slug.\n slug?: string;\n title: string;\n category?: string;\n links?: Page[];\n items?: Page[];\n [key: string]: unknown;\n}\n\ninterface NavigationItem {\n category?: string;\n slug?: string;\n title?: string;\n links?: Page[];\n items?: Page[];\n [key: string]: unknown;\n}\n\ninterface DocsNavigationProps {\n result: NavigationItem[];\n}\n\nfunction DocsNavigation({ result }: DocsNavigationProps) {\n const { isOpen } = useContext(ChatContext);\n const pathname = usePathname();\n // Walk categories and any nested link groups depth-first so prev/next spans\n // every real page (a node with a slug), in reading order.\n const collectPages = (nodes: unknown): Page[] => {\n if (!Array.isArray(nodes)) return [];\n const pages: Page[] = [];\n for (const node of nodes as Page[]) {\n if (node && node.slug !== undefined) {\n pages.push(node);\n }\n const children = node && ((node.links ?? node.items) as unknown);\n if (Array.isArray(children)) {\n pages.push(...collectPages(children));\n }\n }\n return pages;\n };\n const allPages: Page[] = result.flatMap((item) =>\n collectPages(\n item.links ??\n item.items ??\n (item.slug !== undefined ? [item as Page] : []),\n ),\n );\n const currentSlug = pathname.replace(/^\\//, \"\").replace(/\\/$/, \"\");\n const currentIndex = allPages.findIndex((page) => page.slug === currentSlug);\n const prevPage = currentIndex > 0 ? allPages[currentIndex - 1] : null;\n const nextPage =\n currentIndex < allPages.length - 1 ? allPages[currentIndex + 1] : null;\n if (currentIndex === -1 || allPages.length === 0) {\n return null;\n }\n if (!prevPage && !nextPage) {\n return null;\n }\n return (\n <StyledNavigationWrapper $isChatOpen={isOpen}>\n <StyledNavigationInner>\n {prevPage ? (\n <StyledNavButton href={`/${prevPage.slug}`} data-direction=\"prev\">\n <StyledNavTitle>{prevPage.title}</StyledNavTitle>\n <StyledNavLabel>\n <Icon name=\"arrow-left\" size={16} /> Previous\n </StyledNavLabel>\n </StyledNavButton>\n ) : (\n <StyledSpacer />\n )}\n {nextPage && (\n <StyledNavButton href={`/${nextPage.slug}`} data-direction=\"next\">\n <StyledNavTitle>{nextPage.title}</StyledNavTitle>\n <StyledNavLabel>\n Next <Icon name=\"arrow-right\" size={16} />\n </StyledNavLabel>\n </StyledNavButton>\n )}\n </StyledNavigationInner>\n </StyledNavigationWrapper>\n );\n}\n\nexport { DocsNavigation };\n";
@@ -86,9 +86,13 @@ const StyledSpacer = styled.div\`
86
86
  \`;
87
87
 
88
88
  interface Page {
89
- slug: string;
89
+ // Optional: nested group nodes may act as headers without their own page.
90
+ // collectPages() only pushes nodes that actually have a slug.
91
+ slug?: string;
90
92
  title: string;
91
93
  category?: string;
94
+ links?: Page[];
95
+ items?: Page[];
92
96
  [key: string]: unknown;
93
97
  }
94
98
 
@@ -108,18 +112,29 @@ interface DocsNavigationProps {
108
112
  function DocsNavigation({ result }: DocsNavigationProps) {
109
113
  const { isOpen } = useContext(ChatContext);
110
114
  const pathname = usePathname();
111
- const allPages: Page[] = result.flatMap((item) => {
112
- if (item.links && Array.isArray(item.links)) {
113
- return item.links;
115
+ // Walk categories and any nested link groups depth-first so prev/next spans
116
+ // every real page (a node with a slug), in reading order.
117
+ const collectPages = (nodes: unknown): Page[] => {
118
+ if (!Array.isArray(nodes)) return [];
119
+ const pages: Page[] = [];
120
+ for (const node of nodes as Page[]) {
121
+ if (node && node.slug !== undefined) {
122
+ pages.push(node);
123
+ }
124
+ const children = node && ((node.links ?? node.items) as unknown);
125
+ if (Array.isArray(children)) {
126
+ pages.push(...collectPages(children));
127
+ }
114
128
  }
115
- if (item.items && Array.isArray(item.items)) {
116
- return item.items;
117
- }
118
- if (item.slug !== undefined) {
119
- return [item as Page];
120
- }
121
- return [];
122
- });
129
+ return pages;
130
+ };
131
+ const allPages: Page[] = result.flatMap((item) =>
132
+ collectPages(
133
+ item.links ??
134
+ item.items ??
135
+ (item.slug !== undefined ? [item as Page] : []),
136
+ ),
137
+ );
123
138
  const currentSlug = pathname.replace(/^\\//, "").replace(/\\/$/, "");
124
139
  const currentIndex = allPages.findIndex((page) => page.slug === currentSlug);
125
140
  const prevPage = currentIndex > 0 ? allPages[currentIndex - 1] : null;
@@ -1 +1 @@
1
- export declare const footerTemplate = "\"use client\";\nimport { useContext } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport { Space, styledSmall } from \"cherry-styled-components\";\nimport { ChatContext } from \"@/components/Chat\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { GitHubLogo } from \"@/components/layout/Pictograms\";\nimport linksData from \"@/links.json\";\n\ninterface LinkProps {\n title: string;\n url: string;\n icon?: string;\n}\n\nconst links = linksData as LinkProps[];\n\nconst StyledFooter = styled.footer<{\n theme: Theme;\n $isChatOpen?: boolean;\n $hasLinks?: boolean;\n}>`\n padding: 0 20px;\n transition: all 0.3s ease;\n\n ${({ $hasLinks }) =>\n $hasLinks &&\n css`\n margin-top: 20px;\n `}\n\n ${mq(\"lg\")} {\n margin: 0;\n padding: 0 300px 0 300px;\n\n ${({ $isChatOpen }) =>\n $isChatOpen &&\n css`\n padding: 0 440px 0 300px;\n `}\n }\n`;\n\nconst StyledFooterInner = styled.div<{ theme: Theme }>`\n border-top: solid 1px ${({ theme }) => theme.colors.grayLight};\n max-width: 640px;\n margin: 0 auto;\n padding: 28px 0;\n color: ${({ theme }) => theme.colors.grayDark};\n ${({ theme }) => styledSmall(theme)};\n\n ${mq(\"lg\")} {\n padding: 20px 0;\n }\n\n & a {\n font-weight: 700;\n color: ${({ theme }) => theme.colors.accent};\n text-decoration: none;\n transition: all 0.3s ease;\n display: inline-flex;\n\n &:hover {\n color: ${({ theme }) => theme.colors.primary};\n }\n\n & svg {\n width: 18px;\n height: 18px;\n }\n }\n`;\n\nconst StyledFooterFlex = styled.div`\n display: flex;\n justify-content: flex-start;\n align-items: center;\n gap: 20px;\n\n ${mq(\"lg\")} {\n justify-content: space-between;\n }\n`;\n\nfunction Footer({ hideBranding }: { hideBranding?: boolean }) {\n const { isOpen } = useContext(ChatContext);\n\n if (hideBranding) return <Space $xs={80} $lg=\"none\" />;\n\n return (\n <StyledFooter $isChatOpen={isOpen} $hasLinks={links.length > 0}>\n <StyledFooterInner>\n <StyledFooterFlex>\n <span>\n Powered by{\" \"}\n <a\n href=\"https://doccupine.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Doccupine\n </a>\n </span>\n <a\n href=\"https://github.com/doccupine/cli\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label=\"Doccupine on GitHub\"\n >\n <GitHubLogo />\n </a>\n </StyledFooterFlex>\n </StyledFooterInner>\n </StyledFooter>\n );\n}\n\nexport { Footer };\n";
1
+ export declare const footerTemplate = "\"use client\";\nimport { useContext } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport { Space, styledSmall } from \"cherry-styled-components\";\nimport { ChatContext } from \"@/components/Chat\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { GitHubLogo } from \"@/components/layout/Pictograms\";\nimport { focusRing } from \"@/components/layout/SharedStyled\";\nimport linksData from \"@/links.json\";\n\ninterface LinkProps {\n title: string;\n url: string;\n icon?: string;\n}\n\nconst links = linksData as LinkProps[];\n\nconst StyledFooter = styled.footer<{\n theme: Theme;\n $isChatOpen?: boolean;\n $hasLinks?: boolean;\n}>`\n padding: 0 20px;\n transition: all 0.3s ease;\n\n ${({ $hasLinks }) =>\n $hasLinks &&\n css`\n margin-top: 20px;\n `}\n\n ${mq(\"lg\")} {\n margin: 0;\n padding: 0 300px 0 300px;\n\n ${({ $isChatOpen }) =>\n $isChatOpen &&\n css`\n padding: 0 440px 0 300px;\n `}\n }\n`;\n\nconst StyledFooterInner = styled.div<{ theme: Theme }>`\n border-top: solid 1px ${({ theme }) => theme.colors.grayLight};\n max-width: 640px;\n margin: 0 auto;\n padding: 28px 0;\n color: ${({ theme }) => theme.colors.grayDark};\n ${({ theme }) => styledSmall(theme)};\n\n ${mq(\"lg\")} {\n padding: 20px 0;\n }\n\n & a {\n font-weight: 700;\n color: ${({ theme }) => theme.colors.accent};\n text-decoration: none;\n transition: all 0.3s ease;\n display: inline-flex;\n ${focusRing};\n\n &:hover {\n color: ${({ theme }) => theme.colors.primary};\n }\n\n /* The GitHub logo is an icon-only link - round its focus ring to match. */\n &:has(svg):focus-visible {\n border-radius: 100%;\n }\n\n & svg {\n width: 18px;\n height: 18px;\n }\n }\n`;\n\nconst StyledFooterFlex = styled.div`\n display: flex;\n justify-content: flex-start;\n align-items: center;\n gap: 20px;\n\n ${mq(\"lg\")} {\n justify-content: space-between;\n }\n`;\n\nfunction Footer({ hideBranding }: { hideBranding?: boolean }) {\n const { isOpen } = useContext(ChatContext);\n\n if (hideBranding) return <Space $xs={80} $lg=\"none\" />;\n\n return (\n <StyledFooter $isChatOpen={isOpen} $hasLinks={links.length > 0}>\n <StyledFooterInner>\n <StyledFooterFlex>\n <span>\n Powered by{\" \"}\n <a\n href=\"https://doccupine.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Doccupine\n </a>\n </span>\n <a\n href=\"https://github.com/doccupine/cli\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label=\"Doccupine on GitHub\"\n >\n <GitHubLogo />\n </a>\n </StyledFooterFlex>\n </StyledFooterInner>\n </StyledFooter>\n );\n}\n\nexport { Footer };\n";
@@ -6,6 +6,7 @@ import { Space, styledSmall } from "cherry-styled-components";
6
6
  import { ChatContext } from "@/components/Chat";
7
7
  import { mq, Theme } from "@/app/theme";
8
8
  import { GitHubLogo } from "@/components/layout/Pictograms";
9
+ import { focusRing } from "@/components/layout/SharedStyled";
9
10
  import linksData from "@/links.json";
10
11
 
11
12
  interface LinkProps {
@@ -60,11 +61,17 @@ const StyledFooterInner = styled.div<{ theme: Theme }>\`
60
61
  text-decoration: none;
61
62
  transition: all 0.3s ease;
62
63
  display: inline-flex;
64
+ \${focusRing};
63
65
 
64
66
  &:hover {
65
67
  color: \${({ theme }) => theme.colors.primary};
66
68
  }
67
69
 
70
+ /* The GitHub logo is an icon-only link - round its focus ring to match. */
71
+ &:has(svg):focus-visible {
72
+ border-radius: 100%;
73
+ }
74
+
68
75
  & svg {
69
76
  width: 18px;
70
77
  height: 18px;
@@ -1 +1 @@
1
- export declare const headerTemplate = "\"use client\";\nimport React from \"react\";\nimport { useCallback, useContext, useRef, useState } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport Link from \"next/link\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { useOnClickOutside } from \"cherry-styled-components\";\nimport { Search } from \"lucide-react\";\nimport { Logo } from \"@/components/layout/Pictograms\";\nimport { ChatContext, ChatButtonCTA } from \"@/components/Chat\";\nimport {\n SearchContext,\n SearchKbd,\n StyledSearchButton,\n} from \"@/components/SearchDocs\";\nimport themeJson from \"@/theme.json\";\n\nconst customThemeJson = themeJson as typeof themeJson & {\n logo?: { dark: string; light: string };\n};\n\nconst StyledHeader = styled.header<{ theme: Theme; $hasChildren: boolean }>`\n position: sticky;\n top: 0;\n margin: 0;\n z-index: 1000;\n width: 100%;\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n\n ${({ $hasChildren }) =>\n !$hasChildren &&\n css`\n ${mq(\"lg\")} {\n padding-bottom: 16px;\n padding-top: 16px;\n }\n `}\n\n &::before,\n &::after {\n display: block;\n content: \"\";\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n background: ${({ theme }) => theme.colors.light};\n z-index: -2;\n }\n\n &::after {\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 5%, transparent)`};\n z-index: -1;\n }\n\n & .logo {\n display: flex;\n\n & svg,\n & img {\n margin: auto;\n height: auto;\n width: fit-content;\n min-width: fit-content;\n max-width: 182px;\n max-height: 30px;\n\n & path[fill] {\n fill: ${({ theme }) => theme.colors.primary};\n }\n }\n }\n`;\n\nconst StyledHeaderInner = styled.div<{ $hasChildren: boolean }>`\n display: flex;\n align-items: center;\n justify-content: space-between;\n flex-wrap: wrap;\n padding: 16px 0 0 20px;\n\n ${({ $hasChildren }) =>\n !$hasChildren &&\n css`\n padding-bottom: 16px;\n `}\n\n ${mq(\"lg\")} {\n flex-wrap: nowrap;\n padding: 0 20px;\n }\n`;\n\nconst StyledLeftWrapper = styled.div`\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: fit-content;\n padding-right: 20px;\n\n ${mq(\"lg\")} {\n padding-right: 0;\n }\n`;\n\ninterface HeaderProps {\n children?: React.ReactNode;\n}\n\nfunction Header({ children }: HeaderProps) {\n const [isOptionActive, setIsOptionActive] = useState(false);\n const [isLangActive, setIsLangActive] = useState(false);\n\n const wrapperRef = useRef<HTMLSpanElement>(null);\n const elmRef = useRef<HTMLDivElement>(null);\n const langRef = useRef<HTMLSpanElement>(null);\n const closeMenu = useCallback(() => {\n setIsOptionActive(false);\n setIsLangActive(false);\n }, []);\n\n useOnClickOutside(\n [elmRef, wrapperRef],\n isOptionActive ? closeMenu : () => {},\n );\n useOnClickOutside([langRef, wrapperRef], isLangActive ? closeMenu : () => {});\n const { isChatActive } = useContext(ChatContext);\n const { openSearch } = useContext(SearchContext);\n\n return (\n <StyledHeader $hasChildren={children ? true : false} id=\"header\">\n <StyledHeaderInner $hasChildren={children ? true : false}>\n <Link href=\"/\" className=\"logo\" aria-label=\"Logo\">\n {customThemeJson.logo ? (\n <>\n {/* Both logos render; .light-only and .dark-only classes in\n GlobalStyles hide the inactive one based on the \"dark\" class\n on <html>. Avoids a JS-driven swap so no flash on first load. */}\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n className=\"light-only\"\n src={customThemeJson.logo.light}\n alt=\"Logo\"\n width=\"100\"\n height=\"100\"\n />\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n className=\"dark-only\"\n src={customThemeJson.logo.dark}\n alt=\"Logo\"\n width=\"100\"\n height=\"100\"\n />\n </>\n ) : (\n <Logo />\n )}\n </Link>\n {children}\n <StyledLeftWrapper>\n <StyledSearchButton onClick={openSearch} aria-label=\"Search docs\">\n <Search size={14} />\n <SearchKbd>&#8984;K</SearchKbd>\n </StyledSearchButton>\n {isChatActive && <ChatButtonCTA />}\n </StyledLeftWrapper>\n </StyledHeaderInner>\n </StyledHeader>\n );\n}\n\nexport { Header };\n";
1
+ export declare const headerTemplate = "\"use client\";\nimport React from \"react\";\nimport { useCallback, useContext, useRef, useState } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport Link from \"next/link\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { focusRing } from \"@/components/layout/SharedStyled\";\nimport { useOnClickOutside } from \"cherry-styled-components\";\nimport { Search } from \"lucide-react\";\nimport { Logo } from \"@/components/layout/Pictograms\";\nimport { ChatContext, ChatButtonCTA } from \"@/components/Chat\";\nimport {\n SearchContext,\n SearchKbd,\n StyledSearchButton,\n} from \"@/components/SearchDocs\";\nimport themeJson from \"@/theme.json\";\n\nconst customThemeJson = themeJson as typeof themeJson & {\n logo?: { dark: string; light: string };\n};\n\nconst StyledHeader = styled.header<{ theme: Theme; $hasChildren: boolean }>`\n position: sticky;\n top: 0;\n margin: 0;\n z-index: 1000;\n width: 100%;\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n\n ${({ $hasChildren }) =>\n !$hasChildren &&\n css`\n ${mq(\"lg\")} {\n padding-bottom: 16px;\n padding-top: 16px;\n }\n `}\n\n &::before,\n &::after {\n display: block;\n content: \"\";\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n background: ${({ theme }) => theme.colors.light};\n z-index: -2;\n }\n\n &::after {\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 5%, transparent)`};\n z-index: -1;\n }\n\n & .logo {\n display: flex;\n min-width: max-content;\n ${focusRing};\n\n & svg,\n & img {\n margin: auto;\n height: auto;\n width: fit-content;\n min-width: fit-content;\n max-width: 182px;\n max-height: 30px;\n\n & path[fill] {\n fill: ${({ theme }) => theme.colors.primary};\n }\n }\n }\n`;\n\nconst StyledHeaderInner = styled.div<{ $hasChildren: boolean }>`\n display: flex;\n align-items: center;\n justify-content: space-between;\n flex-wrap: wrap;\n padding: 16px 0 0 20px;\n\n ${({ $hasChildren }) =>\n !$hasChildren &&\n css`\n padding-bottom: 16px;\n `}\n\n ${mq(\"lg\")} {\n flex-wrap: nowrap;\n padding: 0 20px;\n }\n`;\n\nconst StyledLeftWrapper = styled.div`\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: fit-content;\n padding-right: 20px;\n\n ${mq(\"lg\")} {\n padding-right: 0;\n }\n`;\n\ninterface HeaderProps {\n children?: React.ReactNode;\n}\n\nfunction Header({ children }: HeaderProps) {\n const [isOptionActive, setIsOptionActive] = useState(false);\n const [isLangActive, setIsLangActive] = useState(false);\n\n const wrapperRef = useRef<HTMLSpanElement>(null);\n const elmRef = useRef<HTMLDivElement>(null);\n const langRef = useRef<HTMLSpanElement>(null);\n const closeMenu = useCallback(() => {\n setIsOptionActive(false);\n setIsLangActive(false);\n }, []);\n\n useOnClickOutside(\n [elmRef, wrapperRef],\n isOptionActive ? closeMenu : () => {},\n );\n useOnClickOutside([langRef, wrapperRef], isLangActive ? closeMenu : () => {});\n const { isChatActive } = useContext(ChatContext);\n const { openSearch } = useContext(SearchContext);\n\n return (\n <StyledHeader $hasChildren={children ? true : false} id=\"header\">\n <StyledHeaderInner $hasChildren={children ? true : false}>\n <Link href=\"/\" className=\"logo\" aria-label=\"Logo\">\n {customThemeJson.logo ? (\n <>\n {/* Both logos render; .light-only and .dark-only classes in\n GlobalStyles hide the inactive one based on the \"dark\" class\n on <html>. Avoids a JS-driven swap so no flash on first load. */}\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n className=\"light-only\"\n src={customThemeJson.logo.light}\n alt=\"Logo\"\n width=\"100\"\n height=\"100\"\n />\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n className=\"dark-only\"\n src={customThemeJson.logo.dark}\n alt=\"Logo\"\n width=\"100\"\n height=\"100\"\n />\n </>\n ) : (\n <Logo />\n )}\n </Link>\n {children}\n <StyledLeftWrapper>\n <StyledSearchButton onClick={openSearch} aria-label=\"Search docs\">\n <Search size={14} />\n <SearchKbd>&#8984;K</SearchKbd>\n </StyledSearchButton>\n {isChatActive && <ChatButtonCTA />}\n </StyledLeftWrapper>\n </StyledHeaderInner>\n </StyledHeader>\n );\n}\n\nexport { Header };\n";
@@ -4,6 +4,7 @@ import { useCallback, useContext, useRef, useState } from "react";
4
4
  import styled, { css } from "styled-components";
5
5
  import Link from "next/link";
6
6
  import { mq, Theme } from "@/app/theme";
7
+ import { focusRing } from "@/components/layout/SharedStyled";
7
8
  import { useOnClickOutside } from "cherry-styled-components";
8
9
  import { Search } from "lucide-react";
9
10
  import { Logo } from "@/components/layout/Pictograms";
@@ -58,6 +59,8 @@ const StyledHeader = styled.header<{ theme: Theme; $hasChildren: boolean }>\`
58
59
 
59
60
  & .logo {
60
61
  display: flex;
62
+ min-width: max-content;
63
+ \${focusRing};
61
64
 
62
65
  & svg,
63
66
  & img {
@@ -0,0 +1 @@
1
+ export declare const notFoundComponentTemplate = "\"use client\";\nimport styled from \"styled-components\";\nimport { Compass } from \"lucide-react\";\nimport { Theme } from \"@/app/theme\";\nimport { Button } from \"@/components/layout/Button\";\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 StyledCard = styled.div<{ theme: Theme }>`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n width: 100%;\n max-width: 420px;\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: 24px;\n font-weight: 700;\n color: ${({ theme }) => theme.colors.accent};\n`;\n\nconst StyledText = styled.p<{ theme: Theme }>`\n margin: 0;\n font-size: 15px;\n color: ${({ theme }) => theme.colors.grayDark};\n`;\n\nexport function NotFound() {\n return (\n <StyledWrapper>\n <StyledCard>\n <StyledIcon>\n <Compass />\n </StyledIcon>\n <StyledTitle>Error 404</StyledTitle>\n <StyledText>This page could not be found.</StyledText>\n <Button href=\"/\" icon=\"house\">\n Home\n </Button>\n </StyledCard>\n </StyledWrapper>\n );\n}\n";
@@ -0,0 +1,76 @@
1
+ export const notFoundComponentTemplate = `"use client";
2
+ import styled from "styled-components";
3
+ import { Compass } from "lucide-react";
4
+ import { Theme } from "@/app/theme";
5
+ import { Button } from "@/components/layout/Button";
6
+
7
+ const StyledWrapper = styled.div<{ theme: Theme }>\`
8
+ min-height: 100dvh;
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ padding: 20px;
13
+ background: \${({ theme }) => theme.colors.light};
14
+ \`;
15
+
16
+ const StyledCard = styled.div<{ theme: Theme }>\`
17
+ display: flex;
18
+ flex-direction: column;
19
+ align-items: center;
20
+ gap: 20px;
21
+ width: 100%;
22
+ max-width: 420px;
23
+ padding: 40px 32px;
24
+ background: \${({ theme }) => theme.colors.light};
25
+ border: solid 1px \${({ theme }) => theme.colors.grayLight};
26
+ border-radius: \${({ theme }) => theme.spacing.radius.lg};
27
+ box-shadow: \${({ theme }) => theme.shadows.sm};
28
+ text-align: center;
29
+ \`;
30
+
31
+ const StyledIcon = styled.div<{ theme: Theme }>\`
32
+ display: inline-flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ width: 48px;
36
+ height: 48px;
37
+ border-radius: 50%;
38
+ color: \${({ theme }) => theme.colors.accent};
39
+ background: \${({ theme }) => theme.colors.grayLight};
40
+
41
+ & svg {
42
+ width: 22px;
43
+ height: 22px;
44
+ }
45
+ \`;
46
+
47
+ const StyledTitle = styled.h1<{ theme: Theme }>\`
48
+ margin: 0;
49
+ font-size: 24px;
50
+ font-weight: 700;
51
+ color: \${({ theme }) => theme.colors.accent};
52
+ \`;
53
+
54
+ const StyledText = styled.p<{ theme: Theme }>\`
55
+ margin: 0;
56
+ font-size: 15px;
57
+ color: \${({ theme }) => theme.colors.grayDark};
58
+ \`;
59
+
60
+ export function NotFound() {
61
+ return (
62
+ <StyledWrapper>
63
+ <StyledCard>
64
+ <StyledIcon>
65
+ <Compass />
66
+ </StyledIcon>
67
+ <StyledTitle>Error 404</StyledTitle>
68
+ <StyledText>This page could not be found.</StyledText>
69
+ <Button href="/" icon="house">
70
+ Home
71
+ </Button>
72
+ </StyledCard>
73
+ </StyledWrapper>
74
+ );
75
+ }
76
+ `;
@@ -1 +1 @@
1
- export declare const sectionBarTemplate = "\"use client\";\nimport { usePathname } from \"next/navigation\";\nimport Link from \"next/link\";\nimport styled from \"styled-components\";\nimport { styledText } from \"cherry-styled-components\";\nimport { mq, Theme } from \"@/app/theme\";\n\ninterface SectionConfig {\n label: string;\n slug: string;\n directory?: string;\n}\n\ninterface SectionBarProps {\n sections: SectionConfig[];\n}\n\nconst StyledSectionBar = styled.nav<{ theme: Theme }>`\n display: flex;\n order: 3;\n width: calc(100% + 20px);\n margin: 0 0 0 -10px;\n padding: 0;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n position: relative;\n\n &::-webkit-scrollbar {\n display: none;\n }\n\n ${mq(\"lg\")} {\n padding: 0 10px;\n order: unset;\n width: 100%;\n margin: 0;\n justify-content: flex-end;\n }\n`;\n\nconst StyledSectionLink = styled(Link)<{\n theme: Theme;\n $isActive: boolean;\n}>`\n ${({ theme }) => styledText(theme)};\n text-decoration: none;\n padding: 16px 10px;\n white-space: nowrap;\n font-weight: ${({ $isActive }) => ($isActive ? \"600\" : \"500\")};\n color: ${({ theme, $isActive }) =>\n $isActive ? theme.colors.primary : theme.colors.gray};\n border-bottom: solid 2px\n ${({ theme, $isActive }) =>\n $isActive ? theme.colors.primary : \"transparent\"};\n transition: all 0.3s ease;\n min-width: fit-content;\n\n &:hover {\n color: ${({ theme }) => theme.colors.primary};\n }\n`;\n\nfunction SectionBar({ sections }: SectionBarProps) {\n const pathname = usePathname();\n const currentPath = pathname.replace(/^\\//, \"\").replace(/\\/$/, \"\");\n\n const activeSection = sections.find((section) => {\n if (section.slug === \"\") return false;\n return (\n currentPath === section.slug || currentPath.startsWith(section.slug + \"/\")\n );\n });\n\n const activeSectionSlug = activeSection ? activeSection.slug : \"\";\n\n return (\n <StyledSectionBar>\n {sections.map((section) => (\n <StyledSectionLink\n key={section.slug}\n href={section.slug === \"\" ? \"/\" : `/${section.slug}`}\n $isActive={activeSectionSlug === section.slug}\n >\n {section.label}\n </StyledSectionLink>\n ))}\n </StyledSectionBar>\n );\n}\n\nexport { SectionBar };\n";
1
+ export declare const sectionBarTemplate = "\"use client\";\nimport { usePathname } from \"next/navigation\";\nimport Link from \"next/link\";\nimport styled from \"styled-components\";\nimport { styledText } from \"cherry-styled-components\";\nimport { mq, Theme } from \"@/app/theme\";\n\ninterface SectionConfig {\n label: string;\n slug: string;\n directory?: string;\n}\n\ninterface SectionBarProps {\n sections: SectionConfig[];\n}\n\nconst StyledSectionBar = styled.nav<{ theme: Theme }>`\n display: flex;\n order: 3;\n width: calc(100% + 20px);\n margin: 0 0 0 -10px;\n padding: 0;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n position: relative;\n\n &::-webkit-scrollbar {\n display: none;\n }\n\n ${mq(\"lg\")} {\n padding: 0 10px;\n order: unset;\n width: 100%;\n margin: 0;\n justify-content: flex-end;\n }\n`;\n\nconst StyledSectionLink = styled(Link)<{\n theme: Theme;\n $isActive: boolean;\n}>`\n ${({ theme }) => styledText(theme)};\n text-decoration: none;\n padding: 16px 10px;\n white-space: nowrap;\n font-weight: ${({ $isActive }) => ($isActive ? \"600\" : \"500\")};\n color: ${({ theme, $isActive }) =>\n $isActive ? theme.colors.primary : theme.colors.gray};\n border-bottom: solid 2px\n ${({ theme, $isActive }) =>\n $isActive ? theme.colors.primary : \"transparent\"};\n transition: all 0.3s ease;\n min-width: fit-content;\n\n /* Inset ring (not the shared outset focusRing): the scrolling section bar\n clips overflow, so an outset glow gets cropped top/bottom. Drawing it inside\n the link's padding keeps the primaryLight focus look fully visible. */\n &:focus-visible {\n outline: none;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n box-shadow: inset 0 0 0 4px ${({ theme }) => theme.colors.primaryLight};\n }\n\n &:hover {\n color: ${({ theme }) => theme.colors.primary};\n }\n`;\n\nfunction SectionBar({ sections }: SectionBarProps) {\n const pathname = usePathname();\n const currentPath = pathname.replace(/^\\//, \"\").replace(/\\/$/, \"\");\n\n const activeSection = sections.find((section) => {\n if (section.slug === \"\") return false;\n return (\n currentPath === section.slug || currentPath.startsWith(section.slug + \"/\")\n );\n });\n\n const activeSectionSlug = activeSection ? activeSection.slug : \"\";\n\n return (\n <StyledSectionBar>\n {sections.map((section) => (\n <StyledSectionLink\n key={section.slug}\n href={section.slug === \"\" ? \"/\" : `/${section.slug}`}\n $isActive={activeSectionSlug === section.slug}\n >\n {section.label}\n </StyledSectionLink>\n ))}\n </StyledSectionBar>\n );\n}\n\nexport { SectionBar };\n";
@@ -55,6 +55,15 @@ const StyledSectionLink = styled(Link)<{
55
55
  transition: all 0.3s ease;
56
56
  min-width: fit-content;
57
57
 
58
+ /* Inset ring (not the shared outset focusRing): the scrolling section bar
59
+ clips overflow, so an outset glow gets cropped top/bottom. Drawing it inside
60
+ the link's padding keeps the primaryLight focus look fully visible. */
61
+ &:focus-visible {
62
+ outline: none;
63
+ border-radius: \${({ theme }) => theme.spacing.radius.xs};
64
+ box-shadow: inset 0 0 0 4px \${({ theme }) => theme.colors.primaryLight};
65
+ }
66
+
58
67
  &:hover {
59
68
  color: \${({ theme }) => theme.colors.primary};
60
69
  }
@@ -1 +1 @@
1
- export declare const sharedStyledTemplate = "\"use client\";\nimport { styledSmall, styledText } from \"cherry-styled-components\";\nimport { mq, Theme } from \"@/app/theme\";\nimport styled, { css } from \"styled-components\";\n\n/** Slim, theme-aware scrollbar for internal scroll areas (code blocks,\n modals, tables) so the chunky native bar doesn't stand out, especially\n in dark mode. */\nexport const thinScrollbar = css<{ theme: Theme }>`\n scrollbar-width: thin;\n scrollbar-color: ${({ theme }) => theme.colors.grayLight} transparent;\n`;\n\nexport const interactiveStyles = css<{ theme: Theme }>`\n transition: all 0.3s ease;\n border: solid 1px transparent;\n box-shadow: 0 0 0 0px ${({ theme }) => theme.colors.primary};\n\n &:hover {\n border-color: ${({ theme }) => theme.colors.primary};\n }\n\n &:focus {\n border-color: ${({ theme }) => theme.colors.primary};\n box-shadow: 0 0 0 4px ${({ theme }) => theme.colors.primaryLight};\n }\n\n &:active {\n box-shadow: 0 0 0 2px ${({ theme }) => theme.colors.primaryLight};\n }\n`;\n\nexport const styledAnchor = css<{ theme: Theme }>`\n & a:not([class]):not(:has(img)) {\n color: inherit;\n transition: all 0.3s ease;\n text-decoration: none;\n box-shadow: 0 2px 0 0 ${({ theme }) => theme.colors.primary};\n\n &:hover {\n color: ${({ theme }) => theme.colors.accent};\n box-shadow: 0 1px 0 0 ${({ theme }) => theme.colors.primary};\n }\n }\n`;\n\nexport const stylesLists = css<{ theme: Theme }>`\n & ul,\n & ol {\n & li {\n & > .code-wrapper {\n margin: 10px 0;\n }\n }\n }\n\n & ul {\n list-style: none;\n padding: 0;\n margin: 0;\n\n & li {\n text-indent: 0;\n display: block;\n position: relative;\n padding: 0 0 0 15px;\n margin: 0;\n ${({ theme }) => styledText(theme)};\n min-height: 23px;\n\n ${mq(\"lg\")} {\n min-height: 27px;\n }\n\n &::before {\n content: \"\";\n display: block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: ${({ theme }) => theme.colors.primary};\n position: absolute;\n top: 8px;\n left: 2px;\n\n ${mq(\"lg\")} {\n top: 10px;\n }\n }\n }\n }\n\n & ol {\n padding: 0;\n margin: 0;\n\n & ul {\n padding-left: 15px;\n }\n\n & > li {\n position: relative;\n padding: 0;\n counter-increment: item;\n margin: 0;\n ${({ theme }) => styledText(theme)};\n\n &::before {\n content: counter(item) \".\";\n display: inline-block;\n margin: 0 4px 0 0;\n font-weight: 700;\n color: ${({ theme }) => theme.colors.primary};\n min-width: max-content;\n }\n }\n }\n`;\n\nexport const styledTable = css<{ theme: Theme }>`\n & .table-wrapper {\n overflow-x: auto;\n width: 100%;\n ${thinScrollbar};\n }\n\n & table {\n margin: 0;\n padding: 0;\n border-collapse: collapse;\n width: 100%;\n text-align: left;\n\n & tr {\n margin: 0;\n padding: 0;\n }\n\n & th {\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n padding: 10px 10px 10px 0;\n ${({ theme }) => styledSmall(theme)};\n font-weight: 600;\n color: ${({ theme }) => theme.colors.dark};\n }\n\n & td {\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n padding: 10px 10px 10px 0;\n color: ${({ theme }) => theme.colors.grayDark};\n ${({ theme }) => styledSmall(theme)};\n }\n }\n`;\n\nexport const StyledSmallButton = styled.button<{ theme: Theme }>`\n ${interactiveStyles};\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n color: ${({ theme }) => theme.colors.accent};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n padding: 6px 8px;\n font-size: 12px;\n font-family: inherit;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s ease;\n display: flex;\n align-items: center;\n gap: 6px;\n margin-right: -6px;\n\n & svg.lucide {\n color: inherit;\n }\n`;\n";
1
+ export declare const sharedStyledTemplate = "\"use client\";\nimport { styledSmall, styledText } from \"cherry-styled-components\";\nimport { mq, Theme } from \"@/app/theme\";\nimport styled, { css } from \"styled-components\";\n\n/** Slim, theme-aware scrollbar for internal scroll areas (code blocks,\n modals, tables) so the chunky native bar doesn't stand out, especially\n in dark mode. */\nexport const thinScrollbar = css<{ theme: Theme }>`\n scrollbar-width: thin;\n scrollbar-color: ${({ theme }) => theme.colors.grayLight} transparent;\n`;\n\nexport const interactiveStyles = css<{ theme: Theme }>`\n transition: all 0.3s ease;\n border: solid 1px transparent;\n box-shadow: 0 0 0 0px ${({ theme }) => theme.colors.primary};\n\n &:hover {\n border-color: ${({ theme }) => theme.colors.primary};\n }\n\n &:focus {\n border-color: ${({ theme }) => theme.colors.primary};\n box-shadow: 0 0 0 4px ${({ theme }) => theme.colors.primaryLight};\n }\n\n &:active {\n box-shadow: 0 0 0 2px ${({ theme }) => theme.colors.primaryLight};\n }\n`;\n\n/** Keyboard-focus ring for bare navigation links (header links, sidebar rows,\n footer links) that don't carry the full interactiveStyles treatment. Mirrors\n the primaryLight glow from interactiveStyles, but uses :focus-visible so the\n ring only shows for keyboard users - not on mouse click. Pair with an element\n that already has a transition for a smooth fade. The small border-radius\n rounds the ring, so don't apply this to elements that set their own radius. */\nexport const focusRing = css<{ theme: Theme }>`\n &:focus-visible {\n outline: none;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n box-shadow: 0 0 0 4px ${({ theme }) => theme.colors.primaryLight};\n }\n`;\n\nexport const styledAnchor = css<{ theme: Theme }>`\n & a:not([class]):not(:has(img)) {\n color: inherit;\n transition: all 0.3s ease;\n text-decoration: none;\n box-shadow: 0 2px 0 0 ${({ theme }) => theme.colors.primary};\n\n &:hover {\n color: ${({ theme }) => theme.colors.accent};\n box-shadow: 0 1px 0 0 ${({ theme }) => theme.colors.primary};\n }\n\n /* Same primaryLight focus glow as focusRing, but layered over the link's\n own box-shadow underline (same property) so the underline is preserved. */\n &:focus-visible {\n outline: none;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n box-shadow:\n 0 2px 0 0 ${({ theme }) => theme.colors.primary},\n 0 0 0 4px ${({ theme }) => theme.colors.primaryLight};\n }\n }\n`;\n\nexport const stylesLists = css<{ theme: Theme }>`\n & ul,\n & ol {\n & li {\n & > .code-wrapper {\n margin: 10px 0;\n }\n }\n }\n\n & ul {\n list-style: none;\n padding: 0;\n margin: 0;\n\n & li {\n text-indent: 0;\n display: block;\n position: relative;\n padding: 0 0 0 15px;\n margin: 0;\n ${({ theme }) => styledText(theme)};\n min-height: 23px;\n\n ${mq(\"lg\")} {\n min-height: 27px;\n }\n\n &::before {\n content: \"\";\n display: block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: ${({ theme }) => theme.colors.primary};\n position: absolute;\n top: 8px;\n left: 2px;\n\n ${mq(\"lg\")} {\n top: 10px;\n }\n }\n }\n }\n\n & ol {\n padding: 0;\n margin: 0;\n\n & ul {\n padding-left: 15px;\n }\n\n & > li {\n position: relative;\n padding: 0;\n counter-increment: item;\n margin: 0;\n ${({ theme }) => styledText(theme)};\n\n &::before {\n content: counter(item) \".\";\n display: inline-block;\n margin: 0 4px 0 0;\n font-weight: 700;\n color: ${({ theme }) => theme.colors.primary};\n min-width: max-content;\n }\n }\n }\n`;\n\nexport const styledTable = css<{ theme: Theme }>`\n & .table-wrapper {\n overflow-x: auto;\n width: 100%;\n ${thinScrollbar};\n }\n\n & table {\n margin: 0;\n padding: 0;\n border-collapse: collapse;\n width: 100%;\n text-align: left;\n\n & tr {\n margin: 0;\n padding: 0;\n }\n\n & th {\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n padding: 10px 10px 10px 0;\n ${({ theme }) => styledSmall(theme)};\n font-weight: 600;\n color: ${({ theme }) => theme.colors.dark};\n }\n\n & td {\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n padding: 10px 10px 10px 0;\n color: ${({ theme }) => theme.colors.grayDark};\n ${({ theme }) => styledSmall(theme)};\n }\n }\n`;\n\nexport const StyledSmallButton = styled.button<{ theme: Theme }>`\n ${interactiveStyles};\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n color: ${({ theme }) => theme.colors.accent};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n padding: 6px 8px;\n font-size: 12px;\n font-family: inherit;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s ease;\n display: flex;\n align-items: center;\n gap: 6px;\n margin-right: -6px;\n\n & svg.lucide {\n color: inherit;\n }\n`;\n";
@@ -30,6 +30,20 @@ export const interactiveStyles = css<{ theme: Theme }>\`
30
30
  }
31
31
  \`;
32
32
 
33
+ /** Keyboard-focus ring for bare navigation links (header links, sidebar rows,
34
+ footer links) that don't carry the full interactiveStyles treatment. Mirrors
35
+ the primaryLight glow from interactiveStyles, but uses :focus-visible so the
36
+ ring only shows for keyboard users - not on mouse click. Pair with an element
37
+ that already has a transition for a smooth fade. The small border-radius
38
+ rounds the ring, so don't apply this to elements that set their own radius. */
39
+ export const focusRing = css<{ theme: Theme }>\`
40
+ &:focus-visible {
41
+ outline: none;
42
+ border-radius: \${({ theme }) => theme.spacing.radius.xs};
43
+ box-shadow: 0 0 0 4px \${({ theme }) => theme.colors.primaryLight};
44
+ }
45
+ \`;
46
+
33
47
  export const styledAnchor = css<{ theme: Theme }>\`
34
48
  & a:not([class]):not(:has(img)) {
35
49
  color: inherit;
@@ -41,6 +55,16 @@ export const styledAnchor = css<{ theme: Theme }>\`
41
55
  color: \${({ theme }) => theme.colors.accent};
42
56
  box-shadow: 0 1px 0 0 \${({ theme }) => theme.colors.primary};
43
57
  }
58
+
59
+ /* Same primaryLight focus glow as focusRing, but layered over the link's
60
+ own box-shadow underline (same property) so the underline is preserved. */
61
+ &:focus-visible {
62
+ outline: none;
63
+ border-radius: \${({ theme }) => theme.spacing.radius.xs};
64
+ box-shadow:
65
+ 0 2px 0 0 \${({ theme }) => theme.colors.primary},
66
+ 0 0 0 4px \${({ theme }) => theme.colors.primaryLight};
67
+ }
44
68
  }
45
69
  \`;
46
70
 
@@ -0,0 +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";
@@ -0,0 +1,32 @@
1
+ export const slugTemplate = `// Shared heading/anchor id helpers.
2
+ //
3
+ // \`slugify\` turns heading text into a URL-safe base slug. \`createSlugger\`
4
+ // wraps it with occurrence-based de-duplication so repeated heading text
5
+ // yields stable, unique ids ("setup", "setup-1", "setup-2", ...) matching
6
+ // the scheme used by GitHub and rehype-slug.
7
+ //
8
+ // The index sidebar (components/Docs.tsx) and the rendered headings
9
+ // (components/MDXComponents.tsx, components/layout/Update.tsx) must walk the
10
+ // document in the same order with a shared slugger so every sidebar link
11
+ // resolves to exactly one heading.
12
+
13
+ export function slugify(text: string): string {
14
+ return text
15
+ .toLowerCase()
16
+ .replace(/[^\\w\\s-]/g, "")
17
+ .replace(/\\s+/g, "-")
18
+ .trim();
19
+ }
20
+
21
+ export type Slugger = (text: string) => string;
22
+
23
+ export function createSlugger(): Slugger {
24
+ const seen = new Map<string, number>();
25
+ return (text: string) => {
26
+ const base = slugify(text);
27
+ const count = seen.get(base) ?? 0;
28
+ seen.set(base, count + 1);
29
+ return count === 0 ? base : \`\${base}-\${count}\`;
30
+ };
31
+ }
32
+ `;
@@ -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.h3<{ 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 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 <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";
@@ -54,11 +54,12 @@ const StepNumber = styled.div<{ theme: Theme }>\`
54
54
  z-index: 1;
55
55
  \`;
56
56
 
57
- const StyledStepTitle = styled.h3<{ theme: Theme }>\`
57
+ const StyledStepTitle = styled.div<{ theme: Theme }>\`
58
58
  margin: 0 0 10px 0;
59
59
  padding: 2px 0 0 0;
60
60
  color: \${({ theme }) => theme.colors.dark};
61
61
  \${({ theme }) => styledText(theme)};
62
+ font-weight: 700;
62
63
  display: flex;
63
64
  align-items: center;
64
65
  gap: 10px;
@@ -1 +1 @@
1
- export declare const tabsTemplate = "\"use client\";\nimport { Theme } from \"@/app/theme\";\nimport { styledText } from \"cherry-styled-components\";\nimport React, { useState, ReactNode } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport { thinScrollbar } from \"@/components/layout/SharedStyled\";\ninterface TabContentProps {\n title: string;\n children: ReactNode;\n}\ninterface TabsProps {\n children: React.ReactElement<TabContentProps>[];\n}\nconst TabsContainer = styled.div`\n width: 100%;\n margin: 0 auto;\n`;\nconst TabsList = styled.div<{ theme: Theme }>`\n display: flex;\n overflow: hidden;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n background-color: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n overflow-x: auto;\n ${thinScrollbar};\n`;\nconst TabButton = styled.button<{ theme: Theme; $isActive?: boolean }>`\n flex: 1;\n padding: 12px 20px;\n border: none;\n background: ${({ theme }) => theme.colors.light};\n cursor: pointer;\n transition: all 0.2s ease;\n border-bottom: 3px solid transparent;\n min-width: fit-content;\n ${({ theme }) => styledText(theme)};\n color: ${({ theme }) => theme.colors.dark};\n font-weight: 600;\n ${({ theme, $isActive }) =>\n $isActive &&\n css`\n color: ${theme.colors.primary};\n border-bottom: 3px solid ${theme.colors.primary};\n `}\n position: relative;\n &:hover {\n ${({ theme, $isActive }) =>\n !$isActive &&\n css`\n color: ${theme.colors.primary};\n background-color: color-mix(\n in srgb,\n ${theme.colors.primaryLight} 10%,\n transparent\n );\n `}\n }\n &:focus {\n outline: none;\n }\n &:not(:last-child) {\n border-right: 1px solid ${({ theme }) => theme.colors.grayLight};\n }\n`;\nconst TabPanel = styled.div<{ theme: Theme }>`\n background-color: ${({ theme }) => theme.colors.light};\n padding: 20px;\n border-radius: 0 0 ${({ theme }) => theme.spacing.radius.lg}\n ${({ theme }) => theme.spacing.radius.lg};\n color: ${({ theme }) => theme.colors.grayDark};\n ${({ theme }) => styledText(theme)}\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-top: none;\n display: flex;\n flex-direction: column;\n gap: 20px;\n flex-wrap: wrap;\n flex: 1;\n`;\nconst TabContent: React.FC<TabContentProps> = ({ children }) => {\n return <>{children}</>;\n};\nconst Tabs: React.FC<TabsProps> = ({ children }) => {\n const [activeTab, setActiveTab] = useState(0);\n const tabs = React.Children.toArray(children).filter(\n (child): child is React.ReactElement<TabContentProps> =>\n Boolean(\n React.isValidElement(child) &&\n child.props &&\n typeof child.props === \"object\" &&\n \"title\" in child.props &&\n typeof child.props.title === \"string\" &&\n child.props.title.trim() !== \"\",\n ),\n );\n return (\n <TabsContainer>\n <TabsList>\n {tabs.map((tab, index) => (\n <TabButton\n key={index}\n $isActive={activeTab === index}\n onClick={() => setActiveTab(index)}\n type=\"button\"\n >\n {tab.props.title}\n </TabButton>\n ))}\n </TabsList>\n <TabPanel>{tabs[activeTab]?.props.children}</TabPanel>\n </TabsContainer>\n );\n};\nexport { Tabs, TabContent };\n";
1
+ export declare const tabsTemplate = "\"use client\";\nimport { Theme } from \"@/app/theme\";\nimport { styledText } from \"cherry-styled-components\";\nimport React, { useState, ReactNode } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport { thinScrollbar } from \"@/components/layout/SharedStyled\";\nimport { Icon } from \"@/components/layout/Icon\";\ninterface TabContentProps {\n title: string;\n icon?: string;\n children: ReactNode;\n}\ninterface TabsProps {\n children: React.ReactElement<TabContentProps>[];\n}\nconst TabsContainer = styled.div`\n width: 100%;\n margin: 0 auto;\n`;\nconst TabsList = styled.div<{ theme: Theme }>`\n display: flex;\n overflow: hidden;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n background-color: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n overflow-x: auto;\n ${thinScrollbar};\n`;\nconst TabButton = styled.button<{ theme: Theme; $isActive?: boolean }>`\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n padding: 12px 20px;\n border: none;\n background: ${({ theme }) => theme.colors.light};\n cursor: pointer;\n transition: all 0.2s ease;\n border-bottom: 3px solid transparent;\n min-width: fit-content;\n ${({ theme }) => styledText(theme)};\n color: ${({ theme }) => theme.colors.dark};\n font-weight: 600;\n\n & svg {\n flex-shrink: 0;\n }\n ${({ theme, $isActive }) =>\n $isActive &&\n css`\n color: ${theme.colors.primary};\n border-bottom: 3px solid ${theme.colors.primary};\n `}\n position: relative;\n &:hover {\n ${({ theme, $isActive }) =>\n !$isActive &&\n css`\n color: ${theme.colors.primary};\n background-color: color-mix(\n in srgb,\n ${theme.colors.primaryLight} 10%,\n transparent\n );\n `}\n }\n &:focus {\n outline: none;\n }\n &:not(:last-child) {\n border-right: 1px solid ${({ theme }) => theme.colors.grayLight};\n }\n`;\nconst TabPanel = styled.div<{ theme: Theme }>`\n background-color: ${({ theme }) => theme.colors.light};\n padding: 20px;\n border-radius: 0 0 ${({ theme }) => theme.spacing.radius.lg}\n ${({ theme }) => theme.spacing.radius.lg};\n color: ${({ theme }) => theme.colors.grayDark};\n ${({ theme }) => styledText(theme)}\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-top: none;\n display: flex;\n flex-direction: column;\n gap: 20px;\n flex-wrap: wrap;\n flex: 1;\n`;\nconst TabContent: React.FC<TabContentProps> = ({ children }) => {\n return <>{children}</>;\n};\nconst Tabs: React.FC<TabsProps> = ({ children }) => {\n const [activeTab, setActiveTab] = useState(0);\n const tabs = React.Children.toArray(children).filter(\n (child): child is React.ReactElement<TabContentProps> =>\n Boolean(\n React.isValidElement(child) &&\n child.props &&\n typeof child.props === \"object\" &&\n \"title\" in child.props &&\n typeof child.props.title === \"string\" &&\n child.props.title.trim() !== \"\",\n ),\n );\n return (\n <TabsContainer>\n <TabsList>\n {tabs.map((tab, index) => (\n <TabButton\n key={index}\n $isActive={activeTab === index}\n onClick={() => setActiveTab(index)}\n type=\"button\"\n >\n {tab.props.icon && <Icon name={tab.props.icon} size={18} />}\n {tab.props.title}\n </TabButton>\n ))}\n </TabsList>\n <TabPanel>{tabs[activeTab]?.props.children}</TabPanel>\n </TabsContainer>\n );\n};\nexport { Tabs, TabContent };\n";