@raystack/chronicle 0.5.3 → 0.6.0

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 (74) hide show
  1. package/dist/cli/index.js +260 -81
  2. package/package.json +8 -6
  3. package/src/cli/commands/build.ts +5 -8
  4. package/src/cli/commands/dev.ts +5 -6
  5. package/src/cli/commands/init.test.ts +77 -0
  6. package/src/cli/commands/init.ts +73 -40
  7. package/src/cli/commands/serve.ts +6 -9
  8. package/src/cli/commands/start.ts +5 -5
  9. package/src/cli/utils/config.ts +6 -12
  10. package/src/cli/utils/scaffold.test.ts +179 -0
  11. package/src/cli/utils/scaffold.ts +70 -9
  12. package/src/components/api/field-row.tsx +1 -1
  13. package/src/components/api/field-section.tsx +2 -2
  14. package/src/components/mdx/index.tsx +1 -1
  15. package/src/components/mdx/mermaid.tsx +24 -21
  16. package/src/components/ui/breadcrumbs.tsx +4 -2
  17. package/src/components/ui/client-theme-switcher.tsx +21 -4
  18. package/src/components/ui/search.module.css +16 -41
  19. package/src/components/ui/search.tsx +30 -41
  20. package/src/lib/config.test.ts +493 -0
  21. package/src/lib/config.ts +123 -22
  22. package/src/lib/head.tsx +23 -5
  23. package/src/lib/llms.test.ts +94 -0
  24. package/src/lib/llms.ts +41 -0
  25. package/src/lib/navigation.test.ts +94 -0
  26. package/src/lib/navigation.ts +51 -0
  27. package/src/lib/page-context.tsx +79 -32
  28. package/src/lib/route-resolver.test.ts +173 -0
  29. package/src/lib/route-resolver.ts +73 -0
  30. package/src/lib/source.ts +94 -1
  31. package/src/lib/version-source.test.ts +163 -0
  32. package/src/lib/version-source.ts +101 -0
  33. package/src/pages/ApiPage.tsx +1 -1
  34. package/src/pages/DocsLayout.tsx +24 -3
  35. package/src/pages/DocsPage.tsx +7 -7
  36. package/src/pages/LandingPage.module.css +56 -0
  37. package/src/pages/LandingPage.tsx +39 -0
  38. package/src/pages/NotFound.module.css +3 -0
  39. package/src/pages/NotFound.tsx +9 -12
  40. package/src/server/App.tsx +21 -23
  41. package/src/server/api/{page/[...slug].ts → page.ts} +7 -3
  42. package/src/server/api/search.ts +51 -24
  43. package/src/server/api/specs.ts +17 -5
  44. package/src/server/entry-client.tsx +42 -14
  45. package/src/server/entry-server.tsx +35 -13
  46. package/src/server/plugins/telemetry.ts +47 -7
  47. package/src/server/routes/[...slug].md.ts +0 -6
  48. package/src/server/routes/[version]/llms.txt.ts +26 -0
  49. package/src/server/routes/llms.txt.ts +10 -13
  50. package/src/server/routes/og.tsx +2 -2
  51. package/src/server/routes/sitemap.xml.ts +14 -6
  52. package/src/server/vite-config.ts +5 -5
  53. package/src/themes/default/ContentDirButtons.tsx +66 -0
  54. package/src/themes/default/Layout.module.css +187 -40
  55. package/src/themes/default/Layout.tsx +166 -65
  56. package/src/themes/default/OpenInAI.tsx +112 -0
  57. package/src/themes/default/Page.module.css +30 -0
  58. package/src/themes/default/Page.tsx +1 -3
  59. package/src/themes/default/SidebarLogo.tsx +26 -0
  60. package/src/themes/default/Toc.module.css +102 -25
  61. package/src/themes/default/Toc.tsx +56 -10
  62. package/src/themes/default/VersionSwitcher.tsx +59 -0
  63. package/src/themes/paper/ContentDirDropdown.tsx +47 -0
  64. package/src/themes/paper/Layout.module.css +7 -0
  65. package/src/themes/paper/Layout.tsx +20 -13
  66. package/src/themes/paper/VersionSwitcher.tsx +60 -0
  67. package/src/types/config.ts +146 -23
  68. package/src/types/content.ts +11 -1
  69. package/src/types/theme.ts +1 -0
  70. package/src/components/ui/footer.module.css +0 -27
  71. package/src/components/ui/footer.tsx +0 -30
  72. package/src/server/api/metrics.ts +0 -23
  73. package/src/server/api/page/index.ts +0 -1
  74. package/src/server/telemetry.ts +0 -49
