@raystack/chronicle 0.5.4 → 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 (69) hide show
  1. package/dist/cli/index.js +258 -80
  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 +51 -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 +3 -6
  36. package/src/pages/LandingPage.module.css +56 -0
  37. package/src/pages/LandingPage.tsx +39 -0
  38. package/src/pages/NotFound.tsx +2 -0
  39. package/src/server/App.tsx +21 -23
  40. package/src/server/api/page.ts +5 -1
  41. package/src/server/api/search.ts +51 -24
  42. package/src/server/api/specs.ts +17 -5
  43. package/src/server/entry-client.tsx +42 -14
  44. package/src/server/entry-server.tsx +33 -11
  45. package/src/server/routes/[...slug].md.ts +0 -6
  46. package/src/server/routes/[version]/llms.txt.ts +26 -0
  47. package/src/server/routes/llms.txt.ts +10 -13
  48. package/src/server/routes/og.tsx +2 -2
  49. package/src/server/routes/sitemap.xml.ts +14 -6
  50. package/src/server/vite-config.ts +5 -5
  51. package/src/themes/default/ContentDirButtons.tsx +66 -0
  52. package/src/themes/default/Layout.module.css +187 -40
  53. package/src/themes/default/Layout.tsx +166 -65
  54. package/src/themes/default/OpenInAI.tsx +112 -0
  55. package/src/themes/default/Page.module.css +30 -0
  56. package/src/themes/default/Page.tsx +1 -3
  57. package/src/themes/default/SidebarLogo.tsx +26 -0
  58. package/src/themes/default/Toc.module.css +102 -25
  59. package/src/themes/default/Toc.tsx +56 -10
  60. package/src/themes/default/VersionSwitcher.tsx +59 -0
  61. package/src/themes/paper/ContentDirDropdown.tsx +47 -0
  62. package/src/themes/paper/Layout.module.css +7 -0
  63. package/src/themes/paper/Layout.tsx +20 -13
  64. package/src/themes/paper/VersionSwitcher.tsx +60 -0
  65. package/src/types/config.ts +145 -23
  66. package/src/types/content.ts +11 -1
  67. package/src/types/theme.ts +1 -0
  68. package/src/components/ui/footer.module.css +0 -27
  69. package/src/components/ui/footer.tsx +0 -30
@@ -1,3 +1,4 @@
1
+ import uniqBy from 'lodash/uniqBy.js'
1
2
  import { z } from 'zod'
2
3
 
