doccupine 0.0.88 → 0.0.89

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 (64) hide show
  1. package/README.md +15 -2
  2. package/dist/index.js +79 -4
  3. package/dist/lib/layout.js +38 -24
  4. package/dist/lib/metadata.d.ts +30 -0
  5. package/dist/lib/metadata.js +98 -1
  6. package/dist/templates/app/theme.d.ts +1 -1
  7. package/dist/templates/app/theme.js +84 -19
  8. package/dist/templates/components/Chat.d.ts +1 -1
  9. package/dist/templates/components/Chat.js +26 -27
  10. package/dist/templates/components/SearchModalContent.d.ts +1 -1
  11. package/dist/templates/components/SearchModalContent.js +12 -6
  12. package/dist/templates/components/SideBar.d.ts +1 -1
  13. package/dist/templates/components/SideBar.js +3 -1
  14. package/dist/templates/components/layout/Accordion.d.ts +1 -1
  15. package/dist/templates/components/layout/Accordion.js +2 -1
  16. package/dist/templates/components/layout/ActionBar.d.ts +1 -1
  17. package/dist/templates/components/layout/ActionBar.js +4 -6
  18. package/dist/templates/components/layout/Button.d.ts +1 -1
  19. package/dist/templates/components/layout/Button.js +10 -0
  20. package/dist/templates/components/layout/Callout.d.ts +1 -1
  21. package/dist/templates/components/layout/Callout.js +75 -20
  22. package/dist/templates/components/layout/Card.d.ts +1 -1
  23. package/dist/templates/components/layout/Card.js +2 -1
  24. package/dist/templates/components/layout/CherryThemeProvider.d.ts +1 -1
  25. package/dist/templates/components/layout/CherryThemeProvider.js +6 -12
  26. package/dist/templates/components/layout/ClientThemeProvider.d.ts +1 -1
  27. package/dist/templates/components/layout/ClientThemeProvider.js +45 -40
  28. package/dist/templates/components/layout/Code.d.ts +1 -1
  29. package/dist/templates/components/layout/Code.js +223 -255
  30. package/dist/templates/components/layout/ColorSwatch.d.ts +1 -1
  31. package/dist/templates/components/layout/ColorSwatch.js +2 -2
  32. package/dist/templates/components/layout/Columns.d.ts +1 -1
  33. package/dist/templates/components/layout/Columns.js +1 -1
  34. package/dist/templates/components/layout/DemoTheme.d.ts +1 -1
  35. package/dist/templates/components/layout/DemoTheme.js +65 -167
  36. package/dist/templates/components/layout/DocsComponents.d.ts +1 -1
  37. package/dist/templates/components/layout/DocsComponents.js +13 -19
  38. package/dist/templates/components/layout/Field.d.ts +1 -1
  39. package/dist/templates/components/layout/Field.js +6 -4
  40. package/dist/templates/components/layout/Footer.d.ts +1 -1
  41. package/dist/templates/components/layout/Footer.js +1 -2
  42. package/dist/templates/components/layout/GlobalStyles.d.ts +1 -1
  43. package/dist/templates/components/layout/GlobalStyles.js +63 -10
  44. package/dist/templates/components/layout/Header.d.ts +1 -1
  45. package/dist/templates/components/layout/Header.js +14 -11
  46. package/dist/templates/components/layout/SharedStyles.d.ts +1 -1
  47. package/dist/templates/components/layout/SharedStyles.js +4 -5
  48. package/dist/templates/components/layout/StaticLinks.d.ts +1 -1
  49. package/dist/templates/components/layout/StaticLinks.js +4 -6
  50. package/dist/templates/components/layout/Steps.d.ts +1 -1
  51. package/dist/templates/components/layout/Steps.js +3 -3
  52. package/dist/templates/components/layout/Tabs.d.ts +1 -1
  53. package/dist/templates/components/layout/Tabs.js +5 -2
  54. package/dist/templates/components/layout/ThemeToggle.d.ts +1 -1
  55. package/dist/templates/components/layout/ThemeToggle.js +17 -19
  56. package/dist/templates/components/layout/Typography.d.ts +1 -1
  57. package/dist/templates/components/layout/Typography.js +1 -1
  58. package/dist/templates/components/layout/Update.d.ts +1 -1
  59. package/dist/templates/components/layout/Update.js +4 -3
  60. package/dist/templates/mdx/platform/site-settings.mdx.d.ts +1 -1
  61. package/dist/templates/mdx/platform/site-settings.mdx.js +5 -1
  62. package/dist/templates/package.js +0 -1
  63. package/dist/templates/proxy.js +14 -20
  64. package/package.json +1 -1
package/README.md CHANGED
@@ -129,7 +129,7 @@ Place these JSON files in your project root (where you run `doccupine`). They ar
129
129
  | File | Purpose |
130
130
  | ----------------- | --------------------------------------------------------------------------------------------------------- |
131
131
  | `doccupine.json` | CLI config (watchDir, outputDir, port). Auto-generated on first run. |
132
- | `config.json` | Site metadata: `name`, `description`, `icon`, `image` URL |
132
+ | `config.json` | Site metadata: `name`, `description`, `icon`, `image`, `url` (public site URL for sitemap/robots) |
133
133
  | `theme.json` | Theme overrides for [cherry-styled-components](https://github.com/cherry-design-system/styled-components) |
134
134
  | `navigation.json` | Manual navigation structure (overrides auto-generated) |
135
135
  | `links.json` | Static header/footer links |
@@ -139,7 +139,20 @@ Place these JSON files in your project root (where you run `doccupine`). They ar
139
139
 
140
140
  ## Public Directory
141
141
 
142
- Place static assets (images, favicons, `robots.txt`, etc.) in a `public/` directory at your project root. Doccupine copies it to the generated Next.js app on startup and watches for changes, so added, modified, or deleted files are synced automatically.
142
+ Place static assets (images, favicons, etc.) in a `public/` directory at your project root. Doccupine copies it to the generated Next.js app on startup and watches for changes, so added, modified, or deleted files are synced automatically.
143
+
144
+ ## Sitemap and robots.txt
145
+
146
+ Doccupine generates `robots.ts` automatically for every site. When you set a `url` in `config.json`, it also generates `sitemap.ts` covering every page (across all sections) and links the sitemap from `robots.txt`.
147
+
148
+ ```json
149
+ {
150
+ "name": "My Docs",
151
+ "url": "https://docs.example.com"
152
+ }
153
+ ```
154
+
155
+ You can override the URL at deploy time by setting the `NEXT_PUBLIC_SITE_URL` environment variable. When no URL is configured (neither in `config.json` nor via env), the sitemap is skipped and `robots.txt` is emitted without a sitemap reference.
143
156
 
144
157
  ## AI Chat Setup
145
158
 
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { appStructure, startingDocsStructure } from "./lib/structures.js";
10
10
  import { layoutTemplate } from "./lib/layout.js";
11
11
  import { ConfigManager } from "./lib/config-manager.js";
12
12
  import { findAvailablePort, generateSlug, getFullSlug, escapeTemplateContent, } from "./lib/utils.js";
13
- import { generateMetadataBlock, generateRuntimeOnlyMetadataBlock, } from "./lib/metadata.js";
13
+ import { generateMetadataBlock, generateRuntimeOnlyMetadataBlock, generateJsonLdScript, } from "./lib/metadata.js";
14
14
  import { nextConfigTemplate } from "./templates/next.config.js";
15
15
  import { proxyTemplate } from "./templates/proxy.js";
16
16
  import { robotsTemplate } from "./templates/app/robots.js";
@@ -786,6 +786,20 @@ export default function SectionIndex() {
786
786
  description: fm.description,
787
787
  icon: fm.icon,
788
788
  image: fm.image,
789
+ canonicalPath: mdxFile.slug,
790
+ });
791
+ const jsonLd = generateJsonLdScript({
792
+ kind: "article",
793
+ canonicalPath: mdxFile.slug,
794
+ title: fm.title,
795
+ description: fm.description,
796
+ date: typeof fm.date === "string" ? fm.date : undefined,
797
+ updated: typeof fm.updated === "string"
798
+ ? fm.updated
799
+ : typeof fm.date === "string"
800
+ ? fm.date
801
+ : undefined,
802
+ image: fm.image,
789
803
  });