@@ -0,0 +1,112 @@
1
+ 'use client';
2
+
3
+ import {
4
+ ChevronDownIcon,
5
+ ClipboardDocumentIcon,
6
+ DocumentTextIcon,
7
+ SparklesIcon
8
+ } from '@heroicons/react/24/outline';
9
+ import { Button, Menu } from '@raystack/apsara';
10
+ import { useCallback } from 'react';
11
+
12
+ function ClaudeIcon() {
13
+ return (
14
+ <svg
15
+ xmlns='http://www.w3.org/2000/svg'
16
+ width='14'
17
+ height='14'
18
+ viewBox='0 0 24 24'
19
+ fill='currentColor'
20
+ fillRule='evenodd'
21
+ aria-hidden='true'
22
+ >
23
+ <title>Claude</title>
24
+ <path d='M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z' />
25
+ </svg>
26
+ );
27
+ }
28
+
29
+ function ChatGPTIcon() {
30
+ return (
31
+ <svg
32
+ xmlns='http://www.w3.org/2000/svg'
33
+ width='14'
34
+ height='14'
35
+ viewBox='0 0 12 12'
36
+ fill='none'
37
+ aria-hidden='true'
38
+ >
39
+ <path
40
+ d='M4.60254 4.37898V3.24915C4.60254 3.154 4.63825 3.08261 4.72146 3.0351L6.99307 1.72688C7.30228 1.54849 7.67098 1.46528 8.0515 1.46528C9.47862 1.46528 10.3825 2.57135 10.3825 3.74869C10.3825 3.83193 10.3825 3.92708 10.3706 4.02224L8.01581 2.64263C7.87311 2.55942 7.73034 2.55942 7.58765 2.64263L4.60254 4.37898ZM9.90677 8.77939V6.07964C9.90677 5.91311 9.83537 5.79418 9.6927 5.71095L6.70759 3.9746L7.6828 3.41559C7.76603 3.36808 7.83742 3.36808 7.92065 3.41559L10.1922 4.72381C10.8464 5.10443 11.2864 5.91311 11.2864 6.69799C11.2864 7.60182 10.7512 8.43436 9.90677 8.77929V8.77939ZM3.90086 6.40083L2.92565 5.83C2.84244 5.78248 2.80672 5.71107 2.80672 5.61592V2.9995C2.80672 1.727 3.78194 0.763609 5.10208 0.763609C5.60165 0.763609 6.06537 0.930148 6.45794 1.22746L4.11504 2.58329C3.97237 2.6665 3.90099 2.78543 3.90099 2.95199V6.40092L3.90086 6.40083ZM5.99999 7.61387L4.60254 6.82896V5.16401L5.99999 4.37911L7.39734 5.16401V6.82896L5.99999 7.61387ZM6.89789 11.2294C6.39835 11.2294 5.93463 11.0628 5.54206 10.7655L7.88493 9.40968C8.02763 9.32647 8.09901 9.20755 8.09901 9.04098V5.59205L9.08618 6.16288C9.16938 6.21039 9.2051 6.28178 9.2051 6.37696V8.99337C9.2051 10.2659 8.21794 11.2293 6.89789 11.2293V11.2294ZM4.07925 8.57726L1.80764 7.26906C1.15348 6.88842 0.713498 6.07977 0.713498 5.29486C0.713498 4.37911 1.26058 3.55851 2.10492 3.21358V5.92515C2.10492 6.09169 2.17633 6.21062 2.319 6.29385L5.29229 8.01825L4.31707 8.57726C4.23387 8.62477 4.16246 8.62477 4.07925 8.57726ZM3.9485 10.5277C2.60459 10.5277 1.61745 9.51678 1.61745 8.26802C1.61745 8.17287 1.62938 8.07772 1.6412 7.98256L3.9841 9.33839C4.12677 9.42163 4.26956 9.42163 4.41223 9.33839L7.39734 7.61399V8.74382C7.39734 8.83898 7.36165 8.91036 7.27841 8.95788L5.00683 10.2661C4.69759 10.4445 4.3289 10.5277 3.94838 10.5277H3.9485ZM6.89789 11.9429C8.33696 11.9429 9.53808 10.9201 9.81172 9.5643C11.1437 9.21937 12 7.97061 12 6.69811C12 5.86557 11.6433 5.05691 11.001 4.47414C11.0605 4.22437 11.0962 3.9746 11.0962 3.72495C11.0962 2.02429 9.71656 0.751662 8.12288 0.751662C7.80185 0.751662 7.49262 0.799178 7.18338 0.906279C6.64812 0.382967 5.91076 0.0499878 5.10208 0.0499878C3.66304 0.0499878 2.46192 1.07272 2.18828 2.42855C0.856291 2.77348 0 4.02224 0 5.29474C0 6.12728 0.356749 6.93594 0.998986 7.51871C0.939523 7.76848 0.903831 8.01825 0.903831 8.26793C0.903831 9.96859 2.28344 11.2412 3.87709 11.2412C4.19815 11.2412 4.50738 11.1937 4.81662 11.0866C5.35175 11.6099 6.08912 11.9429 6.89789 11.9429Z'
41
+ fill='currentColor'
42
+ />
43
+ </svg>
44
+ );
45
+ }
46
+
47
+ function mdUrl() {
48
+ return `${window.location.origin}${window.location.pathname}.md`;
49
+ }
50
+
51
+ async function copyMd() {
52
+ try {
53
+ const res = await fetch(mdUrl());
54
+ const text = await res.text();
55
+ await navigator.clipboard.writeText(text);
56
+ } catch {
57
+ // ignore
58
+ }
59
+ }
60
+
61
+ export function OpenInAI() {
62
+ const onCopy = useCallback(() => {
63
+ void copyMd();
64
+ }, []);
65
+ const onView = useCallback(() => {
66
+ window.open(mdUrl(), '_blank', 'noopener,noreferrer');
67
+ }, []);
68
+ const onChatGPT = useCallback(() => {
69
+ const q = encodeURIComponent(`Read ${mdUrl()}`);
70
+ window.open(`https://chatgpt.com/?q=${q}`, '_blank', 'noopener,noreferrer');
71
+ }, []);
72
+ const onClaude = useCallback(() => {
73
+ const q = encodeURIComponent(`Read ${mdUrl()}`);
74
+ window.open(`https://claude.ai/new?q=${q}`, '_blank', 'noopener,noreferrer');
75
+ }, []);
76
+
77
+ return (
78
+ <Menu>
79
+ <Menu.Trigger
80
+ render={
81
+ <Button
82
+ size='small'
83
+ variant='outline'
84
+ color='neutral'
85
+ leadingIcon={<SparklesIcon width={12} height={12} />}
86
+ trailingIcon={<ChevronDownIcon width={12} height={12} />}
87
+ />
88
+ }
89
+ >
90
+ Open in AI
91
+ </Menu.Trigger>
92
+ <Menu.Content>
93
+ <Menu.Item onClick={onCopy}>
94
+ <ClipboardDocumentIcon width={14} height={14} />
95
+ Copy as MD
96
+ </Menu.Item>
97
+ <Menu.Item onClick={onView}>
98
+ <DocumentTextIcon width={14} height={14} />
99
+ View MD
100
+ </Menu.Item>
101
+ <Menu.Item onClick={onChatGPT}>
102
+ <ChatGPTIcon />
103
+ Open in ChatGPT
104
+ </Menu.Item>
105
+ <Menu.Item onClick={onClaude}>
106
+ <ClaudeIcon />
107
+ Open in Claude
108
+ </Menu.Item>
109
+ </Menu.Content>
110
+ </Menu>
111
+ );
112
+ }
@@ -26,6 +26,36 @@
26
26
  line-height: 1.4;