3
4
  const logoSchema = z.object({
@@ -45,19 +46,11 @@ const apiSchema = z.object({
45
46
  name: z.string(),
46
47
  spec: z.string(),
47
48
  basePath: z.string(),
49
+ icon: z.string().optional(),
48
50
  server: apiServerSchema,
49
51
  auth: apiAuthSchema.optional(),
50
52
  })
51
53
 
52
- const footerSchema = z.object({
53
- copyright: z.string().optional(),
54
- links: z.array(navLinkSchema).optional(),
55
- })
56
-
57
- const llmsSchema = z.object({
58
- enabled: z.boolean().optional(),
59
- })
60
-
61
54
  const googleAnalyticsSchema = z.object({
62
55
  measurementId: z.string(),
63
56
  })
@@ -73,24 +66,155 @@ const telemetrySchema = z.object({
73
66
  port: z.number().int().min(1).max(65535).default(9090),
74
67
  })
75
68
 
76
- export const chronicleConfigSchema = z.object({
69
+ const siteSchema = z.object({
77
70
  title: z.string(),
78
71
  description: z.string().optional(),
79
- url: z.string().optional(),
80
- content: z.string().optional(),
81
- preset: z.string().optional(),
82
- logo: logoSchema.optional(),
83
- theme: themeSchema.optional(),
84
- navigation: navigationSchema.optional(),
85
- search: searchSchema.optional(),
86
- footer: footerSchema.optional(),
72
+ })
73
+
74
+ const DIR_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/
75
+
76
+ const dirNameSchema = z
77
+ .string()
78
+ .min(1)
79
+ .refine((s) => DIR_NAME_PATTERN.test(s) && s !== '.' && s !== '..', {
80
+ message:
81
+ 'dir must start with a letter or digit and contain only letters, digits, ".", "_", or "-"',
82
+ })
83
+
84
+ const contentEntrySchema = z.object({
85
+ dir: dirNameSchema,
86
+ label: z.string().min(1),
87
+ icon: z.string().optional(),
88
+ })
89
+
90
+ // Variants map to Apsara Badge color prop.
91
+ // https://apsara.raystack.org/docs/components/badge
92
+ const badgeVariantSchema = z.enum([
93
+ 'accent',
94
+ 'warning',
95
+ 'danger',
96
+ 'success',
97
+ 'neutral',
98
+ 'gradient',
99
+ ])
100
+
101
+ const badgeSchema = z.object({
102
+ label: z.string().min(1),
103
+ variant: badgeVariantSchema.default('accent'),
104
+ })
105
+
106
+ const latestSchema = z.object({
107
+ label: z.string().min(1),
108
+ landing: z.boolean().optional(),
109
+ })
110
+
111
+ const versionSchema = z.object({
112
+ dir: dirNameSchema,
113
+ label: z.string().min(1),
114
+ badge: badgeSchema.optional(),
115
+ landing: z.boolean().optional(),
116
+ content: z.array(contentEntrySchema).min(1),
87
117
  api: z.array(apiSchema).optional(),
88
- llms: llmsSchema.optional(),
89
- analytics: analyticsSchema.optional(),
90
- telemetry: telemetrySchema.optional(),
91
118
  })
92
119
 
120
+ const allUnique = <T>(items: T[], key: (item: T) => string): boolean =>
121
+ uniqBy(items, key).length === items.length
122
+
123
+ const RESERVED_ROUTE_SEGMENTS = [
124
+ 'api',
125
+ 'apis',
126
+ 'og',
127
+ 'llms.txt',
128
+ 'robots.txt',
129
+ 'sitemap.xml',
130
+ ] as const
131
+
132
+ export const chronicleConfigSchema = z
133
+ .object({
134
+ site: siteSchema,
135
+ url: z.string().optional(),
136
+ content: z.array(contentEntrySchema).min(1),
137
+ latest: latestSchema.optional(),
138
+ versions: z.array(versionSchema).optional(),
139
+ preset: z.string().optional(),
140
+ logo: logoSchema.optional(),
141
+ theme: themeSchema.optional(),
142
+ navigation: navigationSchema.optional(),
143
+ search: searchSchema.optional(),
144
+ api: z.array(apiSchema).optional(),
145
+ analytics: analyticsSchema.optional(),
146
+ telemetry: telemetrySchema.optional(),
147
+ })
148
+ .strict()
149
+ .refine((cfg) => allUnique(cfg.content, (c) => c.dir), {
150
+ message: 'content[].dir must be unique',
151
+ path: ['content'],
152
+ })
153
+ .refine((cfg) => !cfg.versions || allUnique(cfg.versions, (v) => v.dir), {
154
+ message: 'versions[].dir must be unique',
155
+ path: ['versions'],
156
+ })
157
+ .refine(
158
+ (cfg) =>
159
+ !cfg.versions ||
160
+ cfg.versions.every((v) => allUnique(v.content, (c) => c.dir)),
161
+ {
162
+ message: 'versions[].content[].dir must be unique within each version',
163
+ path: ['versions'],
164
+ },
165
+ )
166
+ .refine((cfg) => !cfg.versions || cfg.versions.length === 0 || !!cfg.latest, {
167
+ message: 'latest is required when versions are declared',
168
+ path: ['latest'],
169
+ })
170
+ .refine(
171
+ (cfg) => {
172
+ if (!cfg.versions) return true
173
+ const contentDirs = new Set(cfg.content.map((c) => c.dir))
174
+ return !cfg.versions.some((v) => contentDirs.has(v.dir))
175
+ },
176
+ {
177
+ message:
178
+ 'versions[].dir must not overlap with content[].dir — the URL segment would be shadowed',
179
+ path: ['versions'],
180
+ },
181
+ )
182
+ .superRefine((cfg, ctx) => {
183
+ const reserved = new Set<string>(RESERVED_ROUTE_SEGMENTS)
184
+ const message = `dir must not be a reserved route segment: ${RESERVED_ROUTE_SEGMENTS.join(', ')}`
185
+
186
+ cfg.content.forEach((c, i) => {
187
+ if (reserved.has(c.dir)) {
188
+ ctx.addIssue({ code: 'custom', message, path: ['content', i, 'dir'] })
189
+ }
190
+ })
191
+ cfg.versions?.forEach((v, vi) => {
192
+ if (reserved.has(v.dir)) {
193
+ ctx.addIssue({
194
+ code: 'custom',
195
+ message,
196
+ path: ['versions', vi, 'dir'],
197
+ })
198
+ }
199
+ v.content.forEach((c, ci) => {
200
+ if (reserved.has(c.dir)) {
201
+ ctx.addIssue({
202
+ code: 'custom',
203
+ message,
204
+ path: ['versions', vi, 'content', ci, 'dir'],
205
+ })
206
+ }
207
+ })
208
+ })
209
+ })
210
+
93
211
  export type ChronicleConfig = z.infer<typeof chronicleConfigSchema>
212
+ export type SiteConfig = z.infer<typeof siteSchema>
213
+ export type ContentEntry = z.infer<typeof contentEntrySchema>
214
+ export type BadgeConfig = z.infer<typeof badgeSchema>
215
+ export type BadgeVariant = z.infer<typeof badgeVariantSchema>
216
+ export type LatestConfig = z.infer<typeof latestSchema>
217
+ export type VersionConfig = z.infer<typeof versionSchema>
94
218
  export type LogoConfig = z.infer<typeof logoSchema>
95
219
  export type ThemeConfig = z.infer<typeof themeSchema>
96
220
  export type NavigationConfig = z.infer<typeof navigationSchema>
@@ -100,8 +224,6 @@ export type SearchConfig = z.infer<typeof searchSchema>
100
224
  export type ApiConfig = z.infer<typeof apiSchema>
101
225
  export type ApiServerConfig = z.infer<typeof apiServerSchema>
102
226
  export type ApiAuthConfig = z.infer<typeof apiAuthSchema>
103
- export type FooterConfig = z.infer<typeof footerSchema>
104
- export type LlmsConfig = z.infer<typeof llmsSchema>
105
227
  export type AnalyticsConfig = z.infer<typeof analyticsSchema>
106
228
  export type GoogleAnalyticsConfig = z.infer<typeof googleAnalyticsSchema>
107
229
  export type TelemetryConfig = z.infer<typeof telemetrySchema>
@@ -12,7 +12,17 @@ export interface Frontmatter {
12
12
  lastModified?: string
13
13
  }
14
14
 
15
- export interface Page {
15
+ export interface PageNavLink {
16
+ url: string
17
+ title: string
18
+ }
19
+
20
+ export interface PageNav {
21
+ prev: PageNavLink | null
22
+ next: PageNavLink | null
23
+ }
24
+
25
+ export interface Page extends PageNav {
16
26
  slug: string[]
17
27
  frontmatter: Frontmatter
18
28
  content: ReactNode
@@ -7,6 +7,7 @@ export interface ThemeLayoutProps {
7
7
  children: ReactNode
8
8
  config: ChronicleConfig
9
9
  tree: Root
10
+ hideSidebar?: boolean
10
11
  classNames?: { layout?: string; body?: string; sidebar?: string; content?: string }
11
12
  }
12
13
 
@@ -1,27 +0,0 @@
1
- .footer {
2
- border-top: 1px solid var(--rs-color-border-base-primary);
3
- padding: var(--rs-space-5) var(--rs-space-7);
4
- }
5
-
6
- .container {
7
- max-width: 1200px;
8
- margin: 0 auto;
9
- width: 100%;
10
- }
11
-
12
- .copyright {
13
- color: var(--rs-color-foreground-base-secondary);
14
- }
15
-
16
- .links {
17
- flex-wrap: wrap;
18
- }
19
-
20
- .link {
21
- color: var(--rs-color-foreground-base-secondary);
22
- font-size: 14px;
23
- }
24
-
25
- .link:hover {
26
- color: var(--rs-color-foreground-base-primary);
27
- }
@@ -1,30 +0,0 @@
1
- import { Flex, Link, Text } from "@raystack/apsara";
2
- import type { FooterConfig } from "@/types";
3
- import styles from "./footer.module.css";
4
-
5
- interface FooterProps {
6
- config?: FooterConfig;
7
- }
8
-
9
- export function Footer({ config }: FooterProps) {
10
- return (
11
- <footer className={styles.footer}>
12
- <Flex align="center" justify="between" className={styles.container}>
13
- {config?.copyright && (
14
- <Text size={2} className={styles.copyright}>
15
- {config.copyright}
16
- </Text>
17
- )}
18
- {config?.links && config.links.length > 0 && (
19
- <Flex gap="medium" className={styles.links}>
20
- {config.links.map((link) => (
21
- <Link key={link.href} href={link.href} className={styles.link}>
22
- {link.label}
23
- </Link>
24
- ))}
25
- </Flex>
26
- )}
27
- </Flex>
28
- </footer>
29
- );
30
- }