790
804
  const pageContent = `import { Metadata } from "next";
791
805
  import { Docs } from "@/components/Docs";
@@ -795,8 +809,21 @@ const content = \`${escapeTemplateContent(mdxFile.content)}\`;
795
809
 
796
810
  ${metadataBlock}
797
811
 
812
+ // Doc pages have no per-request data: theme resolves client-side via the
813
+ // "dark" class on <html> (set before paint by the theme-init blocking
814
+ // script). Static rendering lets every response come from the edge cache.
815
+ export const dynamic = "force-static";
816
+ export const revalidate = false;
817
+
798
818
  export default function Page() {
799
- return <Docs content={content} />;
819
+ ${jsonLd.declarations}
820
+
821
+ return (
822
+ <>
823
+ ${jsonLd.element}
824
+ <Docs content={content} />
825
+ </>
826
+ );
800
827
  }
801
828
  `;
802
829
  const pagePath = path.join(this.outputDir, "app", mdxFile.slug, "page.tsx");
@@ -818,6 +845,10 @@ export default function Page() {
818
845
  icon: frontmatter.icon,
819
846
  image: frontmatter.image,
820
847
  name: frontmatter.name,
848
+ date: typeof frontmatter.date === "string" ? frontmatter.date : undefined,
849
+ updated: typeof frontmatter.updated === "string"
850
+ ? frontmatter.updated
851
+ : undefined,
821
852
  };
822
853
  break;
823
854
  }
@@ -831,8 +862,18 @@ export default function Page() {
831
862
  description: indexMDX.description || undefined,
832
863
  icon: indexMDX.icon,
833
864
  image: indexMDX.image,
865
+ canonicalPath: "",
834
866
  })
835
867
  : generateRuntimeOnlyMetadataBlock();
868
+ const homeJsonLd = generateJsonLdScript({
869
+ kind: "homepage",
870
+ canonicalPath: "",
871
+ title: indexMDX?.title,
872
+ description: indexMDX?.description || undefined,
873
+ date: indexMDX?.date,
874
+ updated: indexMDX?.updated ?? indexMDX?.date,
875
+ image: indexMDX?.image,
876
+ });
836
877
  const indexContent = `import { Metadata } from "next";
837
878
  import { Docs } from "@/components/Docs";
838
879
  import { config } from "@/utils/config";
@@ -841,8 +882,18 @@ ${indexMDX ? `const content = \`${escapeTemplateContent(indexMDX.content)}\`;` :
841
882
 
842
883
  ${metadataBlock}
843
884
 
885
+ export const dynamic = "force-static";
886
+ export const revalidate = false;
887
+
844
888
  export default function Home() {
845
- return <Docs content={content} />;
889
+ ${homeJsonLd.declarations}
890
+
891
+ return (
892
+ <>
893
+ ${homeJsonLd.element}
894
+ <Docs content={content} />
895
+ </>
896
+ );
846
897
  }
847
898
  `;
848
899
  await fs.writeFile(path.join(this.outputDir, "app", "page.tsx"), indexContent, "utf8");
@@ -856,6 +907,20 @@ export default function Home() {
856
907
  description: frontmatter.description || undefined,
857
908
  icon: frontmatter.icon,
858
909
  image: frontmatter.image,
910
+ canonicalPath: sectionSlug,
911
+ });
912
+ const sectionJsonLd = generateJsonLdScript({
913
+ kind: "article",
914
+ canonicalPath: sectionSlug,
915
+ title: frontmatter.title,
916
+ description: frontmatter.description,
917
+ date: typeof frontmatter.date === "string" ? frontmatter.date : undefined,
918
+ updated: typeof frontmatter.updated === "string"
919
+ ? frontmatter.updated
920
+ : typeof frontmatter.date === "string"
921
+ ? frontmatter.date
922
+ : undefined,
923
+ image: frontmatter.image,
859
924
  });
860
925
  const indexContent = `import { Metadata } from "next";
861
926
  import { Docs } from "@/components/Docs";
@@ -865,8 +930,18 @@ const content = \`${escapeTemplateContent(mdxContent)}\`;
865
930
 
866
931
  ${metadataBlock}
867
932
 
933
+ export const dynamic = "force-static";
934
+ export const revalidate = false;
935
+
868
936
  export default function Page() {
869
- return <Docs content={content} />;
937
+ ${sectionJsonLd.declarations}
938
+
939
+ return (
940
+ <>
941
+ ${sectionJsonLd.element}
942
+ <Docs content={content} />
943
+ </>
944
+ );
870
945
  }
871
946
  `;
872
947
  const pagePath = path.join(this.outputDir, "app", sectionSlug, "page.tsx");
@@ -48,9 +48,8 @@ ${a} >`
48
48
  return `import type { Metadata } from "next";
49
49
  ${isGoogleFont(fontConfig) ? `import { ${fontConfig.googleFont.fontName} } from "next/font/google";` : isLocalFont(fontConfig) ? 'import localFont from "next/font/local";' : 'import { Inter } from "next/font/google";'}
50
50
  import dynamic from "next/dynamic";
51
- import Script from "next/script";
52
51
  import { StyledComponentsRegistry } from "cherry-styled-components";
53
- import { theme, themeDark } from "@/app/theme";
52
+ import { theme } from "@/app/theme";
54
53
  import { CherryThemeProvider } from "@/components/layout/CherryThemeProvider";
55
54
  import { ChtProvider } from "@/components/Chat";
56
55
  import { SearchProvider } from "@/components/SearchDocs";
