@raystack/chronicle 0.5.4 → 0.6.1

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 (68) hide show
  1. package/dist/cli/index.js +258 -80
  2. package/package.json +7 -5
  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/ui/breadcrumbs.tsx +4 -2
  16. package/src/components/ui/client-theme-switcher.tsx +21 -4
  17. package/src/components/ui/search.module.css +16 -41
  18. package/src/components/ui/search.tsx +30 -41
  19. package/src/lib/config.test.ts +493 -0
  20. package/src/lib/config.ts +123 -22
  21. package/src/lib/head.tsx +23 -5
  22. package/src/lib/llms.test.ts +94 -0
  23. package/src/lib/llms.ts +41 -0
  24. package/src/lib/navigation.test.ts +94 -0
  25. package/src/lib/navigation.ts +51 -0
  26. package/src/lib/page-context.tsx +51 -32
  27. package/src/lib/route-resolver.test.ts +173 -0
  28. package/src/lib/route-resolver.ts +73 -0
  29. package/src/lib/source.ts +94 -1
  30. package/src/lib/version-source.test.ts +163 -0
  31. package/src/lib/version-source.ts +101 -0
  32. package/src/pages/ApiPage.tsx +1 -1
  33. package/src/pages/DocsLayout.tsx +24 -3
  34. package/src/pages/DocsPage.tsx +3 -6
  35. package/src/pages/LandingPage.module.css +56 -0
  36. package/src/pages/LandingPage.tsx +39 -0
  37. package/src/pages/NotFound.tsx +2 -0
  38. package/src/server/App.tsx +21 -23
  39. package/src/server/api/page.ts +5 -1
  40. package/src/server/api/search.ts +51 -24
  41. package/src/server/api/specs.ts +17 -5
  42. package/src/server/entry-client.tsx +42 -14
  43. package/src/server/entry-server.tsx +33 -11
  44. package/src/server/routes/[...slug].md.ts +0 -6
  45. package/src/server/routes/[version]/llms.txt.ts +26 -0
  46. package/src/server/routes/llms.txt.ts +10 -13
  47. package/src/server/routes/og.tsx +2 -2
  48. package/src/server/routes/sitemap.xml.ts +14 -6
  49. package/src/server/vite-config.ts +5 -5
  50. package/src/themes/default/ContentDirButtons.tsx +66 -0
  51. package/src/themes/default/Layout.module.css +187 -40
  52. package/src/themes/default/Layout.tsx +166 -65
  53. package/src/themes/default/OpenInAI.tsx +112 -0
  54. package/src/themes/default/Page.module.css +30 -0
  55. package/src/themes/default/Page.tsx +1 -3
  56. package/src/themes/default/SidebarLogo.tsx +26 -0
  57. package/src/themes/default/Toc.module.css +102 -25
  58. package/src/themes/default/Toc.tsx +56 -10
  59. package/src/themes/default/VersionSwitcher.tsx +59 -0
  60. package/src/themes/paper/ContentDirDropdown.tsx +47 -0
  61. package/src/themes/paper/Layout.module.css +7 -0
  62. package/src/themes/paper/Layout.tsx +20 -13
  63. package/src/themes/paper/VersionSwitcher.tsx +60 -0
  64. package/src/types/config.ts +145 -23
  65. package/src/types/content.ts +11 -1
  66. package/src/types/theme.ts +1 -0
  67. package/src/components/ui/footer.module.css +0 -27
  68. package/src/components/ui/footer.tsx +0 -30
@@ -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
+ }