27
27
  }
28
28
 
29
+ .content > :is(h1, h2, h3, h4, h5, h6):first-child {
30
+ margin-top: 0;
31
+ }
32
+
33
+ .content h1 {
34
+ margin-top: 0;
35
+ margin-bottom: var(--rs-space-10);
36
+ }
37
+
38
+ .content p {
39
+ color: var(--rs-color-foreground-base-primary);
40
+ font-family: var(--rs-font-body);
41
+ font-size: var(--rs-font-size-regular);
42
+ font-style: normal;
43
+ font-weight: var(--rs-font-weight-regular);
44
+ line-height: 171.429%;
45
+ }
46
+
47
+ .content h2 {
48
+ margin-top: var(--rs-space-8);
49
+ margin-bottom: var(--rs-space-8);
50
+ color: var(--rs-color-foreground-base-primary);
51
+ font-family: var(--rs-font-title);
52
+ font-size: var(--rs-font-size-t3);
53
+ font-style: normal;
54
+ font-weight: var(--rs-font-weight-medium);
55
+ line-height: var(--rs-line-height-t3);
56
+ letter-spacing: var(--rs-letter-spacing-t3);
57
+ }
58
+
29
59
  .content ul,
30
60
  .content ol {
31
61
  padding-left: var(--rs-space-5);
@@ -1,16 +1,14 @@
1
1
  'use client';
2
2
 
3
3
  import { Flex } from '@raystack/apsara';
4
- import { Breadcrumbs } from '@/components/ui/breadcrumbs';
5
4
  import type { ThemePageProps } from '@/types';
6
5
  import styles from './Page.module.css';
7
6
  import { Toc } from './Toc';
8
7
 
9
- export function Page({ page, tree }: ThemePageProps) {
8
+ export function Page({ page }: ThemePageProps) {
10
9
  return (
11
10
  <Flex className={styles.page}>
12
11
  <article className={styles.article} data-article-content>
13
- <Breadcrumbs slug={page.slug} tree={tree} />
14
12
  <div className={styles.content}>{page.content}</div>
15
13
  </article>
16
14
  <Toc items={page.toc} />
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import { BookOpenIcon } from '@heroicons/react/24/outline';
4
+ import { useTheme } from '@raystack/apsara';
5
+ import type { ChronicleConfig } from '@/types';
6
+ import styles from './Layout.module.css';
7
+
8
+ interface SidebarLogoProps {
9
+ config: ChronicleConfig;
10
+ }
11
+
12
+ export function SidebarLogo({ config }: SidebarLogoProps) {
13
+ const { resolvedTheme } = useTheme();
14
+ const logo = config.logo;
15
+
16
+ if (logo) {
17
+ const src = resolvedTheme === 'dark'
18
+ ? logo.dark ?? logo.light
19
+ : logo.light ?? logo.dark;
20
+ if (src) {
21
+ return <img src={src} alt={config.site.title} className={styles.sidebarLogo} />;
22
+ }
23
+ }
24
+
25
+ return <BookOpenIcon width={28} height={28} />;
26
+ }
@@ -1,48 +1,125 @@
1
1
  .toc {
2
- width: 200px;
3
- flex-shrink: 0;
4
- position: sticky;
5
- top: var(--rs-space-9);
6
- max-height: calc(100vh - var(--rs-space-17));
7
- overflow-y: auto;
2
+ position: fixed;
3
+ right: var(--rs-space-3);
4
+ top: 50%;
5
+ transform: translateY(-50%);
6
+ z-index: 10;
8
7
  }
9
8
 
10
- .title {
9
+ .markers {
10
+ display: flex;
11
+ flex-direction: column;
12
+ align-items: flex-end;
13
+ gap: var(--rs-space-4);
14
+ opacity: 1;
15
+ transition: opacity 150ms ease;
16
+ }
17
+
18
+ .toc:hover .markers,
19
+ .toc:focus-within .markers {
20
+ opacity: 0;
21
+ pointer-events: none;
22
+ }
23
+
24
+ .marker {
11
25
  display: block;
26
+ height: 2px;
27
+ background: var(--rs-color-border-base-secondary);
28
+ border-radius: 1px;
29
+ transition:
30
+ width 0.15s ease,
31
+ background 0.15s ease;
32
+ }
33
+
34
+ .marker:hover {
35
+ background: var(--rs-color-foreground-base-secondary);
36
+ }
37
+
38
+ .markerActive {
39
+ background: var(--rs-color-border-base-emphasis);
40
+ }
41
+
42
+
43
+ .panel {
44
+ position: absolute;
45
+ top: 50%;
46
+ right: 0;
47
+ transform: translateY(-50%) translateX(8px);
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: var(--rs-space-2);
51
+ min-width: 200px;
52
+ padding: var(--rs-space-3) 0;
53
+ background: var(--rs-color-background-base-primary);
54
+ border: 0.5px solid var(--rs-color-border-base-primary);
55
+ border-radius: var(--rs-radius-4);
56
+ box-shadow: var(--rs-shadow-soft);
57
+ opacity: 0;
58
+ pointer-events: none;
59
+ transition:
60
+ opacity 150ms ease,
61
+ transform 150ms ease;
62
+ }
63
+
64
+ .toc:hover .panel,
65
+ .toc:focus-within .panel {
66
+ opacity: 1;
67
+ pointer-events: auto;
68
+ transform: translateY(-50%) translateX(0);
69
+ }
70
+
71
+ .panelHeader {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: var(--rs-space-3);
75
+ padding: var(--rs-space-3);
12
76
  color: var(--rs-color-foreground-base-secondary);
13
- text-transform: uppercase;
14
- letter-spacing: 0.05em;
15
- margin-bottom: var(--rs-space-3);
16
- font-size: var(--rs-font-size-mini);
77
+ font-family: var(--rs-font-body);
78
+ font-size: var(--rs-font-size-small);
79
+ font-weight: var(--rs-font-weight-medium);
80
+ line-height: var(--rs-line-height-small);
81
+ letter-spacing: var(--rs-letter-spacing-small);
17
82
  }
18
83
 
19
- .nav {
84
+ .panelHeaderLabel {
85
+ flex: 1;
86
+ }
87
+
88
+ .panelList {
20
89
  display: flex;
21
90
  flex-direction: column;
22
- gap: 0;
23
- border-left: 1px solid var(--rs-color-border-base-primary);
24
- padding-left: var(--rs-space-3);
25
- margin-bottom: var(--rs-space-6);
91
+ gap: var(--rs-space-1);
92
+ padding-left: var(--rs-space-8);
26
93
  }
27
94
 
28
- .link {
95
+ .panelItem {
96
+ display: block;
97
+ padding: var(--rs-space-3);
98
+ border-radius: var(--rs-radius-2);
29
99
  color: var(--rs-color-foreground-base-tertiary);
30
- text-decoration: none;
100
+ font-family: var(--rs-font-body);
31
101
  font-size: var(--rs-font-size-small);
32
- line-height: 1.4;
33
- padding: var(--rs-space-1) 0;
102
+ font-weight: var(--rs-font-weight-medium);
103
+ line-height: var(--rs-line-height-small);
104
+ letter-spacing: var(--rs-letter-spacing-small);
105
+ text-decoration: none;
34
106
  transition: color 0.15s ease;
35
107
  }
36
108
 
37
- .link:hover {
109
+ .panelItem:hover {
38
110
  color: var(--rs-color-foreground-base-primary);
39
111
  }
40
112
 
41
- .active {
113
+ .panelItemNested {
114
+ padding-left: var(--rs-space-5);
115
+ }
116
+
117
+ .panelItemActive {
42
118
  color: var(--rs-color-foreground-base-primary);
43
- font-weight: 500;
44
119
  }
45
120
 
46
- .nested {
47
- padding-left: var(--rs-space-3);
121
+ @media (max-width: 900px) {
122
+ .toc {
123
+ display: none;
124
+ }
48
125
  }
@@ -1,10 +1,23 @@
1
1
  'use client';
2
2
 
3
- import { Text } from '@raystack/apsara';
3
+ import { Bars3BottomLeftIcon } from '@heroicons/react/24/outline';
4
4
  import { AnchorProvider, useActiveAnchor } from 'fumadocs-core/toc';
5
5
  import type { TableOfContents, TOCItemType } from 'fumadocs-core/toc';
6
+ import { cx } from 'class-variance-authority';
7
+ import { isValidElement, type ReactNode } from 'react';
6
8
  import styles from './Toc.module.css';
7
9
 
10
+ function nodeToText(node: ReactNode): string {
11
+ if (node == null || typeof node === 'boolean') return '';
12
+ if (typeof node === 'string' || typeof node === 'number') return String(node);
13
+ if (Array.isArray(node)) return node.map(nodeToText).join('');
14
+ if (isValidElement(node)) {
15
+ const children = (node.props as { children?: ReactNode }).children;
16
+ return nodeToText(children);
17
+ }
18
+ return '';
19
+ }
20
+
8
21
  interface TocProps {
9
22
  items: TableOfContents;
10
23
  }
@@ -23,30 +36,63 @@ export function Toc({ items }: TocProps) {
23
36
  );
24
37
  }
25
38
 
39
+ const MARKER_BASE = 8;
40
+ const MARKER_PER_CHAR = 1;
41
+ const MARKER_MAX = 40;
42
+
43
+ function markerWidth(title: ReactNode): number {
44
+ const len = nodeToText(title).length;
45
+ return Math.min(MARKER_MAX, MARKER_BASE + len * MARKER_PER_CHAR);
46
+ }
47
+
26
48
  function TocContent({ items }: { items: TOCItemType[] }) {
27
49
  const activeAnchor = useActiveAnchor();
28
50
 
29
51
  return (
30
- <aside className={styles.toc}>
31
- <Text size={1} weight='medium' className={styles.title}>
32
- On this page
33
- </Text>
34
- <nav className={styles.nav}>
52
+ <aside className={styles.toc} aria-label='Table of contents'>
53
+ <div className={styles.markers} aria-hidden='true'>
35
54
  {items.map(item => {
36
55
  const id = item.url.replace('#', '');
37
56
  const isActive = activeAnchor === id;
38
- const isNested = item.depth > 2;
39
57
  return (
40
58
  <a
41
59
  key={item.url}
42
60
  href={item.url}
43
- className={`${styles.link} ${isActive ? styles.active : ''} ${isNested ? styles.nested : ''}`}
61
+ tabIndex={-1}
62
+ className={cx(styles.marker, isActive && styles.markerActive)}
63
+ style={{ width: `${markerWidth(item.title)}px` }}
44
64
  >
45
- {item.title}
65
+ <span />
46
66
  </a>
47
67
  );
48
68
  })}
49
- </nav>
69
+ </div>
70
+ <div className={styles.panel} role='presentation'>
71
+ <div className={styles.panelHeader}>
72
+ <Bars3BottomLeftIcon width={16} height={16} />
73
+ <span className={styles.panelHeaderLabel}>On this page</span>
74
+ </div>
75
+ <nav className={styles.panelList}>
76
+ {items.map(item => {
77
+ const id = item.url.replace('#', '');
78
+ const isActive = activeAnchor === id;
79
+ const isNested = item.depth > 2;
80
+ return (
81
+ <a
82
+ key={item.url}
83
+ href={item.url}
84
+ className={cx(
85
+ styles.panelItem,
86
+ isNested && styles.panelItemNested,
87
+ isActive && styles.panelItemActive
88
+ )}
89
+ >
90
+ {nodeToText(item.title)}
91
+ </a>
92
+ );
93
+ })}
94
+ </nav>
95
+ </div>
50
96
  </aside>
51
97
  );
52
98
  }
@@ -0,0 +1,59 @@
1
+ import { ChevronDownIcon } from '@heroicons/react/24/outline';
2
+ import { Badge, Button, Menu, Flex } from '@raystack/apsara';
3
+ import { useNavigate } from 'react-router';
4
+ import { getAllVersions } from '@/lib/config';
5
+ import { getVersionHomeHref } from '@/lib/navigation';
6
+ import { usePageContext } from '@/lib/page-context';
7
+
8
+ export function VersionSwitcher() {
9
+ const { config, version } = usePageContext();
10
+ const navigate = useNavigate();
11
+
12
+ if (!config.versions?.length) return null;
13
+
14
+ const versions = getAllVersions(config);
15
+ const active = versions.find(v =>
16
+ v.isLatest ? version.dir === null : v.dir === version.dir,
17
+ );
18
+
19
+ return (
20
+ <Menu>
21
+ <Menu.Trigger
22
+ render={
23
+ <Button
24
+ size='small'
25
+ variant='outline'
26
+ color='neutral'
27
+ trailingIcon={<ChevronDownIcon width={14} height={14} />}
28
+ />
29
+ }
30
+ >
31
+ <Flex gap='small' align='center'>
32
+ {active?.label ?? 'Version'}
33
+ {active?.badge ? (
34
+ <Badge variant={active.badge.variant} size='micro'>
35
+ {active.badge.label}
36
+ </Badge>
37
+ ) : null}
38
+ </Flex>
39
+ </Menu.Trigger>
40
+ <Menu.Content>
41
+ {versions.map(v => (
42
+ <Menu.Item
43
+ key={v.dir ?? '_latest'}
44
+ onClick={() => navigate(getVersionHomeHref(config, v.dir))}
45
+ >
46
+ <Flex gap='small' align='center'>
47
+ {v.label}
48
+ {v.badge ? (
49
+ <Badge variant={v.badge.variant} size='micro'>
50
+ {v.badge.label}
51
+ </Badge>
52
+ ) : null}
53
+ </Flex>
54
+ </Menu.Item>
55
+ ))}
56
+ </Menu.Content>
57
+ </Menu>
58
+ );
59
+ }
@@ -0,0 +1,47 @@
1
+ import { ChevronDownIcon } from '@heroicons/react/24/outline';
2
+ import { Button, Menu } from '@raystack/apsara';
3
+ import { useLocation, useNavigate } from 'react-router';
4
+ import { getLandingEntries } from '@/lib/config';
5
+ import { getActiveContentDir } from '@/lib/navigation';
6
+ import { usePageContext } from '@/lib/page-context';
7
+
8
+ export function ContentDirDropdown() {
9
+ const { config, version } = usePageContext();
10
+ const { pathname } = useLocation();
11
+ const navigate = useNavigate();
12
+
13
+ const entries = getLandingEntries(config, version.dir);
14
+ if (entries.length <= 1) return null;
15
+
16
+ const activeDir = getActiveContentDir(pathname, config);
17
+ const activeEntry =
18
+ entries.find(e => e.contentDir === activeDir) ?? entries[0];
19
+
20
+ return (
21
+ <Menu>
22
+ <Menu.Trigger
23
+ render={
24
+ <Button
25
+ size='small'
26
+ variant='outline'
27
+ color='neutral'
28
+ width='100%'
29
+ trailingIcon={<ChevronDownIcon width={14} height={14} />}
30
+ />
31
+ }
32
+ >
33
+ {activeEntry.label}
34
+ </Menu.Trigger>
35
+ <Menu.Content>
36
+ {entries.map(entry => (
37
+ <Menu.Item
38
+ key={entry.href}
39
+ onClick={() => navigate(entry.href)}
40
+ >
41
+ {entry.label}
42
+ </Menu.Item>
43
+ ))}
44
+ </Menu.Content>
45
+ </Menu>
46
+ );
47
+ }
@@ -25,6 +25,13 @@
25
25
  margin-bottom: var(--rs-space-7);
26
26
  }
27
27
 
28
+ .nav {
29
+ display: flex;
30
+ flex-direction: column;
31
+ gap: var(--rs-space-3);
32
+ margin-bottom: var(--rs-space-7);
33
+ }
34
+
28
35
  .content {
29
36
  flex: 1;
30
37
  overflow-y: auto;
@@ -2,36 +2,43 @@
2
2
 
3
3
  import { Flex, Headline } from '@raystack/apsara';
4
4
  import { cx } from 'class-variance-authority';
5
- import { Footer } from '@/components/ui/footer';
6
5
  import type { ThemeLayoutProps } from '@/types';
7
6
  import { ChapterNav } from './ChapterNav';
7
+ import { ContentDirDropdown } from './ContentDirDropdown';
8
8
  import styles from './Layout.module.css';
9
+ import { VersionSwitcher } from './VersionSwitcher';
9
10
 
10
11
  export function Layout({
11
12
  children,
12
13
  config,
13
14
  tree,
15
+ hideSidebar,
14
16
  classNames
15
17
  }: ThemeLayoutProps) {
16
18
  return (
17
19
  <Flex direction='column' className={cx(styles.layout, classNames?.layout)}>
18
20
  <Flex className={cx(styles.body, classNames?.body)}>
19
- <aside className={cx(styles.sidebar, classNames?.sidebar)}>
20
- <Headline
21
- size='small'
22
- weight='medium'
23
- as='h1'
24
- className={styles.title}
25
- >
26
- {config.title}
27
- </Headline>
28
- <ChapterNav tree={tree} />
29
- </aside>
21
+ {hideSidebar ? null : (
22
+ <aside className={cx(styles.sidebar, classNames?.sidebar)}>
23
+ <Headline
24
+ size='small'
25
+ weight='medium'
26
+ as='h1'
27
+ className={styles.title}
28
+ >
29
+ {config.site.title}
30
+ </Headline>
31
+ <div className={styles.nav}>
32
+ <VersionSwitcher />
33
+ <ContentDirDropdown />
34
+ </div>
35
+ <ChapterNav tree={tree} />
36
+ </aside>
37
+ )}
30
38
  <div className={cx(styles.content, classNames?.content)}>
31
39
  {children}
32
40
  </div>
33
41
  </Flex>
34
- <Footer config={config.footer} />
35
42
  </Flex>
36
43
  );
37
44
  }
@@ -0,0 +1,60 @@
1
+ import { ChevronDownIcon } from '@heroicons/react/24/outline';
2
+ import { Badge, Button, Menu, Flex } from '@raystack/apsara';
3
+ import { useNavigate } from 'react-router';
4
+ import { getAllVersions } from '@/lib/config';
5
+ import { getVersionHomeHref } from '@/lib/navigation';
6
+ import { usePageContext } from '@/lib/page-context';
7
+
8
+ export function VersionSwitcher() {
9
+ const { config, version } = usePageContext();
10
+ const navigate = useNavigate();
11
+
12
+ if (!config.versions?.length) return null;
13
+
14
+ const versions = getAllVersions(config);
15
+ const active = versions.find(v =>
16
+ v.isLatest ? version.dir === null : v.dir === version.dir,
17
+ );
18
+
19
+ return (
20
+ <Menu>
21
+ <Menu.Trigger
22
+ render={
23
+ <Button
24
+ size='small'
25
+ variant='outline'
26
+ color='neutral'
27
+ width='100%'
28
+ trailingIcon={<ChevronDownIcon width={14} height={14} />}
29
+ />
30
+ }
31
+ >
32
+ <Flex gap='small' align='center' justify='start'>
33
+ {active?.label ?? 'Version'}
34
+ {active?.badge ? (
35
+ <Badge variant={active.badge.variant} size='micro'>
36
+ {active.badge.label}
37
+ </Badge>
38
+ ) : null}
39
+ </Flex>
40
+ </Menu.Trigger>
41
+ <Menu.Content>
42
+ {versions.map(v => (
43
+ <Menu.Item
44
+ key={v.dir ?? '_latest'}
45
+ onClick={() => navigate(getVersionHomeHref(config, v.dir))}
46
+ >
47
+ <Flex gap='small' align='center'>
48
+ {v.label}
49
+ {v.badge ? (
50
+ <Badge variant={v.badge.variant} size='micro'>
51
+ {v.badge.label}
52
+ </Badge>
53
+ ) : null}
54
+ </Flex>
55
+ </Menu.Item>
56
+ ))}
57
+ </Menu.Content>
58
+ </Menu>
59
+ );
60
+ }