@@ -90,7 +89,18 @@ ${isGoogleFont(fontConfig)
90
89
  });`
91
90
  : 'const font = Inter({ subsets: ["latin"] });'}
92
91
 
92
+ function resolveSiteUrl(): URL | undefined {
93
+ const raw = process.env.NEXT_PUBLIC_SITE_URL ?? config.url;
94
+ if (!raw || typeof raw !== "string") return undefined;
95
+ try {
96
+ return new URL(raw);
97
+ } catch {
98
+ return undefined;
99
+ }
100
+ }
101
+
93
102
  export const metadata: Metadata = {
103
+ metadataBase: resolveSiteUrl(),
94
104
  title: config.name || "${DEFAULT_SITE_NAME}",
95
105
  description:
96
106
  config.description ||
@@ -118,24 +128,26 @@ ${hasSections
118
128
  const pages: PagesProps[] = doccupinePages;
119
129
 
120
130
  return (
121
- <html lang="en">
131
+ <html lang="en" suppressHydrationWarning>
122
132
  <head>
123
- {/* Prevents dark-mode FOUC on Safari/Firefox. These browsers don't support
124
- Sec-CH-Prefers-Color-Scheme (handled by middleware for Chrome), so on
125
- a first visit this blocking script detects prefers-color-scheme, sets
126
- the theme cookie, and hides the body until router.refresh() re-renders
127
- with the correct theme (see ClientThemeProvider). */}
128
- <Script
129
- id="theme-init"
130
- strategy="beforeInteractive"
133
+ {/* Resolves dark mode before first paint by adding the "dark" class
134
+ to <html> when needed. CSS variables in GlobalStyles flip values
135
+ on :root vs :root.dark, so the right palette renders without a
136
+ React roundtrip. Inlined as a plain <script> (not next/script) so
137
+ it ships in the SSR HTML and runs synchronously before paint —
138
+ next/script with beforeInteractive is async in App Router and
139
+ would still show a flash. suppressHydrationWarning on <html>
140
+ tells React the class/colorScheme attributes are intentionally
141
+ different between server (no class) and client (after script). */}
142
+ <script
131
143
  dangerouslySetInnerHTML={{
132
- __html: \`(function(){try{var c=document.cookie.split(";").find(function(s){return s.trim().startsWith("theme=")});if(!c){var d=window.matchMedia&&window.matchMedia("(prefers-color-scheme:dark)").matches;document.cookie="theme="+(d?"dark":"light")+";path=/;max-age=31536000;SameSite=Lax";if(d){var s=document.createElement("style");s.id="__theme-init";s.textContent="html{background:#000!important;color-scheme:dark}body{visibility:hidden}";document.head.appendChild(s)}}}catch(e){}})();\`,
144
+ __html: \`(function(){try{var c=document.cookie.split(";").map(function(s){return s.trim();}).find(function(s){return s.indexOf("theme=")===0;});var v=c?c.split("=")[1]:null;var d=v?v==="dark":(window.matchMedia&&window.matchMedia("(prefers-color-scheme:dark)").matches);if(!v){document.cookie="theme="+(d?"dark":"light")+";path=/;max-age=31536000;SameSite=Lax";}if(d){document.documentElement.classList.add("dark");document.documentElement.style.colorScheme="dark";}else{document.documentElement.style.colorScheme="light";}}catch(e){}})();\`,
133
145
  }}
134
146
  />
135
147
  </head>
136
148
  <body className={font.className}>
137
149
  <StyledComponentsRegistry>
138
- ${analyticsEnabled ? " <PostHogProvider>\n" : ""}${a} <CherryThemeProvider theme={theme} themeDark={themeDark}>
150
+ ${analyticsEnabled ? " <PostHogProvider>\n" : ""}${a} <CherryThemeProvider theme={theme}>
139
151
  ${a} ${chtOpen}
140
152
  ${a} <SearchProvider pages={pages} sections={doccupineSections}>
141
153
  ${a} <Header>
@@ -181,24 +193,26 @@ ${analyticsEnabled ? " </PostHogProvider>\n" : ""} </StyledCompo
181
193
  const defaultResults = transformPagesToGroupedStructure(defaultPages);
182
194
 
183
195
  return (
184
- <html lang="en">
196
+ <html lang="en" suppressHydrationWarning>
185
197
  <head>
186
- {/* Prevents dark-mode FOUC on Safari/Firefox. These browsers don't support
187
- Sec-CH-Prefers-Color-Scheme (handled by middleware for Chrome), so on
188
- a first visit this blocking script detects prefers-color-scheme, sets
189
- the theme cookie, and hides the body until router.refresh() re-renders
190
- with the correct theme (see ClientThemeProvider). */}
191
- <Script
192
- id="theme-init"
193
- strategy="beforeInteractive"
198
+ {/* Resolves dark mode before first paint by adding the "dark" class
199
+ to <html> when needed. CSS variables in GlobalStyles flip values
200
+ on :root vs :root.dark, so the right palette renders without a
201
+ React roundtrip. Inlined as a plain <script> (not next/script) so
202
+ it ships in the SSR HTML and runs synchronously before paint —
203
+ next/script with beforeInteractive is async in App Router and
204
+ would still show a flash. suppressHydrationWarning on <html>
205
+ tells React the class/colorScheme attributes are intentionally
206
+ different between server (no class) and client (after script). */}
207
+ <script
194
208
  dangerouslySetInnerHTML={{
195
- __html: \`(function(){try{var c=document.cookie.split(";").find(function(s){return s.trim().startsWith("theme=")});if(!c){var d=window.matchMedia&&window.matchMedia("(prefers-color-scheme:dark)").matches;document.cookie="theme="+(d?"dark":"light")+";path=/;max-age=31536000;SameSite=Lax";if(d){var s=document.createElement("style");s.id="__theme-init";s.textContent="html{background:#000!important;color-scheme:dark}body{visibility:hidden}";document.head.appendChild(s)}}}catch(e){}})();\`,
209
+ __html: \`(function(){try{var c=document.cookie.split(";").map(function(s){return s.trim();}).find(function(s){return s.indexOf("theme=")===0;});var v=c?c.split("=")[1]:null;var d=v?v==="dark":(window.matchMedia&&window.matchMedia("(prefers-color-scheme:dark)").matches);if(!v){document.cookie="theme="+(d?"dark":"light")+";path=/;max-age=31536000;SameSite=Lax";}if(d){document.documentElement.classList.add("dark");document.documentElement.style.colorScheme="dark";}else{document.documentElement.style.colorScheme="light";}}catch(e){}})();\`,
196
210
  }}
197
211
  />
198
212
  </head>
199
213
  <body className={font.className}>
200
214
  <StyledComponentsRegistry>
201
- ${analyticsEnabled ? " <PostHogProvider>\n" : ""}${a} <CherryThemeProvider theme={theme} themeDark={themeDark}>
215
+ ${analyticsEnabled ? " <PostHogProvider>\n" : ""}${a} <CherryThemeProvider theme={theme}>
202
216
  ${a} ${chtOpen}
203
217
  ${a} <SearchProvider pages={pages}>
204
218
  ${a} <Header />
@@ -1,3 +1,28 @@
1
+ export type JsonLdKind = "article" | "homepage";
2
+ export interface JsonLdOptions {
3
+ kind: JsonLdKind;
4
+ /** Page slug ("" for homepage). Used to build the absolute URL with config.url. */
5
+ canonicalPath: string;
6
+ title?: string;
7
+ description?: string;
8
+ date?: string;
9
+ /** Optional last-modified date; defaults to date when present. */
10
+ updated?: string;
11
+ image?: string;
12
+ }
13
+ /**
14
+ * Returns a code snippet that:
15
+ * - declares a `jsonLd` object at runtime using the site's config,
16
+ * - and renders a single `<script type="application/ld+json">` element.
17
+ *
18
+ * The snippet is meant to be inlined inside a generated page component.
19
+ * It emits a TechArticle on every doc page; the homepage additionally
20
+ * emits a graph that includes an Organization entity for entity recognition.
21
+ */
22
+ export declare function generateJsonLdScript(opts: JsonLdOptions): {
23
+ declarations: string;
24
+ element: string;
25
+ };
1
26
  export interface MetadataOptions {
2
27
  title?: string;
3
28
  titleFallback: string;
@@ -6,6 +31,11 @@ export interface MetadataOptions {
6
31
  description?: string;
7
32
  icon?: string;
8
33
  image?: string;
34
+ /**
35
+ * Canonical path for this page, e.g. "" for the homepage or "components".
36
+ * Resolves against `metadataBase` defined in the root layout.
37
+ */
38
+ canonicalPath?: string;
9
39
  }
10
40
  export declare function generateMetadataBlock(opts: MetadataOptions): string;
11
41
  export declare function generateRuntimeOnlyMetadataBlock(): string;
@@ -1,4 +1,91 @@
1
1
  import { DEFAULT_FAVICON, DEFAULT_META_DESCRIPTION, DEFAULT_OG_IMAGE, DEFAULT_SITE_NAME, } from "./constants.js";
2
+ /**
3
+ * Returns a code snippet that:
4
+ * - declares a `jsonLd` object at runtime using the site's config,
5
+ * - and renders a single `<script type="application/ld+json">` element.
6
+ *
7
+ * The snippet is meant to be inlined inside a generated page component.
8
+ * It emits a TechArticle on every doc page; the homepage additionally
9
+ * emits a graph that includes an Organization entity for entity recognition.
10
+ */
11
+ export function generateJsonLdScript(opts) {
12
+ const safePath = opts.canonicalPath.replace(/^\/+/, "");
13
+ const titleLiteral = JSON.stringify(opts.title ?? DEFAULT_SITE_NAME);
14
+ const descLiteral = JSON.stringify(opts.description ?? DEFAULT_META_DESCRIPTION);
15
+ const dateLiteral = opts.date ? JSON.stringify(opts.date) : "undefined";
16
+ const updatedLiteral = opts.updated
17
+ ? JSON.stringify(opts.updated)
18
+ : opts.date
19
+ ? JSON.stringify(opts.date)
20
+ : "undefined";
21
+ const defaultFaviconLiteral = JSON.stringify(DEFAULT_FAVICON);
22
+ const faviconLine = opts.image
23
+ ? `const faviconUrl =
24
+ ${JSON.stringify(opts.image)} ||
25
+ config.icon ||
26
+ ${defaultFaviconLiteral};`
27
+ : `const faviconUrl = config.icon || ${defaultFaviconLiteral};`;
28
+ // Indent 10 + "description: ".length(13) + ",".length(1) = 24. Prettier
29
+ // wraps the value to the next line (indent 12) once total exceeds 80.
30
+ const descriptionLine = descLiteral.length > 56
31
+ ? `description:
32
+ ${descLiteral},`
33
+ : `description: ${descLiteral},`;
34
+ const pathLiteral = JSON.stringify(safePath);
35
+ const homepageGraph = opts.kind === "homepage"
36
+ ? `,
37
+ {
38
+ "@type": "Organization",
39
+ name: siteName,
40
+ url: baseUrl ?? undefined,
41
+ logo: faviconUrl,
42
+ }`
43
+ : "";
44
+ const declarations = `const __jsonLdBaseUrl = (() => {
45
+ const raw =
46
+ typeof process !== "undefined"
47
+ ? process.env.NEXT_PUBLIC_SITE_URL
48
+ : undefined;
49
+ const fromEnv = typeof raw === "string" && raw.trim() !== "" ? raw : null;
50
+ const fromConfig =
51
+ typeof config.url === "string" && config.url.trim() !== ""
52
+ ? config.url
53
+ : null;
54
+ return (fromEnv ?? fromConfig)?.replace(/\\/$/, "") ?? null;
55
+ })();
56
+ const __jsonLd = (() => {
57
+ const baseUrl = __jsonLdBaseUrl;
58
+ const path = ${pathLiteral};
59
+ const url = baseUrl ? (path ? \`\${baseUrl}/\${path}\` : baseUrl) : undefined;
60
+ const siteName = config.name || ${JSON.stringify(DEFAULT_SITE_NAME)};
61
+ ${faviconLine}
62
+ return {
63
+ "@context": "https://schema.org",
64
+ "@graph": [
65
+ {
66
+ "@type": "TechArticle",
67
+ headline: ${titleLiteral},
68
+ ${descriptionLine}
69
+ url,
70
+ mainEntityOfPage: url,
71
+ datePublished: ${dateLiteral},
72
+ dateModified: ${updatedLiteral},
73
+ author: { "@type": "Organization", name: siteName },
74
+ publisher: {
75
+ "@type": "Organization",
76
+ name: siteName,
77
+ logo: { "@type": "ImageObject", url: faviconUrl },
78
+ },
79
+ }${homepageGraph},
80
+ ],
81
+ };
82
+ })();`;
83
+ const element = `<script
84
+ type="application/ld+json"
85
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(__jsonLd) }}
86
+ />`;
87
+ return { declarations, element };
88
+ }
2
89
  function buildFieldExpression(staticValue, configKey, defaultValue) {
3
90
  if (staticValue)
4
91
  return staticValue;
@@ -18,15 +105,24 @@ function buildTitleExpression(opts) {
18
105
  : `\${config.name ? config.name + " -" : "${DEFAULT_SITE_NAME} -"}`;
19
106
  return `${prefix} ${title}`;
20
107
  }
108
+ function buildCanonicalLine(canonicalPath) {
109
+ if (canonicalPath === undefined)
110
+ return "";
111
+ const safePath = canonicalPath.replace(/^\/+/, "");
112
+ // Relative path resolves against `metadataBase` set in the root layout.
113
+ // Empty string means the homepage canonical equals the base URL itself.
114
+ return `\n alternates: { canonical: ${JSON.stringify("/" + safePath)} },`;
115
+ }
21
116
  export function generateMetadataBlock(opts) {
22
117
  const title = buildTitleExpression(opts);
23
118
  const desc = buildFieldExpression(opts.description, "description", DEFAULT_META_DESCRIPTION);
24
119
  const icon = buildFieldExpression(opts.icon, "icon", DEFAULT_FAVICON);
25
120
  const image = buildFieldExpression(opts.image, "image", DEFAULT_OG_IMAGE);
121
+ const canonical = buildCanonicalLine(opts.canonicalPath);
26
122
  return `export const metadata: Metadata = {
27
123
  title: \`${title}\`,
28
124
  description: \`${desc}\`,
29
- icons: \`${icon}\`,
125
+ icons: \`${icon}\`,${canonical}
30
126
  openGraph: {
31
127
  title: \`${title}\`,
32
128
  description: \`${desc}\`,
@@ -43,6 +139,7 @@ export function generateRuntimeOnlyMetadataBlock() {
43
139
  title: \`${title}\`,
44
140
  description: \`${desc}\`,
45
141
  icons: \`${icon}\`,
142
+ alternates: { canonical: "/" },
46
143
  openGraph: {
47
144
  title: \`${title}\`,
48
145
  description: \`${desc}\`,
@@ -1,3 +1,3 @@
1
1
  export declare const SIDEBAR_WIDTH = 280;
2
2
  export declare const CHAT_WIDTH = 420;
3
- export declare const themeTemplate = "\"use client\";\nimport customThemeJson from \"@/theme.json\";\n\ninterface CustomTheme {\n default?: Partial<Colors>;\n dark?: Partial<Colors>;\n}\n\nconst customTheme = customThemeJson as CustomTheme;\n\nconst breakpoints: Breakpoints = {\n xs: 0,\n sm: 576,\n md: 768,\n lg: 992,\n xl: 1200,\n xxl: 1440,\n xxxl: 1920,\n};\n\nexport function mq(minWidth: keyof Breakpoints) {\n return `@media screen and (min-width: ${breakpoints[minWidth]}px)`;\n}\n\nconst spacing: Spacing = {\n maxWidth: { xs: \"1280px\", xxxl: \"1440px\" },\n padding: { xs: \"20px\", lg: \"40px\" },\n radius: { xs: \"6px\", lg: \"12px\", xl: \"30px\" },\n gridGap: { xs: \"20px\", lg: \"40px\" },\n};\n\nconst colors: Colors = {\n primaryLight: \"#d1d5db\",\n primary: \"#556272\",\n primaryDark: \"#374151\",\n secondaryLight: \"#c4b5fd\",\n secondary: \"#8b5cf6\",\n secondaryDark: \"#5b21b6\",\n tertiaryLight: \"#86efac\",\n tertiary: \"#22c55e\",\n tertiaryDark: \"#15803d\",\n grayLight: \"#e5e7eb\",\n gray: \"#9ca3af\",\n grayDark: \"#4b5563\",\n success: \"#84cc16\",\n error: \"#ef4444\",\n warning: \"#eab308\",\n info: \"#06b6d4\",\n dark: \"#000000\",\n light: \"#ffffff\",\n ...(customTheme.default ? (customTheme.default as Partial<Colors>) : {}),\n};\n\nconst colorsDark: Colors = {\n primaryLight: \"#9ca3af\",\n primary: \"#6b7280\",\n primaryDark: \"#374151\",\n secondaryLight: \"#ddd6fe\",\n secondary: \"#a78bfa\",\n secondaryDark: \"#7c3aed\",\n tertiaryLight: \"#6ee7b7\",\n tertiary: \"#10b981\",\n tertiaryDark: \"#065f46\",\n grayLight: \"#1a1a1a\",\n gray: \"#454444\",\n grayDark: \"#808080\",\n success: \"#84cc16\",\n error: \"#ef4444\",\n warning: \"#eab308\",\n info: \"#06b6d4\",\n dark: \"#ffffff\",\n light: \"#000000\",\n ...(customTheme.dark ? (customTheme.dark as Partial<Colors>) : {}),\n};\n\nconst shadows: Shadows = {\n xs: \"0px 4px 4px 0px rgba(18, 18, 18, 0.04), 0px 1px 3px 0px rgba(39, 41, 45, 0.02)\",\n sm: \"0px 4px 4px 0px rgba(18, 18, 18, 0.08), 0px 1px 3px 0px rgba(39, 41, 45, 0.04)\",\n md: \"0px 8px 8px 0px rgba(18, 18, 18, 0.16), 0px 2px 3px 0px rgba(39, 41, 45, 0.06)\",\n lg: \"0px 16px 24px 0px rgba(18, 18, 18, 0.20), 0px 2px 3px 0px rgba(39, 41, 45, 0.08)\",\n xl: \"0px 24px 32px 0px rgba(18, 18, 18, 0.24), 0px 2px 3px 0px rgba(39, 41, 45, 0.12)\",\n};\n\nconst shadowsDark: Shadows = {\n xs: \"0px 4px 4px 0px rgba(255, 255, 255, 0.04), 0px 1px 3px 0px rgba(255, 255, 255, 0.02)\",\n sm: \"0px 4px 4px 0px rgba(255, 255, 255, 0.08), 0px 1px 3px 0px rgba(255, 255, 255, 0.04)\",\n md: \"0px 8px 8px 0px rgba(255, 255, 255, 0.16), 0px 2px 3px 0px rgba(255, 255, 255, 0.06)\",\n lg: \"0px 16px 24px 0px rgba(255, 255, 255, 0.20), 0px 2px 3px 0px rgba(255, 255, 255, 0.08)\",\n xl: \"0px 24px 32px 0px rgba(255, 255, 255, 0.24), 0px 2px 3px 0px rgba(255, 255, 255, 0.12)\",\n};\n\nconst fonts: Fonts = {\n text: \"Inter\",\n head: \"Inter\",\n mono: \"Roboto Mono, monospace\",\n};\n\nconst fontSizes: FontSizes = {\n hero1: { xs: \"72px\", lg: \"128px\" },\n hero2: { xs: \"60px\", lg: \"96px\" },\n hero3: { xs: \"36px\", lg: \"72px\" },\n\n h1: { xs: \"40px\", lg: \"60px\" },\n h2: { xs: \"30px\", lg: \"36px\" },\n h3: { xs: \"28px\", lg: \"30px\" },\n h4: { xs: \"24px\", lg: \"26px\" },\n h5: { xs: \"18px\", lg: \"20px\" },\n h6: { xs: \"16px\", lg: \"18px\" },\n\n text: { xs: \"14px\", lg: \"16px\" },\n strong: { xs: \"14px\", lg: \"16px\" },\n small: { xs: \"12px\", lg: \"14px\" },\n\n blockquote: { xs: \"16px\", lg: \"18px\" },\n code: { xs: \"14px\", lg: \"16px\" },\n\n button: { xs: \"16px\", lg: \"16px\" },\n buttonBig: { xs: \"18px\", lg: \"18px\" },\n buttonSmall: { xs: \"14px\", lg: \"14px\" },\n\n input: { xs: \"16px\", lg: \"16px\" },\n inputBig: { xs: \"18px\", lg: \"18px\" },\n inputSmall: { xs: \"14px\", lg: \"14px\" },\n};\n\nconst lineHeights: LineHeights = {\n hero1: { xs: \"1.1\", lg: \"1.1\" },\n hero2: { xs: \"1.1\", lg: \"1.1\" },\n hero3: { xs: \"1.17\", lg: \"1.1\" },\n\n h1: { xs: \"1\", lg: \"1.07\" },\n h2: { xs: \"1.2\", lg: \"1.2\" },\n h3: { xs: \"1.3\", lg: \"1.5\" },\n h4: { xs: \"1.3\", lg: \"1.5\" },\n h5: { xs: \"1.6\", lg: \"1.5\" },\n h6: { xs: \"1.6\", lg: \"1.6\" },\n\n text: { xs: \"1.7\", lg: \"1.7\" },\n strong: { xs: \"1.7\", lg: \"1.7\" },\n small: { xs: \"1.7\", lg: \"1.7\" },\n\n blockquote: { xs: \"1.7\", lg: \"1.7\" },\n code: { xs: \"1.7\", lg: \"1.7\" },\n\n button: { xs: \"1\", lg: \"1\" },\n buttonBig: { xs: \"1\", lg: \"1\" },\n buttonSmall: { xs: \"1\", lg: \"1\" },\n\n input: { xs: \"1\", lg: \"1\" },\n inputBig: { xs: \"1\", lg: \"1\" },\n inputSmall: { xs: \"1\", lg: \"1\" },\n};\n\nexport const theme: Theme = {\n breakpoints,\n spacing,\n colors,\n shadows,\n fonts,\n fontSizes,\n lineHeights,\n isDark: false,\n};\n\nexport const themeDark: Theme = {\n breakpoints,\n spacing,\n colors: colorsDark,\n shadows: shadowsDark,\n fonts,\n fontSizes,\n lineHeights,\n isDark: true,\n};\n\ninterface Breakpoints<TNumber = number> {\n xs: TNumber;\n sm: TNumber;\n md: TNumber;\n lg: TNumber;\n xl: TNumber;\n xxl: TNumber;\n xxxl: TNumber;\n}\n\ninterface Spacing<TString = string> {\n maxWidth: { xs: TString; xxxl: TString };\n padding: { xs: TString; lg: TString };\n radius: { xs: TString; lg: TString; xl: TString };\n gridGap: { xs: TString; lg: TString };\n}\n\ninterface Colors<TString = string> {\n primaryLight: TString;\n primary: TString;\n primaryDark: TString;\n\n secondaryLight: TString;\n secondary: TString;\n secondaryDark: TString;\n\n tertiaryLight: TString;\n tertiary: TString;\n tertiaryDark: TString;\n\n grayLight: TString;\n gray: TString;\n grayDark: TString;\n\n success: TString;\n error: TString;\n warning: TString;\n info: TString;\n\n dark: TString;\n light: TString;\n}\n\ninterface Shadows<TString = string> {\n xs: TString;\n sm: TString;\n md: TString;\n lg: TString;\n xl: TString;\n}\n\ninterface Fonts<TString = string> {\n head: TString;\n text: TString;\n mono: TString;\n}\n\ninterface FontSizes<TString = string> {\n hero1: { xs: TString; lg: TString };\n hero2: { xs: TString; lg: TString };\n hero3: { xs: TString; lg: TString };\n\n h1: { xs: TString; lg: TString };\n h2: { xs: TString; lg: TString };\n h3: { xs: TString; lg: TString };\n h4: { xs: TString; lg: TString };\n h5: { xs: TString; lg: TString };\n h6: { xs: TString; lg: TString };\n\n text: { xs: TString; lg: TString };\n strong: { xs: TString; lg: TString };\n small: { xs: TString; lg: TString };\n\n blockquote: { xs: TString; lg: TString };\n code: { xs: TString; lg: TString };\n\n button: { xs: TString; lg: TString };\n buttonBig: { xs: TString; lg: TString };\n buttonSmall: { xs: TString; lg: TString };\n\n input: { xs: TString; lg: TString };\n inputBig: { xs: TString; lg: TString };\n inputSmall: { xs: TString; lg: TString };\n}\n\ninterface LineHeights<TString = string> {\n hero1: { xs: TString; lg: TString };\n hero2: { xs: TString; lg: TString };\n hero3: { xs: TString; lg: TString };\n\n h1: { xs: TString; lg: TString };\n h2: { xs: TString; lg: TString };\n h3: { xs: TString; lg: TString };\n h4: { xs: TString; lg: TString };\n h5: { xs: TString; lg: TString };\n h6: { xs: TString; lg: TString };\n\n text: { xs: TString; lg: TString };\n strong: { xs: TString; lg: TString };\n small: { xs: TString; lg: TString };\n\n blockquote: { xs: TString; lg: TString };\n code: { xs: TString; lg: TString };\n\n button: { xs: TString; lg: TString };\n buttonBig: { xs: TString; lg: TString };\n buttonSmall: { xs: TString; lg: TString };\n\n input: { xs: TString; lg: TString };\n inputBig: { xs: TString; lg: TString };\n inputSmall: { xs: TString; lg: TString };\n}\n\nexport interface Theme {\n breakpoints: Breakpoints;\n spacing: Spacing;\n colors: Colors;\n shadows: Shadows;\n fonts: Fonts;\n fontSizes: FontSizes;\n lineHeights: LineHeights;\n isDark: boolean;\n}\n";
3
+ export declare const themeTemplate = "\"use client\";\nimport customThemeJson from \"@/theme.json\";\n\n// Users can only override the brand palette via theme.json \u2014 semantic tokens\n// (accent, surface, etc.) are derived in GlobalStyles from the brand colors\n// and are not directly overridable.\ntype CustomColors = Omit<\n Colors,\n \"accent\" | \"accentStrong\" | \"accentMuted\" | \"surface\"\n>;\n\ninterface CustomTheme {\n default?: Partial<CustomColors>;\n dark?: Partial<CustomColors>;\n}\n\nconst customTheme = customThemeJson as CustomTheme;\n\nconst breakpoints: Breakpoints = {\n xs: 0,\n sm: 576,\n md: 768,\n lg: 992,\n xl: 1200,\n xxl: 1440,\n xxxl: 1920,\n};\n\nexport function mq(minWidth: keyof Breakpoints) {\n return `@media screen and (min-width: ${breakpoints[minWidth]}px)`;\n}\n\nconst spacing: Spacing = {\n maxWidth: { xs: \"1280px\", xxxl: \"1440px\" },\n padding: { xs: \"20px\", lg: \"40px\" },\n radius: { xs: \"6px\", lg: \"12px\", xl: \"30px\" },\n gridGap: { xs: \"20px\", lg: \"40px\" },\n};\n\n// Resolved hex palettes for each mode. GlobalStyles emits these as CSS\n// custom properties on :root and :root.dark. Components never read these\n// directly \u2014 they read `theme.colors.*` which resolves to var(--color-\u2026)\n// and lets the browser pick the right value based on the active class.\nexport const colorsLight: CustomColors = {\n primaryLight: \"#d1d5db\",\n primary: \"#556272\",\n primaryDark: \"#374151\",\n secondaryLight: \"#c4b5fd\",\n secondary: \"#8b5cf6\",\n secondaryDark: \"#5b21b6\",\n tertiaryLight: \"#86efac\",\n tertiary: \"#22c55e\",\n tertiaryDark: \"#15803d\",\n grayLight: \"#e5e7eb\",\n gray: \"#9ca3af\",\n grayDark: \"#4b5563\",\n success: \"#84cc16\",\n error: \"#ef4444\",\n warning: \"#eab308\",\n info: \"#06b6d4\",\n dark: \"#000000\",\n light: \"#ffffff\",\n ...(customTheme.default\n ? (customTheme.default as Partial<CustomColors>)\n : {}),\n};\n\nexport const colorsDark: CustomColors = {\n primaryLight: \"#9ca3af\",\n primary: \"#6b7280\",\n primaryDark: \"#374151\",\n secondaryLight: \"#ddd6fe\",\n secondary: \"#a78bfa\",\n secondaryDark: \"#7c3aed\",\n tertiaryLight: \"#6ee7b7\",\n tertiary: \"#10b981\",\n tertiaryDark: \"#065f46\",\n grayLight: \"#1a1a1a\",\n gray: \"#454444\",\n grayDark: \"#808080\",\n success: \"#84cc16\",\n error: \"#ef4444\",\n warning: \"#eab308\",\n info: \"#06b6d4\",\n dark: \"#ffffff\",\n light: \"#000000\",\n ...(customTheme.dark ? (customTheme.dark as Partial<CustomColors>) : {}),\n};\n\nexport const shadowsLight: Shadows = {\n xs: \"0px 4px 4px 0px rgba(18, 18, 18, 0.04), 0px 1px 3px 0px rgba(39, 41, 45, 0.02)\",\n sm: \"0px 4px 4px 0px rgba(18, 18, 18, 0.08), 0px 1px 3px 0px rgba(39, 41, 45, 0.04)\",\n md: \"0px 8px 8px 0px rgba(18, 18, 18, 0.16), 0px 2px 3px 0px rgba(39, 41, 45, 0.06)\",\n lg: \"0px 16px 24px 0px rgba(18, 18, 18, 0.20), 0px 2px 3px 0px rgba(39, 41, 45, 0.08)\",\n xl: \"0px 24px 32px 0px rgba(18, 18, 18, 0.24), 0px 2px 3px 0px rgba(39, 41, 45, 0.12)\",\n};\n\nexport const shadowsDark: Shadows = {\n xs: \"0px 4px 4px 0px rgba(255, 255, 255, 0.04), 0px 1px 3px 0px rgba(255, 255, 255, 0.02)\",\n sm: \"0px 4px 4px 0px rgba(255, 255, 255, 0.08), 0px 1px 3px 0px rgba(255, 255, 255, 0.04)\",\n md: \"0px 8px 8px 0px rgba(255, 255, 255, 0.16), 0px 2px 3px 0px rgba(255, 255, 255, 0.06)\",\n lg: \"0px 16px 24px 0px rgba(255, 255, 255, 0.20), 0px 2px 3px 0px rgba(255, 255, 255, 0.08)\",\n xl: \"0px 24px 32px 0px rgba(255, 255, 255, 0.24), 0px 2px 3px 0px rgba(255, 255, 255, 0.12)\",\n};\n\nconst fonts: Fonts = {\n text: \"Inter\",\n head: \"Inter\",\n mono: \"Roboto Mono, monospace\",\n};\n\nconst fontSizes: FontSizes = {\n hero1: { xs: \"72px\", lg: \"128px\" },\n hero2: { xs: \"60px\", lg: \"96px\" },\n hero3: { xs: \"36px\", lg: \"72px\" },\n\n h1: { xs: \"40px\", lg: \"60px\" },\n h2: { xs: \"30px\", lg: \"36px\" },\n h3: { xs: \"28px\", lg: \"30px\" },\n h4: { xs: \"24px\", lg: \"26px\" },\n h5: { xs: \"18px\", lg: \"20px\" },\n h6: { xs: \"16px\", lg: \"18px\" },\n\n text: { xs: \"14px\", lg: \"16px\" },\n strong: { xs: \"14px\", lg: \"16px\" },\n small: { xs: \"12px\", lg: \"14px\" },\n\n blockquote: { xs: \"16px\", lg: \"18px\" },\n code: { xs: \"14px\", lg: \"16px\" },\n\n button: { xs: \"16px\", lg: \"16px\" },\n buttonBig: { xs: \"18px\", lg: \"18px\" },\n buttonSmall: { xs: \"14px\", lg: \"14px\" },\n\n input: { xs: \"16px\", lg: \"16px\" },\n inputBig: { xs: \"18px\", lg: \"18px\" },\n inputSmall: { xs: \"14px\", lg: \"14px\" },\n};\n\nconst lineHeights: LineHeights = {\n hero1: { xs: \"1.1\", lg: \"1.1\" },\n hero2: { xs: \"1.1\", lg: \"1.1\" },\n hero3: { xs: \"1.17\", lg: \"1.1\" },\n\n h1: { xs: \"1\", lg: \"1.07\" },\n h2: { xs: \"1.2\", lg: \"1.2\" },\n h3: { xs: \"1.3\", lg: \"1.5\" },\n h4: { xs: \"1.3\", lg: \"1.5\" },\n h5: { xs: \"1.6\", lg: \"1.5\" },\n h6: { xs: \"1.6\", lg: \"1.6\" },\n\n text: { xs: \"1.7\", lg: \"1.7\" },\n strong: { xs: \"1.7\", lg: \"1.7\" },\n small: { xs: \"1.7\", lg: \"1.7\" },\n\n blockquote: { xs: \"1.7\", lg: \"1.7\" },\n code: { xs: \"1.7\", lg: \"1.7\" },\n\n button: { xs: \"1\", lg: \"1\" },\n buttonBig: { xs: \"1\", lg: \"1\" },\n buttonSmall: { xs: \"1\", lg: \"1\" },\n\n input: { xs: \"1\", lg: \"1\" },\n inputBig: { xs: \"1\", lg: \"1\" },\n inputSmall: { xs: \"1\", lg: \"1\" },\n};\n\n// Single theme object exported to consumers. Every color/shadow value is a\n// CSS custom-property reference; the browser resolves it against :root or\n// :root.dark depending on the active class. Components access these exactly\n// like before \u2014 `theme.colors.primary`, `theme.shadows.sm` \u2014 but the values\n// flip without React re-rendering.\nconst colors: Colors = {\n // Brand palette \u2014 directly customizable via theme.json's \"default\" / \"dark\".\n primaryLight: \"var(--color-primaryLight)\",\n primary: \"var(--color-primary)\",\n primaryDark: \"var(--color-primaryDark)\",\n secondaryLight: \"var(--color-secondaryLight)\",\n secondary: \"var(--color-secondary)\",\n secondaryDark: \"var(--color-secondaryDark)\",\n tertiaryLight: \"var(--color-tertiaryLight)\",\n tertiary: \"var(--color-tertiary)\",\n tertiaryDark: \"var(--color-tertiaryDark)\",\n grayLight: \"var(--color-grayLight)\",\n gray: \"var(--color-gray)\",\n grayDark: \"var(--color-grayDark)\",\n success: \"var(--color-success)\",\n error: \"var(--color-error)\",\n warning: \"var(--color-warning)\",\n info: \"var(--color-info)\",\n dark: \"var(--color-dark)\",\n light: \"var(--color-light)\",\n\n // Semantic tokens \u2014 derived in GlobalStyles to capture the most common\n // mode-aware swaps that components used to express via `theme.isDark ? A : B`.\n // They follow the same single-noun convention as the brand palette.\n accent: \"var(--color-accent)\",\n accentStrong: \"var(--color-accentStrong)\",\n accentMuted: \"var(--color-accentMuted)\",\n surface: \"var(--color-surface)\",\n};\n\nconst shadows: Shadows = {\n xs: \"var(--shadow-xs)\",\n sm: \"var(--shadow-sm)\",\n md: \"var(--shadow-md)\",\n lg: \"var(--shadow-lg)\",\n xl: \"var(--shadow-xl)\",\n};\n\nexport const theme: Theme = {\n breakpoints,\n spacing,\n colors,\n shadows,\n fonts,\n fontSizes,\n lineHeights,\n // Stub for type compatibility with cherry-styled-components' Theme. Mode\n // switching lives entirely in CSS vars flipped by :root.dark, so our own\n // code never branches on this. Cherry's internal `isDark ? \u2026 : \u2026` branches\n // (e.g. buttonStyles' filled-button text) are handled where they surface\n // by re-pinning to a mode-agnostic semantic token \u2014 see components/layout/Button.tsx.\n isDark: false,\n};\n\ninterface Breakpoints<TNumber = number> {\n xs: TNumber;\n sm: TNumber;\n md: TNumber;\n lg: TNumber;\n xl: TNumber;\n xxl: TNumber;\n xxxl: TNumber;\n}\n\ninterface Spacing<TString = string> {\n maxWidth: { xs: TString; xxxl: TString };\n padding: { xs: TString; lg: TString };\n radius: { xs: TString; lg: TString; xl: TString };\n gridGap: { xs: TString; lg: TString };\n}\n\n// Matches cherry-styled-components' Colors shape for the brand keys, plus a\n// handful of semantic tokens derived from the brand palette by GlobalStyles.\n// The brand keys (primaryLight\u2026light) are customizable via theme.json; the\n// semantic keys (accent*, surface) are derived and not overridable \u2014 see\n// CustomColors above.\ninterface Colors<TString = string> {\n primaryLight: TString;\n primary: TString;\n primaryDark: TString;\n\n secondaryLight: TString;\n secondary: TString;\n secondaryDark: TString;\n\n tertiaryLight: TString;\n tertiary: TString;\n tertiaryDark: TString;\n\n grayLight: TString;\n gray: TString;\n grayDark: TString;\n\n success: TString;\n error: TString;\n warning: TString;\n info: TString;\n\n dark: TString;\n light: TString;\n\n /** High-contrast accent text. Was: isDark ? primaryLight : primaryDark. */\n accent: TString;\n /** Slightly stronger accent (lightened/darkened by ~10%). */\n accentStrong: TString;\n /** Muted body text. Was: isDark ? grayDark : primary. */\n accentMuted: TString;\n /** Elevated surface color, white-ish in both modes. Was: isDark ? dark : light. */\n surface: TString;\n}\n\ninterface Shadows<TString = string> {\n xs: TString;\n sm: TString;\n md: TString;\n lg: TString;\n xl: TString;\n}\n\ninterface Fonts<TString = string> {\n head: TString;\n text: TString;\n mono: TString;\n}\n\ninterface FontSizes<TString = string> {\n hero1: { xs: TString; lg: TString };\n hero2: { xs: TString; lg: TString };\n hero3: { xs: TString; lg: TString };\n\n h1: { xs: TString; lg: TString };\n h2: { xs: TString; lg: TString };\n h3: { xs: TString; lg: TString };\n h4: { xs: TString; lg: TString };\n h5: { xs: TString; lg: TString };\n h6: { xs: TString; lg: TString };\n\n text: { xs: TString; lg: TString };\n strong: { xs: TString; lg: TString };\n small: { xs: TString; lg: TString };\n\n blockquote: { xs: TString; lg: TString };\n code: { xs: TString; lg: TString };\n\n button: { xs: TString; lg: TString };\n buttonBig: { xs: TString; lg: TString };\n buttonSmall: { xs: TString; lg: TString };\n\n input: { xs: TString; lg: TString };\n inputBig: { xs: TString; lg: TString };\n inputSmall: { xs: TString; lg: TString };\n}\n\ninterface LineHeights<TString = string> {\n hero1: { xs: TString; lg: TString };\n hero2: { xs: TString; lg: TString };\n hero3: { xs: TString; lg: TString };\n\n h1: { xs: TString; lg: TString };\n h2: { xs: TString; lg: TString };\n h3: { xs: TString; lg: TString };\n h4: { xs: TString; lg: TString };\n h5: { xs: TString; lg: TString };\n h6: { xs: TString; lg: TString };\n\n text: { xs: TString; lg: TString };\n strong: { xs: TString; lg: TString };\n small: { xs: TString; lg: TString };\n\n blockquote: { xs: TString; lg: TString };\n code: { xs: TString; lg: TString };\n\n button: { xs: TString; lg: TString };\n buttonBig: { xs: TString; lg: TString };\n buttonSmall: { xs: TString; lg: TString };\n\n input: { xs: TString; lg: TString };\n inputBig: { xs: TString; lg: TString };\n inputSmall: { xs: TString; lg: TString };\n}\n\nexport interface Theme {\n breakpoints: Breakpoints;\n spacing: Spacing;\n colors: Colors;\n shadows: Shadows;\n fonts: Fonts;\n fontSizes: FontSizes;\n lineHeights: LineHeights;\n isDark: boolean;\n}\n";