doccupine 0.0.95 → 0.0.96
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.
- package/dist/index.js +4 -1
- package/dist/lib/layout.js +30 -10
- package/dist/lib/structures.d.ts +1 -0
- package/dist/lib/structures.js +9 -6
- package/dist/templates/app/theme.d.ts +1 -1
- package/dist/templates/app/theme.js +36 -54
- package/dist/templates/components/SearchDocs.d.ts +1 -1
- package/dist/templates/components/SearchDocs.js +26 -17
- package/dist/templates/components/SideBar.d.ts +1 -1
- package/dist/templates/components/SideBar.js +3 -9
- package/dist/templates/components/layout/ActionBar.d.ts +1 -1
- package/dist/templates/components/layout/ActionBar.js +22 -27
- package/dist/templates/components/layout/CherryThemeProvider.d.ts +1 -1
- package/dist/templates/components/layout/CherryThemeProvider.js +26 -6
- package/dist/templates/components/layout/Code.d.ts +1 -1
- package/dist/templates/components/layout/Code.js +23 -38
- package/dist/templates/components/layout/DemoTheme.d.ts +1 -1
- package/dist/templates/components/layout/DemoTheme.js +45 -14
- package/dist/templates/components/layout/Header.d.ts +1 -1
- package/dist/templates/components/layout/Header.js +1 -1
- package/dist/templates/components/layout/SiteGate.d.ts +1 -1
- package/dist/templates/components/layout/SiteGate.js +3 -9
- package/dist/templates/package.js +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import path from "path";
|
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import matter from "gray-matter";
|
|
8
8
|
import chalk from "chalk";
|
|
9
|
-
import { appStructure, startingDocsStructure } from "./lib/structures.js";
|
|
9
|
+
import { appStructure, obsoleteFiles, startingDocsStructure, } from "./lib/structures.js";
|
|
10
10
|
import { rootLayoutTemplate, siteLayoutTemplate } from "./lib/layout.js";
|
|
11
11
|
import { ConfigManager } from "./lib/config-manager.js";
|
|
12
12
|
import { findAvailablePort, generateSlug, getFullSlug, escapeTemplateContent, } from "./lib/utils.js";
|
|
@@ -87,6 +87,9 @@ class MDXToNextJSGenerator {
|
|
|
87
87
|
// generateSectionIndexPages, so nothing here is user-authored. Config JSONs
|
|
88
88
|
// and other generated dirs live outside app/ and are untouched.
|
|
89
89
|
await fs.remove(path.join(this.outputDir, "app"));
|
|
90
|
+
// Drop files that earlier CLI versions generated but no longer exist in
|
|
91
|
+
// the template set, so upgraded projects don't keep stale copies.
|
|
92
|
+
await Promise.all(obsoleteFiles.map((file) => fs.remove(path.join(this.outputDir, file))));
|
|
90
93
|
const siteUrl = await this.loadSiteUrl();
|
|
91
94
|
const structure = {
|
|
92
95
|
...appStructure,
|
package/dist/lib/layout.js
CHANGED
|
@@ -62,9 +62,24 @@ function fontDeclLine(fontConfig) {
|
|
|
62
62
|
});`
|
|
63
63
|
: 'const font = Inter({ subsets: ["latin"] });';
|
|
64
64
|
}
|
|
65
|
-
// The inline blocking script that resolves dark mode before first paint
|
|
66
|
-
//
|
|
67
|
-
|
|
65
|
+
// The inline blocking script that resolves dark mode before first paint.
|
|
66
|
+
// Adds the "dark" class to <html> (so the CSS variables emitted by
|
|
67
|
+
// GlobalStyles paint dark immediately) and, on dark visits, injects a
|
|
68
|
+
// "#__theme-init" style that hides the body AND forces a dark html
|
|
69
|
+
// background: the server always renders with the light theme (pages are
|
|
70
|
+
// force-static, so no cookie read), and Cherry's ClientThemeProvider
|
|
71
|
+
// briefly re-syncs the "dark" class off the initial light theme during
|
|
72
|
+
// mount, so without the forced background the html would flash white for a
|
|
73
|
+
// frame. A second "__theme-transitions" style disables all transitions so
|
|
74
|
+
// the light-to-dark swap on mount snaps instantly instead of animating every
|
|
75
|
+
// element that declares a color transition. It can't live in "__theme-init":
|
|
76
|
+
// the provider removes that in the same frame the dark colors commit, so the
|
|
77
|
+
// browser would compute one light-to-dark style diff with transitions
|
|
78
|
+
// re-enabled and animate it anyway. Instead a MutationObserver waits for the
|
|
79
|
+
// provider to remove "__theme-init" (themeDark committed, body unhidden),
|
|
80
|
+
// forces a reflow, and drops the transition suppression two painted frames
|
|
81
|
+
// later — restoring transitions for user-initiated toggles.
|
|
82
|
+
const THEME_INIT_SCRIPT = `(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";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);var t=document.createElement("style");t.id="__theme-transitions";t.textContent="*,*::before,*::after{transition:none!important}";document.head.appendChild(t);var f=function(){var e=document.getElementById("__theme-transitions");if(!e)return;void document.body.offsetHeight;requestAnimationFrame(function(){requestAnimationFrame(function(){e.remove();});});};var o=new MutationObserver(function(){if(!document.getElementById("__theme-init")){o.disconnect();f();}});o.observe(document.head,{childList:true});setTimeout(f,10000);}else{document.documentElement.style.colorScheme="light";}}catch(e){}})();`;
|
|
68
83
|
/**
|
|
69
84
|
* Root layout ("app/layout.tsx"). Minimal shell: html/body, fonts, the theme
|
|
70
85
|
* provider stack, and (optionally) PostHog. It renders `children` directly so
|
|
@@ -78,7 +93,7 @@ export const rootLayoutTemplate = (fontConfig, analyticsEnabled = false) => {
|
|
|
78
93
|
return `import type { Metadata } from "next";
|
|
79
94
|
${fontImportLine(fontConfig)}
|
|
80
95
|
import { StyledComponentsRegistry } from "cherry-styled-components";
|
|
81
|
-
import { theme } from "@/app/theme";
|
|
96
|
+
import { theme, themeDark } from "@/app/theme";
|
|
82
97
|
import { CherryThemeProvider } from "@/components/layout/CherryThemeProvider";
|
|
83
98
|
import { config } from "@/utils/config";
|
|
84
99
|
${analyticsEnabled ? `import { PostHogProvider } from "@/components/PostHogProvider";\n` : ""}
|
|
@@ -118,10 +133,11 @@ export default function RootLayout({
|
|
|
118
133
|
return (
|
|
119
134
|
<html lang="en" suppressHydrationWarning>
|
|
120
135
|
<head>
|
|
121
|
-
{/* Resolves dark mode before first paint
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
136
|
+
{/* Resolves dark mode before first paint: adds the "dark" class to
|
|
137
|
+
<html> (flipping the CSS variables in GlobalStyles) and hides the
|
|
138
|
+
body until Cherry's ClientThemeProvider has swapped in themeDark
|
|
139
|
+
on mount — the server always renders the light theme since pages
|
|
140
|
+
are force-static. Inlined as a plain <script> (not next/script) so
|
|
125
141
|
it ships in the SSR HTML and runs synchronously before paint —
|
|
126
142
|
next/script with beforeInteractive is async in App Router and
|
|
127
143
|
would still show a flash. suppressHydrationWarning on <html>
|
|
@@ -137,9 +153,13 @@ export default function RootLayout({
|
|
|
137
153
|
<StyledComponentsRegistry>
|
|
138
154
|
${analyticsEnabled
|
|
139
155
|
? ` <PostHogProvider>
|
|
140
|
-
<CherryThemeProvider theme={theme}
|
|
156
|
+
<CherryThemeProvider theme={theme} themeDark={themeDark}>
|
|
157
|
+
{children}
|
|
158
|
+
</CherryThemeProvider>
|
|
141
159
|
</PostHogProvider>`
|
|
142
|
-
: ` <CherryThemeProvider theme={theme}
|
|
160
|
+
: ` <CherryThemeProvider theme={theme} themeDark={themeDark}>
|
|
161
|
+
{children}
|
|
162
|
+
</CherryThemeProvider>`}
|
|
143
163
|
</StyledComponentsRegistry>
|
|
144
164
|
</body>
|
|
145
165
|
</html>
|
package/dist/lib/structures.d.ts
CHANGED
package/dist/lib/structures.js
CHANGED
|
@@ -13,7 +13,6 @@ import { searchRoutesTemplate } from "../templates/app/api/search/route.js";
|
|
|
13
13
|
import { notFoundTemplate } from "../templates/app/not-found.js";
|
|
14
14
|
import { themeTemplate } from "../templates/app/theme.js";
|
|
15
15
|
import { chatTemplate } from "../templates/components/Chat.js";
|
|
16
|
-
import { clickOutsideTemplate } from "../templates/components/ClickOutside.js";
|
|
17
16
|
import { lockBodyScrollTemplate } from "../templates/components/LockBodyScroll.js";
|
|
18
17
|
import { docsTemplate } from "../templates/components/Docs.js";
|
|
19
18
|
import { docsSideBarTemplate } from "../templates/components/DocsSideBar.js";
|
|
@@ -32,7 +31,6 @@ import { buttonTemplate } from "../templates/components/layout/Button.js";
|
|
|
32
31
|
import { calloutTemplate } from "../templates/components/layout/Callout.js";
|
|
33
32
|
import { cardTemplate } from "../templates/components/layout/Card.js";
|
|
34
33
|
import { cherryThemeProviderTemplate } from "../templates/components/layout/CherryThemeProvider.js";
|
|
35
|
-
import { clientThemeProviderTemplate } from "../templates/components/layout/ClientThemeProvider.js";
|
|
36
34
|
import { colorSwatchTemplate } from "../templates/components/layout/ColorSwatch.js";
|
|
37
35
|
import { codeTemplate } from "../templates/components/layout/Code.js";
|
|
38
36
|
import { columnsTemplate } from "../templates/components/layout/Columns.js";
|
|
@@ -50,7 +48,6 @@ import { siteGateComponentTemplate } from "../templates/components/layout/SiteGa
|
|
|
50
48
|
import { staticLinksTemplate } from "../templates/components/layout/StaticLinks.js";
|
|
51
49
|
import { stepsTemplate } from "../templates/components/layout/Steps.js";
|
|
52
50
|
import { tabsTemplate } from "../templates/components/layout/Tabs.js";
|
|
53
|
-
import { themeToggleTemplate } from "../templates/components/layout/ThemeToggle.js";
|
|
54
51
|
import { typographyTemplate } from "../templates/components/layout/Typography.js";
|
|
55
52
|
import { updateTemplate } from "../templates/components/layout/Update.js";
|
|
56
53
|
import { searchServiceTemplate } from "../templates/services/search.js";
|
|
@@ -147,7 +144,6 @@ export const appStructure = {
|
|
|
147
144
|
"utils/rateLimit.ts": rateLimitTemplate,
|
|
148
145
|
"utils/config.ts": configTemplate,
|
|
149
146
|
"components/Chat.tsx": chatTemplate,
|
|
150
|
-
"components/ClickOutside.ts": clickOutsideTemplate,
|
|
151
147
|
"components/LockBodyScroll.ts": lockBodyScrollTemplate,
|
|
152
148
|
"components/Docs.tsx": docsTemplate,
|
|
153
149
|
"components/DocsSideBar.tsx": docsSideBarTemplate,
|
|
@@ -165,7 +161,6 @@ export const appStructure = {
|
|
|
165
161
|
"components/layout/Callout.tsx": calloutTemplate,
|
|
166
162
|
"components/layout/Card.tsx": cardTemplate,
|
|
167
163
|
"components/layout/CherryThemeProvider.tsx": cherryThemeProviderTemplate,
|
|
168
|
-
"components/layout/ClientThemeProvider.tsx": clientThemeProviderTemplate,
|
|
169
164
|
"components/layout/ColorSwatch.tsx": colorSwatchTemplate,
|
|
170
165
|
"components/layout/Code.tsx": codeTemplate,
|
|
171
166
|
"components/layout/Columns.tsx": columnsTemplate,
|
|
@@ -184,10 +179,18 @@ export const appStructure = {
|
|
|
184
179
|
"components/layout/StaticLinks.tsx": staticLinksTemplate,
|
|
185
180
|
"components/layout/Steps.tsx": stepsTemplate,
|
|
186
181
|
"components/layout/Tabs.tsx": tabsTemplate,
|
|
187
|
-
"components/layout/ThemeToggle.tsx": themeToggleTemplate,
|
|
188
182
|
"components/layout/Typography.ts": typographyTemplate,
|
|
189
183
|
"components/layout/Update.tsx": updateTemplate,
|
|
190
184
|
};
|
|
185
|
+
// Files generated by earlier CLI versions that no longer exist in the
|
|
186
|
+
// template set. Unlike app/ (wiped on every run), components/ and friends
|
|
187
|
+
// are only overwritten, so upgraded projects would otherwise keep stale
|
|
188
|
+
// copies around — failing lint or importing modules that are gone.
|
|
189
|
+
export const obsoleteFiles = [
|
|
190
|
+
"components/ClickOutside.ts",
|
|
191
|
+
"components/layout/ClientThemeProvider.tsx",
|
|
192
|
+
"components/layout/ThemeToggle.tsx",
|
|
193
|
+
];
|
|
191
194
|
export const startingDocsStructure = {
|
|
192
195
|
"accordion.mdx": accordionMdxTemplate,
|
|
193
196
|
"ai-assistant.mdx": aiAssistantMdxTemplate,
|
|
@@ -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\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";
|
|
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. They feed the literal `theme` /\n// `themeDark` objects below (swapped by Cherry's ClientThemeProvider) and\n// GlobalStyles, which also emits them as CSS custom properties on :root and\n// :root.dark for plain-CSS consumers. Custom theme.json overrides are merged\n// here, so both consumers pick them up automatically.\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// Builds the full Colors record for a mode: the resolved brand palette plus\n// the semantic tokens derived from it. Cherry's ClientThemeProvider swaps\n// whole theme objects on toggle, so components read literal values here \u2014\n// the CSS variables emitted by GlobalStyles stay in sync via the \"dark\"\n// class for plain-CSS consumers (e.g. Callout's / Code's :root.dark blocks).\n// Exported so DemoTheme can derive preset themes with the same rules.\nexport function buildColors(palette: CustomColors, isDark: boolean): Colors {\n const accent = isDark ? palette.primaryLight : palette.primaryDark;\n return {\n ...palette,\n accent,\n // accent shifted ~10% toward black (light mode) or white (dark mode).\n accentStrong: `color-mix(in srgb, ${accent} 90%, ${isDark ? \"white\" : \"black\"})`,\n accentMuted: isDark ? palette.grayDark : palette.primary,\n surface: isDark ? palette.dark : palette.light,\n };\n}\n\nexport const theme: Theme = {\n breakpoints,\n spacing,\n colors: buildColors(colorsLight, false),\n shadows: shadowsLight,\n fonts,\n fontSizes,\n lineHeights,\n isDark: false,\n};\n\nexport const themeDark: Theme = {\n breakpoints,\n spacing,\n colors: buildColors(colorsDark, true),\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\n// Matches cherry-styled-components' Colors shape for the brand keys, plus a\n// handful of semantic tokens derived from the brand palette by buildColors.\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";
|
|
@@ -39,10 +39,11 @@ const spacing: Spacing = {
|
|
|
39
39
|
gridGap: { xs: "20px", lg: "40px" },
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
// Resolved hex palettes for each mode.
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
42
|
+
// Resolved hex palettes for each mode. They feed the literal \`theme\` /
|
|
43
|
+
// \`themeDark\` objects below (swapped by Cherry's ClientThemeProvider) and
|
|
44
|
+
// GlobalStyles, which also emits them as CSS custom properties on :root and
|
|
45
|
+
// :root.dark for plain-CSS consumers. Custom theme.json overrides are merged
|
|
46
|
+
// here, so both consumers pick them up automatically.
|
|
46
47
|
export const colorsLight: CustomColors = {
|
|
47
48
|
primaryLight: "#d1d5db",
|
|
48
49
|
primary: "#556272",
|
|
@@ -167,65 +168,46 @@ const lineHeights: LineHeights = {
|
|
|
167
168
|
inputSmall: { xs: "1", lg: "1" },
|
|
168
169
|
};
|
|
169
170
|
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
primaryLight:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
gray: "var(--color-gray)",
|
|
188
|
-
grayDark: "var(--color-grayDark)",
|
|
189
|
-
success: "var(--color-success)",
|
|
190
|
-
error: "var(--color-error)",
|
|
191
|
-
warning: "var(--color-warning)",
|
|
192
|
-
info: "var(--color-info)",
|
|
193
|
-
dark: "var(--color-dark)",
|
|
194
|
-
light: "var(--color-light)",
|
|
195
|
-
|
|
196
|
-
// Semantic tokens — derived in GlobalStyles to capture the most common
|
|
197
|
-
// mode-aware swaps that components used to express via \`theme.isDark ? A : B\`.
|
|
198
|
-
// They follow the same single-noun convention as the brand palette.
|
|
199
|
-
accent: "var(--color-accent)",
|
|
200
|
-
accentStrong: "var(--color-accentStrong)",
|
|
201
|
-
accentMuted: "var(--color-accentMuted)",
|
|
202
|
-
surface: "var(--color-surface)",
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
const shadows: Shadows = {
|
|
206
|
-
xs: "var(--shadow-xs)",
|
|
207
|
-
sm: "var(--shadow-sm)",
|
|
208
|
-
md: "var(--shadow-md)",
|
|
209
|
-
lg: "var(--shadow-lg)",
|
|
210
|
-
xl: "var(--shadow-xl)",
|
|
211
|
-
};
|
|
171
|
+
// Builds the full Colors record for a mode: the resolved brand palette plus
|
|
172
|
+
// the semantic tokens derived from it. Cherry's ClientThemeProvider swaps
|
|
173
|
+
// whole theme objects on toggle, so components read literal values here —
|
|
174
|
+
// the CSS variables emitted by GlobalStyles stay in sync via the "dark"
|
|
175
|
+
// class for plain-CSS consumers (e.g. Callout's / Code's :root.dark blocks).
|
|
176
|
+
// Exported so DemoTheme can derive preset themes with the same rules.
|
|
177
|
+
export function buildColors(palette: CustomColors, isDark: boolean): Colors {
|
|
178
|
+
const accent = isDark ? palette.primaryLight : palette.primaryDark;
|
|
179
|
+
return {
|
|
180
|
+
...palette,
|
|
181
|
+
accent,
|
|
182
|
+
// accent shifted ~10% toward black (light mode) or white (dark mode).
|
|
183
|
+
accentStrong: \`color-mix(in srgb, \${accent} 90%, \${isDark ? "white" : "black"})\`,
|
|
184
|
+
accentMuted: isDark ? palette.grayDark : palette.primary,
|
|
185
|
+
surface: isDark ? palette.dark : palette.light,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
212
188
|
|
|
213
189
|
export const theme: Theme = {
|
|
214
190
|
breakpoints,
|
|
215
191
|
spacing,
|
|
216
|
-
colors,
|
|
217
|
-
shadows,
|
|
192
|
+
colors: buildColors(colorsLight, false),
|
|
193
|
+
shadows: shadowsLight,
|
|
218
194
|
fonts,
|
|
219
195
|
fontSizes,
|
|
220
196
|
lineHeights,
|
|
221
|
-
// Stub for type compatibility with cherry-styled-components' Theme. Mode
|
|
222
|
-
// switching lives entirely in CSS vars flipped by :root.dark, so our own
|
|
223
|
-
// code never branches on this. Cherry's internal \`isDark ? … : …\` branches
|
|
224
|
-
// (e.g. buttonStyles' filled-button text) are handled where they surface
|
|
225
|
-
// by re-pinning to a mode-agnostic semantic token — see components/layout/Button.tsx.
|
|
226
197
|
isDark: false,
|
|
227
198
|
};
|
|
228
199
|
|
|
200
|
+
export const themeDark: Theme = {
|
|
201
|
+
breakpoints,
|
|
202
|
+
spacing,
|
|
203
|
+
colors: buildColors(colorsDark, true),
|
|
204
|
+
shadows: shadowsDark,
|
|
205
|
+
fonts,
|
|
206
|
+
fontSizes,
|
|
207
|
+
lineHeights,
|
|
208
|
+
isDark: true,
|
|
209
|
+
};
|
|
210
|
+
|
|
229
211
|
interface Breakpoints<TNumber = number> {
|
|
230
212
|
xs: TNumber;
|
|
231
213
|
sm: TNumber;
|
|
@@ -244,7 +226,7 @@ interface Spacing<TString = string> {
|
|
|
244
226
|
}
|
|
245
227
|
|
|
246
228
|
// Matches cherry-styled-components' Colors shape for the brand keys, plus a
|
|
247
|
-
// handful of semantic tokens derived from the brand palette by
|
|
229
|
+
// handful of semantic tokens derived from the brand palette by buildColors.
|
|
248
230
|
// The brand keys (primaryLight…light) are customizable via theme.json; the
|
|
249
231
|
// semantic keys (accent*, surface) are derived and not overridable — see
|
|
250
232
|
// CustomColors above.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const searchDocsTemplate = "\"use client\";\nimport React, {\n createContext,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport dynamic from \"next/dynamic\";\nimport styled from \"styled-components\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { interactiveStyles } from \"@/components/layout/SharedStyled\";\nimport type { PageItem, MergedResult } from \"@/components/SearchModalContent\";\n\nconst SearchModalContent = dynamic(\n () =>\n import(\"@/components/SearchModalContent\").then(\n (mod) => mod.SearchModalContent,\n ),\n { ssr: false },\n);\n\ninterface SectionItem {\n label: string;\n slug: string;\n}\n\ninterface ContentHit {\n slug: string;\n snippet: string;\n}\n\ninterface SearchContextValue {\n openSearch: () => void;\n}\n\nconst SearchContext = createContext<SearchContextValue>({\n openSearch: () => {},\n});\n\nconst StyledKbd = styled.kbd<{ theme: Theme }>`\n font-size: 11px;\n font-family: inherit;\n background: ${({ theme }) => theme.colors.grayLight};\n color: ${({ theme }) => theme.colors.grayDark};\n padding: 2px 6px;\n border-radius: 4px;\n margin-left: auto;\n font-weight: 600;\n display: none;\n\n ${mq(\"lg\")} {\n display: initial;\n }\n`;\n\nconst StyledSearchButton = styled.button<{ theme: Theme }>`\n ${interactiveStyles};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n display: flex;\n align-items: center;\n gap: 6px;\n background: ${({ theme }) => theme.colors.light};\n color: ${({ theme }) => theme.colors.primary};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n padding: 7px 8px;\n font-family: inherit;\n cursor: pointer;\n\n ${mq(\"lg\")} {\n padding: 5px 8px;\n }\n\n & svg.lucide {\n color: inherit;\n }\n`;\n\nfunction SearchProvider({\n pages,\n sections,\n children,\n}: {\n pages: PageItem[];\n sections?: SectionItem[];\n children: React.ReactNode;\n}) {\n const [isVisible, setIsVisible] = useState(false);\n const [isClosing, setIsClosing] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n
|
|
1
|
+
export declare const searchDocsTemplate = "\"use client\";\nimport React, {\n createContext,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport dynamic from \"next/dynamic\";\nimport styled from \"styled-components\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { interactiveStyles } from \"@/components/layout/SharedStyled\";\nimport type { PageItem, MergedResult } from \"@/components/SearchModalContent\";\n\nconst SearchModalContent = dynamic(\n () =>\n import(\"@/components/SearchModalContent\").then(\n (mod) => mod.SearchModalContent,\n ),\n { ssr: false },\n);\n\ninterface SectionItem {\n label: string;\n slug: string;\n}\n\n// Stable empty array so the derived contentResults keeps the same identity\n// across renders when there are no hits (memo deps stay stable).\nconst EMPTY_RESULTS: ContentHit[] = [];\n\ninterface ContentHit {\n slug: string;\n snippet: string;\n}\n\ninterface SearchContextValue {\n openSearch: () => void;\n}\n\nconst SearchContext = createContext<SearchContextValue>({\n openSearch: () => {},\n});\n\nconst StyledKbd = styled.kbd<{ theme: Theme }>`\n font-size: 11px;\n font-family: inherit;\n background: ${({ theme }) => theme.colors.grayLight};\n color: ${({ theme }) => theme.colors.grayDark};\n padding: 2px 6px;\n border-radius: 4px;\n margin-left: auto;\n font-weight: 600;\n display: none;\n\n ${mq(\"lg\")} {\n display: initial;\n }\n`;\n\nconst StyledSearchButton = styled.button<{ theme: Theme }>`\n ${interactiveStyles};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n display: flex;\n align-items: center;\n gap: 6px;\n background: ${({ theme }) => theme.colors.light};\n color: ${({ theme }) => theme.colors.primary};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n padding: 7px 8px;\n font-family: inherit;\n cursor: pointer;\n\n ${mq(\"lg\")} {\n padding: 5px 8px;\n }\n\n & svg.lucide {\n color: inherit;\n }\n`;\n\nfunction SearchProvider({\n pages,\n sections,\n children,\n}: {\n pages: PageItem[];\n sections?: SectionItem[];\n children: React.ReactNode;\n}) {\n const [isVisible, setIsVisible] = useState(false);\n const [isClosing, setIsClosing] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n // Latest completed content search, keyed by the query that produced it.\n // contentResults and isSearching are derived from it at render time, so\n // the debounced-search effect never sets state synchronously (which would\n // trigger cascading renders \u2014 react-hooks/set-state-in-effect).\n const [fetched, setFetched] = useState<{\n q: string;\n results: ContentHit[];\n } | null>(null);\n const resultsRef = useRef<HTMLUListElement>(null);\n const closingRef = useRef(false);\n const abortRef = useRef<AbortController | null>(null);\n const debounceRef = useRef<ReturnType<typeof setTimeout>>(null);\n const router = useRouter();\n\n const sectionLabels = useMemo(() => {\n const map: Record<string, string> = {};\n sections?.forEach((s) => {\n map[s.slug] = s.label;\n });\n return map;\n }, [sections]);\n\n const openSearch = useCallback(() => {\n closingRef.current = false;\n setIsClosing(false);\n setIsVisible(true);\n }, []);\n\n const closeSearch = useCallback(() => {\n closingRef.current = true;\n setIsClosing(true);\n if (abortRef.current) abortRef.current.abort();\n if (debounceRef.current) clearTimeout(debounceRef.current);\n }, []);\n\n const handleCloseAnimationEnd = useCallback(() => {\n if (!closingRef.current) return;\n closingRef.current = false;\n setIsVisible(false);\n setIsClosing(false);\n setQuery(\"\");\n setActiveIndex(0);\n setFetched(null);\n }, []);\n\n const trimmedQuery = query.trim();\n const contentResults =\n trimmedQuery.length >= 2 && fetched?.q === trimmedQuery\n ? fetched.results\n : EMPTY_RESULTS;\n const isSearching = trimmedQuery.length >= 2 && fetched?.q !== trimmedQuery;\n\n // Instant title/description filtering\n const titleFiltered = useMemo(() => {\n if (!query.trim()) return pages;\n const q = query.toLowerCase();\n return pages.filter(\n (p) =>\n p.title.toLowerCase().includes(q) ||\n p.description?.toLowerCase().includes(q),\n );\n }, [pages, query]);\n\n // Merge title matches with content matches\n const merged = useMemo<MergedResult[]>(() => {\n if (!query.trim()) {\n return pages.map((p) => ({ page: p }));\n }\n\n const titleMatchSlugs = new Set(titleFiltered.map((p) => p.slug));\n const titleMatches: MergedResult[] = titleFiltered.map((p) => {\n const hit = contentResults.find((cr) => cr.slug === p.slug);\n return { page: p, snippet: hit?.snippet };\n });\n\n const pageMap = new Map(pages.map((p) => [p.slug, p]));\n const contentOnly: MergedResult[] = [];\n for (const cr of contentResults) {\n if (!titleMatchSlugs.has(cr.slug)) {\n const page = pageMap.get(cr.slug);\n if (page) {\n contentOnly.push({ page, snippet: cr.snippet });\n }\n }\n }\n\n return [...titleMatches, ...contentOnly];\n }, [pages, query, titleFiltered, contentResults]);\n\n // Debounced content search. State updates happen only inside the timeout's\n // async callback; short queries need no reset because the derived values\n // above ignore results whose query no longer matches.\n useEffect(() => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (abortRef.current) abortRef.current.abort();\n\n const q = query.trim();\n if (q.length < 2) return;\n\n debounceRef.current = setTimeout(async () => {\n const controller = new AbortController();\n abortRef.current = controller;\n try {\n const res = await fetch(\n `/api/search?q=${encodeURIComponent(q)}&limit=15`,\n { signal: controller.signal },\n );\n if (!res.ok) throw new Error(\"Search failed\");\n const data = await res.json();\n setFetched({ q, results: data.results ?? [] });\n } catch (err: unknown) {\n if (err instanceof DOMException && err.name === \"AbortError\") return;\n setFetched({ q, results: [] });\n }\n }, 300);\n\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n };\n }, [query]);\n\n const navigate = useCallback(\n (slug: string) => {\n closeSearch();\n router.push(`/${slug}`);\n },\n [closeSearch, router],\n );\n\n // Global Cmd+K / Ctrl+K listener\n const isVisibleRef = useRef(false);\n\n useEffect(() => {\n isVisibleRef.current = isVisible;\n }, [isVisible]);\n\n useEffect(() => {\n function handleKeyDown(e: KeyboardEvent) {\n if ((e.metaKey || e.ctrlKey) && e.key === \"k\") {\n e.preventDefault();\n if (isVisibleRef.current) {\n closeSearch();\n } else {\n openSearch();\n }\n }\n }\n document.addEventListener(\"keydown\", handleKeyDown);\n return () => document.removeEventListener(\"keydown\", handleKeyDown);\n }, [closeSearch, openSearch]);\n\n // Scroll active item into view\n useEffect(() => {\n if (!resultsRef.current) return;\n const active = resultsRef.current.children[activeIndex] as HTMLElement;\n active?.scrollIntoView({ block: \"nearest\" });\n }, [activeIndex]);\n\n function handleKeyDown(e: React.KeyboardEvent) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActiveIndex((i) => (i < merged.length - 1 ? i + 1 : 0));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActiveIndex((i) => (i > 0 ? i - 1 : merged.length - 1));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n if (merged[activeIndex]) {\n navigate(merged[activeIndex].page.slug);\n }\n } else if (e.key === \"Escape\") {\n closeSearch();\n }\n }\n\n return (\n <SearchContext.Provider value={{ openSearch }}>\n {children}\n {isVisible && (\n <SearchModalContent\n isClosing={isClosing}\n closeSearch={closeSearch}\n onCloseAnimationEnd={handleCloseAnimationEnd}\n query={query}\n setQuery={setQuery}\n activeIndex={activeIndex}\n setActiveIndex={setActiveIndex}\n resultsRef={resultsRef}\n onKeyDown={handleKeyDown}\n merged={merged}\n sectionLabels={sectionLabels}\n isSearching={isSearching}\n navigate={navigate}\n />\n )}\n </SearchContext.Provider>\n );\n}\n\nexport {\n SearchProvider,\n SearchContext,\n StyledKbd as SearchKbd,\n StyledSearchButton,\n};\n";
|
|
@@ -27,6 +27,10 @@ interface SectionItem {
|
|
|
27
27
|
slug: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Stable empty array so the derived contentResults keeps the same identity
|
|
31
|
+
// across renders when there are no hits (memo deps stay stable).
|
|
32
|
+
const EMPTY_RESULTS: ContentHit[] = [];
|
|
33
|
+
|
|
30
34
|
interface ContentHit {
|
|
31
35
|
slug: string;
|
|
32
36
|
snippet: string;
|
|
@@ -91,8 +95,14 @@ function SearchProvider({
|
|
|
91
95
|
const [isClosing, setIsClosing] = useState(false);
|
|
92
96
|
const [query, setQuery] = useState("");
|
|
93
97
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
// Latest completed content search, keyed by the query that produced it.
|
|
99
|
+
// contentResults and isSearching are derived from it at render time, so
|
|
100
|
+
// the debounced-search effect never sets state synchronously (which would
|
|
101
|
+
// trigger cascading renders — react-hooks/set-state-in-effect).
|
|
102
|
+
const [fetched, setFetched] = useState<{
|
|
103
|
+
q: string;
|
|
104
|
+
results: ContentHit[];
|
|
105
|
+
} | null>(null);
|
|
96
106
|
const resultsRef = useRef<HTMLUListElement>(null);
|
|
97
107
|
const closingRef = useRef(false);
|
|
98
108
|
const abortRef = useRef<AbortController | null>(null);
|
|
@@ -127,10 +137,16 @@ function SearchProvider({
|
|
|
127
137
|
setIsClosing(false);
|
|
128
138
|
setQuery("");
|
|
129
139
|
setActiveIndex(0);
|
|
130
|
-
|
|
131
|
-
setIsSearching(false);
|
|
140
|
+
setFetched(null);
|
|
132
141
|
}, []);
|
|
133
142
|
|
|
143
|
+
const trimmedQuery = query.trim();
|
|
144
|
+
const contentResults =
|
|
145
|
+
trimmedQuery.length >= 2 && fetched?.q === trimmedQuery
|
|
146
|
+
? fetched.results
|
|
147
|
+
: EMPTY_RESULTS;
|
|
148
|
+
const isSearching = trimmedQuery.length >= 2 && fetched?.q !== trimmedQuery;
|
|
149
|
+
|
|
134
150
|
// Instant title/description filtering
|
|
135
151
|
const titleFiltered = useMemo(() => {
|
|
136
152
|
if (!query.trim()) return pages;
|
|
@@ -168,19 +184,16 @@ function SearchProvider({
|
|
|
168
184
|
return [...titleMatches, ...contentOnly];
|
|
169
185
|
}, [pages, query, titleFiltered, contentResults]);
|
|
170
186
|
|
|
171
|
-
// Debounced content search
|
|
187
|
+
// Debounced content search. State updates happen only inside the timeout's
|
|
188
|
+
// async callback; short queries need no reset because the derived values
|
|
189
|
+
// above ignore results whose query no longer matches.
|
|
172
190
|
useEffect(() => {
|
|
173
191
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
174
192
|
if (abortRef.current) abortRef.current.abort();
|
|
175
193
|
|
|
176
194
|
const q = query.trim();
|
|
177
|
-
if (q.length < 2)
|
|
178
|
-
setContentResults([]);
|
|
179
|
-
setIsSearching(false);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
195
|
+
if (q.length < 2) return;
|
|
182
196
|
|
|
183
|
-
setIsSearching(true);
|
|
184
197
|
debounceRef.current = setTimeout(async () => {
|
|
185
198
|
const controller = new AbortController();
|
|
186
199
|
abortRef.current = controller;
|
|
@@ -191,14 +204,10 @@ function SearchProvider({
|
|
|
191
204
|
);
|
|
192
205
|
if (!res.ok) throw new Error("Search failed");
|
|
193
206
|
const data = await res.json();
|
|
194
|
-
|
|
207
|
+
setFetched({ q, results: data.results ?? [] });
|
|
195
208
|
} catch (err: unknown) {
|
|
196
209
|
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
197
|
-
|
|
198
|
-
} finally {
|
|
199
|
-
if (!controller.signal.aborted) {
|
|
200
|
-
setIsSearching(false);
|
|
201
|
-
}
|
|
210
|
+
setFetched({ q, results: [] });
|
|
202
211
|
}
|
|
203
212
|
}, 300);
|
|
204
213
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const sideBarTemplate = "\"use client\";\nimport { useContext, useState
|
|
1
|
+
export declare const sideBarTemplate = "\"use client\";\nimport { useContext, useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { Flex, Space, ThemeToggle } from \"cherry-styled-components\";\nimport {\n DocsSidebar,\n SectionBarContext,\n StyledSidebar,\n StyledSidebarList,\n StyledSidebarListItem,\n StyledStrong,\n StyledSidebarListItemLink,\n StyledSidebarFooter,\n StyleMobileBar,\n StyledMobileBurger,\n} from \"@/components/layout/DocsComponents\";\nimport { useLockBodyScroll } from \"@/components/LockBodyScroll\";\n\ntype NavItem = {\n label: string;\n links: NavItemLink[];\n};\n\ntype NavItemLink = {\n slug: string;\n title: string;\n};\n\ninterface SideBarProps {\n result: NavItem[];\n}\n\nfunction SideBar({ result }: SideBarProps) {\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n const hasSectionBar = useContext(SectionBarContext);\n const pathname = usePathname();\n\n useLockBodyScroll(isMobileMenuOpen);\n\n return (\n <DocsSidebar>\n <StyleMobileBar\n onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}\n $isActive={isMobileMenuOpen}\n aria-label={\n isMobileMenuOpen ? \"Close navigation menu\" : \"Open navigation menu\"\n }\n aria-expanded={isMobileMenuOpen}\n >\n <StyledMobileBurger $isActive={isMobileMenuOpen} />\n </StyleMobileBar>\n\n <StyledSidebar\n $isActive={isMobileMenuOpen}\n $hasSectionBar={hasSectionBar}\n >\n {result &&\n result.map((item: NavItem, index: number) => {\n return (\n <StyledSidebarList key={index}>\n <StyledSidebarListItem>\n <StyledStrong>{item.label}</StyledStrong>{\" \"}\n </StyledSidebarListItem>\n <li>\n <Space $size={20} />\n </li>\n {item.links &&\n item.links.map((link: NavItemLink, indexChild: number) => {\n return (\n <StyledSidebarListItem key={indexChild}>\n <StyledSidebarListItemLink\n href={`/${link.slug}`}\n $isActive={pathname === `/${link.slug}`}\n onClick={() => setIsMobileMenuOpen(false)}\n >\n {link.title}\n </StyledSidebarListItemLink>\n </StyledSidebarListItem>\n );\n })}\n <li aria-hidden=\"true\">\n <Space $size={20} />\n </li>\n </StyledSidebarList>\n );\n })}\n <StyledSidebarFooter>\n <Flex $xsJustifyContent=\"flex-start\" $lgJustifyContent=\"flex-end\">\n <ThemeToggle />\n </Flex>\n </StyledSidebarFooter>\n </StyledSidebar>\n </DocsSidebar>\n );\n}\n\nexport { SideBar };\n";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const sideBarTemplate = `"use client";
|
|
2
|
-
import { useContext, useState
|
|
2
|
+
import { useContext, useState } from "react";
|
|
3
3
|
import { usePathname } from "next/navigation";
|
|
4
|
-
import { Flex, Space } from "cherry-styled-components";
|
|
4
|
+
import { Flex, Space, ThemeToggle } from "cherry-styled-components";
|
|
5
5
|
import {
|
|
6
6
|
DocsSidebar,
|
|
7
7
|
SectionBarContext,
|
|
@@ -14,10 +14,6 @@ import {
|
|
|
14
14
|
StyleMobileBar,
|
|
15
15
|
StyledMobileBurger,
|
|
16
16
|
} from "@/components/layout/DocsComponents";
|
|
17
|
-
import {
|
|
18
|
-
ToggleTheme,
|
|
19
|
-
ToggleThemeLoading,
|
|
20
|
-
} from "@/components/layout/ThemeToggle";
|
|
21
17
|
import { useLockBodyScroll } from "@/components/LockBodyScroll";
|
|
22
18
|
|
|
23
19
|
type NavItem = {
|
|
@@ -90,9 +86,7 @@ function SideBar({ result }: SideBarProps) {
|
|
|
90
86
|
})}
|
|
91
87
|
<StyledSidebarFooter>
|
|
92
88
|
<Flex $xsJustifyContent="flex-start" $lgJustifyContent="flex-end">
|
|
93
|
-
<
|
|
94
|
-
<ToggleTheme />
|
|
95
|
-
</Suspense>
|
|
89
|
+
<ThemeToggle />
|
|
96
90
|
</Flex>
|
|
97
91
|
</StyledSidebarFooter>
|
|
98
92
|
</StyledSidebar>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const actionBarTemplate = "\"use client\";\nimport { useContext, useState } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { mq, Theme } from \"@/app/theme\";\nimport {
|
|
1
|
+
export declare const actionBarTemplate = "\"use client\";\nimport { useContext, useState } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { mq, Theme } from \"@/app/theme\";\nimport {\n interactiveStyles,\n resetButton,\n Textarea,\n} from \"cherry-styled-components\";\nimport { SectionBarContext } from \"@/components/layout/DocsComponents\";\nimport { StyledSmallButton } from \"@/components/layout/SharedStyled\";\n\ninterface ActionBarProps {\n children: React.ReactNode;\n content: string;\n}\n\nconst StyledActionBar = styled.div<{\n theme: Theme;\n $isChatOpen?: boolean;\n}>`\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n left: 0;\n padding: 12px 0;\n display: flex;\n justify-content: space-between;\n width: 100%;\n max-width: 640px;\n margin: auto;\n transition: all 0.3s ease;\n\n ${mq(\"lg\")} {\n padding: 12px 0;\n }\n`;\n\nconst StyledActionBarContent = styled.div`\n margin: auto 0;\n`;\n\nconst StyledCopyButton = styled(StyledSmallButton)<{\n theme: Theme;\n $copied: boolean;\n}>`\n border: solid 1px\n ${({ theme, $copied }) =>\n $copied ? theme.colors.success : theme.colors.grayLight};\n color: ${({ theme, $copied }) =>\n $copied ? theme.colors.success : theme.colors.primary};\n\n & svg.lucide {\n color: ${({ theme, $copied }) =>\n $copied ? theme.colors.success : theme.colors.primary};\n }\n`;\n\n// Mirrors the geometry and interaction of Cherry's ThemeToggle so the two\n// pills look identical side by side: interactiveStyles supplies the border\n// highlight + focus/active rings (no scale effect), and the space-between\n// layout with 6px padding puts each 16px icon's center exactly where the\n// 24px knob's center sits in its resting (left: 2px) and active\n// (translateX(26px)) positions.\nconst StyledToggle = styled.button<{ theme: Theme; $isActive?: boolean }>`\n ${resetButton}\n ${interactiveStyles}\n width: 56px;\n height: 30px;\n border-radius: 30px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0 6px;\n position: relative;\n margin: auto 0;\n background: ${({ theme }) => theme.colors.light};\n border-color: ${({ theme }) => theme.colors.grayLight};\n\n &::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 20%, transparent)`};\n transition: all 0.3s ease;\n z-index: 1;\n ${({ $isActive }) =>\n !$isActive &&\n css`\n transform: translateX(26px);\n `}\n }\n\n & svg {\n width: 16px;\n height: 16px;\n object-fit: contain;\n transition: all 0.3s ease;\n position: relative;\n z-index: 2;\n }\n\n & svg[stroke] {\n stroke: ${({ theme }) => theme.colors.primary};\n }\n\n &:hover svg[stroke] {\n stroke: ${({ theme }) => theme.colors.accent};\n }\n`;\n\nconst StyledContent = styled.div<{\n theme: Theme;\n $hasSectionBar?: boolean;\n}>`\n padding-top: 20px;\n transition: all 0.3s ease;\n\n & textarea {\n max-width: 640px;\n margin: auto;\n width: 100%;\n height: 100%;\n min-height: calc(\n 100vh - ${({ $hasSectionBar }) => ($hasSectionBar ? 202 : 160)}px\n );\n\n ${mq(\"lg\")} {\n min-height: calc(100vh - 159px);\n }\n }\n`;\n\nfunction ActionBar({ children, content }: ActionBarProps) {\n const [isView, setIsView] = useState(true);\n const [copied, setCopied] = useState(false);\n const hasSectionBar = useContext(SectionBarContext);\n\n const handleCopyContent = async () => {\n try {\n await navigator.clipboard.writeText(content);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (err) {\n console.error(\"Failed to copy:\", err);\n }\n };\n\n return (\n <>\n <StyledActionBar>\n <StyledCopyButton onClick={handleCopyContent} $copied={copied}>\n {copied ? (\n <>\n <Icon name=\"check\" size={16} />\n Copied!\n </>\n ) : (\n <>\n <Icon name=\"copy\" size={16} />\n Copy content\n </>\n )}\n </StyledCopyButton>\n <StyledActionBarContent>\n <StyledToggle\n onClick={() => setIsView(!isView)}\n aria-label=\"Toggle View\"\n $isActive={isView}\n >\n <Icon name=\"Eye\" />\n <Icon name=\"CodeXml\" />\n </StyledToggle>\n </StyledActionBarContent>\n </StyledActionBar>\n {isView && (\n <StyledContent $hasSectionBar={hasSectionBar}>{children}</StyledContent>\n )}\n {!isView && (\n <StyledContent $hasSectionBar={hasSectionBar}>\n <Textarea defaultValue={content} $fullWidth />\n </StyledContent>\n )}\n </>\n );\n}\n\nexport { ActionBar };\n";
|
|
@@ -3,7 +3,11 @@ import { useContext, useState } from "react";
|
|
|
3
3
|
import styled, { css } from "styled-components";
|
|
4
4
|
import { Icon } from "@/components/layout/Icon";
|
|
5
5
|
import { mq, Theme } from "@/app/theme";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
interactiveStyles,
|
|
8
|
+
resetButton,
|
|
9
|
+
Textarea,
|
|
10
|
+
} from "cherry-styled-components";
|
|
7
11
|
import { SectionBarContext } from "@/components/layout/DocsComponents";
|
|
8
12
|
import { StyledSmallButton } from "@/components/layout/SharedStyled";
|
|
9
13
|
|
|
@@ -51,23 +55,32 @@ const StyledCopyButton = styled(StyledSmallButton)<{
|
|
|
51
55
|
}
|
|
52
56
|
\`;
|
|
53
57
|
|
|
58
|
+
// Mirrors the geometry and interaction of Cherry's ThemeToggle so the two
|
|
59
|
+
// pills look identical side by side: interactiveStyles supplies the border
|
|
60
|
+
// highlight + focus/active rings (no scale effect), and the space-between
|
|
61
|
+
// layout with 6px padding puts each 16px icon's center exactly where the
|
|
62
|
+
// 24px knob's center sits in its resting (left: 2px) and active
|
|
63
|
+
// (translateX(26px)) positions.
|
|
54
64
|
const StyledToggle = styled.button<{ theme: Theme; $isActive?: boolean }>\`
|
|
55
65
|
\${resetButton}
|
|
66
|
+
\${interactiveStyles}
|
|
56
67
|
width: 56px;
|
|
57
|
-
height:
|
|
68
|
+
height: 30px;
|
|
58
69
|
border-radius: 30px;
|
|
59
70
|
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: space-between;
|
|
73
|
+
padding: 0 6px;
|
|
60
74
|
position: relative;
|
|
61
75
|
margin: auto 0;
|
|
62
|
-
transform: scale(1);
|
|
63
76
|
background: \${({ theme }) => theme.colors.light};
|
|
64
|
-
border:
|
|
77
|
+
border-color: \${({ theme }) => theme.colors.grayLight};
|
|
65
78
|
|
|
66
79
|
&::after {
|
|
67
80
|
content: "";
|
|
68
81
|
position: absolute;
|
|
69
|
-
top:
|
|
70
|
-
left:
|
|
82
|
+
top: 2px;
|
|
83
|
+
left: 2px;
|
|
71
84
|
width: 24px;
|
|
72
85
|
height: 24px;
|
|
73
86
|
border-radius: 50%;
|
|
@@ -78,7 +91,7 @@ const StyledToggle = styled.button<{ theme: Theme; $isActive?: boolean }>\`
|
|
|
78
91
|
\${({ $isActive }) =>
|
|
79
92
|
!$isActive &&
|
|
80
93
|
css\`
|
|
81
|
-
transform: translateX(
|
|
94
|
+
transform: translateX(26px);
|
|
82
95
|
\`}
|
|
83
96
|
}
|
|
84
97
|
|
|
@@ -86,35 +99,17 @@ const StyledToggle = styled.button<{ theme: Theme; $isActive?: boolean }>\`
|
|
|
86
99
|
width: 16px;
|
|
87
100
|
height: 16px;
|
|
88
101
|
object-fit: contain;
|
|
89
|
-
margin: auto;
|
|
90
102
|
transition: all 0.3s ease;
|
|
91
103
|
position: relative;
|
|
92
104
|
z-index: 2;
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
& .lucide-eye {
|
|
96
|
-
transform: translateX(1px);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
& .lucide-code-xml {
|
|
100
|
-
transform: translateX(-1px);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
107
|
& svg[stroke] {
|
|
104
108
|
stroke: \${({ theme }) => theme.colors.primary};
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
&:hover {
|
|
108
|
-
|
|
109
|
-
color: \${({ theme }) => theme.colors.accent};
|
|
110
|
-
|
|
111
|
-
& svg[stroke] {
|
|
112
|
-
stroke: \${({ theme }) => theme.colors.accent};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
&:active {
|
|
117
|
-
transform: scale(0.97);
|
|
111
|
+
&:hover svg[stroke] {
|
|
112
|
+
stroke: \${({ theme }) => theme.colors.accent};
|
|
118
113
|
}
|
|
119
114
|
\`;
|
|
120
115
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const cherryThemeProviderTemplate = "import React from \"react\";\nimport { Theme } from \"@/app/theme\";\nimport {
|
|
1
|
+
export declare const cherryThemeProviderTemplate = "import React from \"react\";\nimport { ClientThemeProvider } from \"cherry-styled-components\";\nimport { Theme } from \"@/app/theme\";\nimport { GlobalStyles } from \"@/components/layout/GlobalStyles\";\n\n/**\n * Wraps Cherry's ClientThemeProvider, which swaps theme/themeDark on toggle,\n * persists the choice to the `theme` cookie + localStorage, and keeps the\n * \"dark\" class on <html> in sync. Deliberately reads NO request data\n * (cookies/headers) so pages stay statically renderable \u2014 the blocking\n * theme-init script in the root layout sets the \"dark\" class before paint\n * and hides the body on dark visits; the provider reconciles against the\n * cookie on mount and removes the hiding style. $globalStyles is off because\n * this app ships its own GlobalStyles; $themeColor tracks \"light\", the\n * html/body background.\n */\nfunction CherryThemeProvider({\n children,\n theme,\n themeDark,\n}: {\n children: React.ReactNode;\n theme: Theme;\n themeDark: Theme;\n}) {\n return (\n <ClientThemeProvider\n theme={theme}\n themeDark={themeDark}\n $globalStyles={false}\n $themeColor=\"light\"\n >\n <GlobalStyles />\n {children}\n </ClientThemeProvider>\n );\n}\n\nexport { CherryThemeProvider };\n";
|
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
export const cherryThemeProviderTemplate = `import React from "react";
|
|
2
|
+
import { ClientThemeProvider } from "cherry-styled-components";
|
|
2
3
|
import { Theme } from "@/app/theme";
|
|
3
|
-
import {
|
|
4
|
+
import { GlobalStyles } from "@/components/layout/GlobalStyles";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Wraps Cherry's ClientThemeProvider, which swaps theme/themeDark on toggle,
|
|
8
|
+
* persists the choice to the \`theme\` cookie + localStorage, and keeps the
|
|
9
|
+
* "dark" class on <html> in sync. Deliberately reads NO request data
|
|
10
|
+
* (cookies/headers) so pages stay statically renderable — the blocking
|
|
11
|
+
* theme-init script in the root layout sets the "dark" class before paint
|
|
12
|
+
* and hides the body on dark visits; the provider reconciles against the
|
|
13
|
+
* cookie on mount and removes the hiding style. $globalStyles is off because
|
|
14
|
+
* this app ships its own GlobalStyles; $themeColor tracks "light", the
|
|
15
|
+
* html/body background.
|
|
16
|
+
*/
|
|
9
17
|
function CherryThemeProvider({
|
|
10
18
|
children,
|
|
11
19
|
theme,
|
|
20
|
+
themeDark,
|
|
12
21
|
}: {
|
|
13
22
|
children: React.ReactNode;
|
|
14
23
|
theme: Theme;
|
|
24
|
+
themeDark: Theme;
|
|
15
25
|
}) {
|
|
16
|
-
return
|
|
26
|
+
return (
|
|
27
|
+
<ClientThemeProvider
|
|
28
|
+
theme={theme}
|
|
29
|
+
themeDark={themeDark}
|
|
30
|
+
$globalStyles={false}
|
|
31
|
+
$themeColor="light"
|
|
32
|
+
>
|
|
33
|
+
<GlobalStyles />
|
|
34
|
+
{children}
|
|
35
|
+
</ClientThemeProvider>
|
|
36
|
+
);
|
|
17
37
|
}
|
|
18
38
|
|
|
19
39
|
export { CherryThemeProvider };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const codeTemplate = "\"use client\";\nimport { useState, useCallback, useMemo } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport {
|
|
1
|
+
export declare const codeTemplate = "\"use client\";\nimport { useState, useCallback, useMemo } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport {\n interactiveStyles,\n resetButton,\n styledCode,\n} from \"cherry-styled-components\";\nimport { Theme } from \"@/app/theme\";\nimport { unified } from \"unified\";\nimport rehypeParse from \"rehype-parse\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport rehypeStringify from \"rehype-stringify\";\nimport { Icon } from \"@/components/layout/Icon\";\n\ninterface CodeProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"theme\"\n> {\n code: string;\n language?: string;\n theme?: Theme;\n}\n\nconst CodeWrapper = styled.span<{ theme: Theme }>`\n position: relative;\n z-index: 2;\n display: block;\n width: 100%;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n border: solid 1px rgba(0, 0, 0, 0.1);\n\n :root.dark & {\n border-color: rgba(255, 255, 255, 0.2);\n }\n`;\n\n/* Code block uses a fixed GitHub-style palette in both modes. Independent of\n theme.json so syntax highlighting stays legible regardless of brand colors.\n Dark variants live in :root.dark & blocks so the swap happens via the\n active <html> class with no re-render. */\nconst TopBar = styled.div<{ theme: Theme }>`\n background: #f6f8fa;\n border-top-left-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-top-right-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom: solid 1px rgba(0, 0, 0, 0.1);\n height: 33px;\n width: 100%;\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 5px;\n padding: 0 10px;\n\n :root.dark & {\n background: #0d1117;\n border-bottom-color: rgba(255, 255, 255, 0.1);\n }\n`;\n\nconst DotsContainer = styled.div`\n display: flex;\n gap: 5px;\n`;\n\nconst Dot = styled.span<{ theme: Theme }>`\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.1);\n\n :root.dark & {\n background: rgba(255, 255, 255, 0.1);\n }\n`;\n\n/* Icon-only copy button. interactiveStyles supplies the border highlight on\n hover plus the focus/active rings (no scale effect); the GitHub-style\n copied/base colors stay fixed like the rest of the code block. The dark\n block re-declares the hover border because its higher-specificity\n :root.dark & border-color would otherwise override the mixin's hover. */\nconst CopyButton = styled.button<{ theme: Theme; $copied: boolean }>`\n ${resetButton}\n ${interactiveStyles}\n background: ${({ $copied }) =>\n $copied ? \"rgba(45, 164, 78, 0.1)\" : \"transparent\"};\n border-color: ${({ $copied }) => ($copied ? \"#2da44e\" : \"rgba(0, 0, 0, 0.1)\")};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n padding: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-right: -6px;\n\n & svg.lucide {\n color: ${({ $copied }) => ($copied ? \"#2da44e\" : \"#57606a\")};\n }\n\n :root.dark & {\n background: ${({ $copied }) =>\n $copied ? \"rgba(126, 231, 135, 0.2)\" : \"transparent\"};\n border-color: ${({ $copied }) =>\n $copied ? \"#7ee787\" : \"rgba(255, 255, 255, 0.1)\"};\n\n & svg.lucide {\n color: ${({ $copied }) => ($copied ? \"#7ee787\" : \"#c9d1d9\")};\n }\n\n &:hover {\n border-color: ${({ theme }) => theme.colors.primary};\n }\n }\n`;\n\n/* GitHub Light syntax highlighting by default; GitHub Dark in :root.dark.\n Browser resolves which rule wins based on the active <html> class with no\n JS or React re-render involved. */\nconst lightSyntaxHighlight = css`\n & .hljs {\n color: #24292f;\n background: #ffffff;\n }\n & .hljs-doctag,\n & .hljs-keyword,\n & .hljs-meta .hljs-keyword,\n & .hljs-template-tag,\n & .hljs-template-variable,\n & .hljs-type,\n & .hljs-variable.language_ {\n color: #cf222e;\n }\n & .hljs-title,\n & .hljs-title.class_,\n & .hljs-title.class_.inherited__,\n & .hljs-title.function_ {\n color: #8250df;\n }\n & .hljs-attr,\n & .hljs-attribute,\n & .hljs-literal,\n & .hljs-meta,\n & .hljs-number,\n & .hljs-operator,\n & .hljs-selector-attr,\n & .hljs-selector-class,\n & .hljs-selector-id,\n & .hljs-variable {\n color: #0550ae;\n }\n & .hljs-meta .hljs-string,\n & .hljs-regexp,\n & .hljs-string {\n color: #0a3069;\n }\n & .hljs-built_in,\n & .hljs-symbol {\n color: #953800;\n }\n & .hljs-code,\n & .hljs-comment,\n & .hljs-formula {\n color: #6e7781;\n }\n & .hljs-name,\n & .hljs-quote,\n & .hljs-selector-pseudo,\n & .hljs-selector-tag {\n color: #116329;\n }\n & .hljs-subst {\n color: #24292f;\n }\n & .hljs-section {\n color: #0550ae;\n font-weight: 700;\n }\n & .hljs-bullet {\n color: #953800;\n }\n & .hljs-emphasis {\n color: #24292f;\n font-style: italic;\n }\n & .hljs-strong {\n color: #24292f;\n font-weight: 700;\n }\n & .hljs-addition {\n color: #116329;\n background-color: #dafbe1;\n }\n & .hljs-deletion {\n color: #82071e;\n background-color: #ffebe9;\n }\n`;\n\nconst darkSyntaxHighlight = css`\n & .hljs {\n color: #c9d1d9;\n background: #0d1117;\n }\n & .hljs-doctag,\n & .hljs-keyword,\n & .hljs-meta .hljs-keyword,\n & .hljs-template-tag,\n & .hljs-template-variable,\n & .hljs-type,\n & .hljs-variable.language_ {\n color: #ff7b72;\n }\n & .hljs-title,\n & .hljs-title.class_,\n & .hljs-title.class_.inherited__,\n & .hljs-title.function_ {\n color: #d2a8ff;\n }\n & .hljs-attr,\n & .hljs-attribute,\n & .hljs-literal,\n & .hljs-meta,\n & .hljs-number,\n & .hljs-operator,\n & .hljs-selector-attr,\n & .hljs-selector-class,\n & .hljs-selector-id,\n & .hljs-variable {\n color: #79c0ff;\n }\n & .hljs-meta .hljs-string,\n & .hljs-regexp,\n & .hljs-string {\n color: #a5d6ff;\n }\n & .hljs-built_in,\n & .hljs-symbol {\n color: #ffa657;\n }\n & .hljs-code,\n & .hljs-comment,\n & .hljs-formula {\n color: #8b949e;\n }\n & .hljs-name,\n & .hljs-quote,\n & .hljs-selector-pseudo,\n & .hljs-selector-tag {\n color: #7ee787;\n }\n & .hljs-subst {\n color: #c9d1d9;\n }\n & .hljs-section {\n color: #1f6feb;\n font-weight: 700;\n }\n & .hljs-bullet {\n color: #f2cc60;\n }\n & .hljs-emphasis {\n color: #c9d1d9;\n font-style: italic;\n }\n & .hljs-strong {\n color: #c9d1d9;\n font-weight: 700;\n }\n & .hljs-addition {\n color: #aff5b4;\n background-color: #033a16;\n }\n & .hljs-deletion {\n color: #ffdcd7;\n background-color: #67060c;\n }\n`;\n\nconst Body = styled.div<{ theme: Theme }>`\n background: #ffffff;\n border-bottom-left-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom-right-radius: ${({ theme }) => theme.spacing.radius.lg};\n color: #24292f;\n padding: 20px;\n font-family: ${({ theme }) => theme.fonts.mono};\n text-align: left;\n overflow-x: auto;\n overflow-y: auto;\n max-height: calc(100dvh - 400px);\n ${({ theme }) => styledCode(theme)};\n ${lightSyntaxHighlight};\n\n :root.dark & {\n background: #0d1117;\n color: #ffffff;\n ${darkSyntaxHighlight};\n }\n`;\n\nconst escapeHtml = (unsafe: string): string => {\n return unsafe\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n};\n\nconst sanitizeLanguage = (lang: string): string =>\n lang.replace(/[^a-zA-Z0-9_-]/g, \"\");\n\nconst highlightCode = (code: string, language: string): string => {\n const escapedCode = escapeHtml(code);\n const safeLang = sanitizeLanguage(language);\n const result = unified()\n .use(rehypeParse, { fragment: true })\n .use(rehypeHighlight, {\n detect: true,\n ignoreMissing: true,\n })\n .use(rehypeStringify)\n .processSync(\n `<pre><code class=\"language-${safeLang}\">${escapedCode}</code></pre>`,\n );\n\n return String(result);\n};\n\nfunction Code({ code, language = \"javascript\", theme, className }: CodeProps) {\n const [copied, setCopied] = useState(false);\n const highlightedCode = useMemo(\n () => highlightCode(code, language),\n [code, language],\n );\n\n const handleCopy = useCallback(async () => {\n try {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (err) {\n console.error(\"Failed to copy code:\", err);\n }\n }, [code]);\n\n return (\n <CodeWrapper\n className={`${className ?? \"\"} code-wrapper`.trim()}\n theme={theme}\n >\n <TopBar theme={theme}>\n <DotsContainer>\n <Dot theme={theme} />\n <Dot theme={theme} />\n <Dot theme={theme} />\n </DotsContainer>\n <CopyButton\n onClick={handleCopy}\n $copied={copied}\n theme={theme}\n aria-label={copied ? \"Copied\" : \"Copy code\"}\n >\n <Icon name={copied ? \"check\" : \"copy\"} size={12} />\n </CopyButton>\n </TopBar>\n <Body\n dangerouslySetInnerHTML={{ __html: highlightedCode }}\n theme={theme}\n className=\"code-wrapper-body\"\n />\n </CodeWrapper>\n );\n}\n\nexport { Code };\n";
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export const codeTemplate = `"use client";
|
|
2
2
|
import { useState, useCallback, useMemo } from "react";
|
|
3
3
|
import styled, { css } from "styled-components";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
interactiveStyles,
|
|
6
|
+
resetButton,
|
|
7
|
+
styledCode,
|
|
8
|
+
} from "cherry-styled-components";
|
|
5
9
|
import { Theme } from "@/app/theme";
|
|
6
10
|
import { unified } from "unified";
|
|
7
11
|
import rehypeParse from "rehype-parse";
|
|
@@ -70,54 +74,40 @@ const Dot = styled.span<{ theme: Theme }>\`
|
|
|
70
74
|
}
|
|
71
75
|
\`;
|
|
72
76
|
|
|
77
|
+
/* Icon-only copy button. interactiveStyles supplies the border highlight on
|
|
78
|
+
hover plus the focus/active rings (no scale effect); the GitHub-style
|
|
79
|
+
copied/base colors stay fixed like the rest of the code block. The dark
|
|
80
|
+
block re-declares the hover border because its higher-specificity
|
|
81
|
+
:root.dark & border-color would otherwise override the mixin's hover. */
|
|
73
82
|
const CopyButton = styled.button<{ theme: Theme; $copied: boolean }>\`
|
|
83
|
+
\${resetButton}
|
|
84
|
+
\${interactiveStyles}
|
|
74
85
|
background: \${({ $copied }) =>
|
|
75
86
|
$copied ? "rgba(45, 164, 78, 0.1)" : "transparent"};
|
|
76
|
-
border:
|
|
77
|
-
\${({ $copied }) => ($copied ? "#2da44e" : "rgba(0, 0, 0, 0.1)")};
|
|
78
|
-
color: \${({ $copied }) => ($copied ? "#2da44e" : "#57606a")};
|
|
87
|
+
border-color: \${({ $copied }) => ($copied ? "#2da44e" : "rgba(0, 0, 0, 0.1)")};
|
|
79
88
|
border-radius: \${({ theme }) => theme.spacing.radius.xs};
|
|
80
|
-
padding: 4px
|
|
81
|
-
font-size: 12px;
|
|
82
|
-
font-family: \${({ theme }) => theme.fonts.mono};
|
|
83
|
-
cursor: pointer;
|
|
84
|
-
transition: all 0.2s ease;
|
|
89
|
+
padding: 4px;
|
|
85
90
|
display: flex;
|
|
86
91
|
align-items: center;
|
|
87
|
-
|
|
92
|
+
justify-content: center;
|
|
88
93
|
margin-right: -6px;
|
|
89
94
|
|
|
90
95
|
& svg.lucide {
|
|
91
96
|
color: \${({ $copied }) => ($copied ? "#2da44e" : "#57606a")};
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
&:hover {
|
|
95
|
-
background: \${({ $copied }) =>
|
|
96
|
-
$copied ? "rgba(45, 164, 78, 0.2)" : "rgba(0, 0, 0, 0.05)"};
|
|
97
|
-
border-color: \${({ $copied }) =>
|
|
98
|
-
$copied ? "#2da44e" : "rgba(0, 0, 0, 0.2)"};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
&:active {
|
|
102
|
-
transform: scale(0.95);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
99
|
:root.dark & {
|
|
106
100
|
background: \${({ $copied }) =>
|
|
107
101
|
$copied ? "rgba(126, 231, 135, 0.2)" : "transparent"};
|
|
108
102
|
border-color: \${({ $copied }) =>
|
|
109
103
|
$copied ? "#7ee787" : "rgba(255, 255, 255, 0.1)"};
|
|
110
|
-
color: \${({ $copied }) => ($copied ? "#7ee787" : "#c9d1d9")};
|
|
111
104
|
|
|
112
105
|
& svg.lucide {
|
|
113
106
|
color: \${({ $copied }) => ($copied ? "#7ee787" : "#c9d1d9")};
|
|
114
107
|
}
|
|
115
108
|
|
|
116
109
|
&:hover {
|
|
117
|
-
|
|
118
|
-
$copied ? "rgba(126, 231, 135, 0.3)" : "rgba(255, 255, 255, 0.1)"};
|
|
119
|
-
border-color: \${({ $copied }) =>
|
|
120
|
-
$copied ? "#7ee787" : "rgba(255, 255, 255, 0.2)"};
|
|
110
|
+
border-color: \${({ theme }) => theme.colors.primary};
|
|
121
111
|
}
|
|
122
112
|
}
|
|
123
113
|
\`;
|
|
@@ -363,18 +353,13 @@ function Code({ code, language = "javascript", theme, className }: CodeProps) {
|
|
|
363
353
|
<Dot theme={theme} />
|
|
364
354
|
<Dot theme={theme} />
|
|
365
355
|
</DotsContainer>
|
|
366
|
-
<CopyButton
|
|
367
|
-
{
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
<>
|
|
374
|
-
<Icon name="copy" size={12} />
|
|
375
|
-
<span>Copy</span>
|
|
376
|
-
</>
|
|
377
|
-
)}
|
|
356
|
+
<CopyButton
|
|
357
|
+
onClick={handleCopy}
|
|
358
|
+
$copied={copied}
|
|
359
|
+
theme={theme}
|
|
360
|
+
aria-label={copied ? "Copied" : "Copy code"}
|
|
361
|
+
>
|
|
362
|
+
<Icon name={copied ? "check" : "copy"} size={12} />
|
|
378
363
|
</CopyButton>
|
|
379
364
|
</TopBar>
|
|
380
365
|
<Body
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const demoThemeTemplate = "\"use client\";\nimport {
|
|
1
|
+
export declare const demoThemeTemplate = "\"use client\";\nimport { useContext } from \"react\";\nimport { useTheme } from \"styled-components\";\nimport { ThemeContext } from \"cherry-styled-components\";\nimport {\n buildColors,\n colorsLight,\n colorsDark,\n theme,\n themeDark,\n Theme,\n} from \"@/app/theme\";\nimport { Button } from \"@/components/layout/Button\";\nimport { Columns } from \"@/components/layout/Columns\";\n\ninterface DemoThemeProps {\n variant: \"purple\" | \"green\" | \"yellow\";\n}\n\ntype Palette = Record<string, string>;\n\n// Each preset is light + dark color overrides, mirroring what a user editing\n// theme.json would see. Applying one does two things: swaps in a theme object\n// rebuilt with the overridden palette (Cherry's setTheme, which also persists\n// the mode), and mirrors the overrides onto the --color-* CSS custom\n// properties on <html> for the plain-CSS consumers that read variables\n// (e.g. Callout's and Code's :root.dark blocks). Reset clears both so the\n// defaults take over again.\nconst PRESETS: Record<\n DemoThemeProps[\"variant\"],\n { light: Palette; dark: Palette; label: string }\n> = {\n purple: {\n label: \"Purple\",\n light: {\n primaryLight: \"#c4b5fd\",\n primary: \"#8b5cf6\",\n primaryDark: \"#5b21b6\",\n },\n dark: {\n primaryLight: \"#ddd6fe\",\n primary: \"#a78bfa\",\n primaryDark: \"#7c3aed\",\n },\n },\n green: {\n label: \"Green\",\n light: {\n primaryLight: \"#86efac\",\n primary: \"#22c55e\",\n primaryDark: \"#15803d\",\n },\n dark: {\n primaryLight: \"#6ee7b7\",\n primary: \"#10b981\",\n primaryDark: \"#065f46\",\n },\n },\n yellow: {\n label: \"Yellow\",\n light: {\n primaryLight: \"#fbbf24\",\n primary: \"#f59e0b\",\n primaryDark: \"#d97706\",\n },\n dark: {\n primaryLight: \"#fed7aa\",\n primary: \"#fb923c\",\n primaryDark: \"#ea580c\",\n },\n },\n};\n\nconst PRESET_KEYS = [\"primaryLight\", \"primary\", \"primaryDark\"] as const;\n\nfunction applyPresetVars(palette: Palette) {\n const root = document.documentElement;\n for (const key of PRESET_KEYS) {\n if (palette[key]) root.style.setProperty(`--color-${key}`, palette[key]);\n }\n}\n\nfunction clearPresetVars() {\n const root = document.documentElement;\n for (const key of PRESET_KEYS) {\n root.style.removeProperty(`--color-${key}`);\n }\n}\n\n// Rebuilds a mode's theme with the preset palette merged in, so the semantic\n// tokens (accent, accentStrong, \u2026) derive from the preset colors exactly like\n// they would from a theme.json override.\nfunction presetTheme(palette: Palette, dark: boolean): Theme {\n const base = dark ? themeDark : theme;\n const paletteBase = dark ? colorsDark : colorsLight;\n return {\n ...base,\n colors: buildColors({ ...paletteBase, ...palette }, dark),\n };\n}\n\nfunction DemoTheme({ variant }: DemoThemeProps) {\n const { setTheme } = useContext(ThemeContext);\n const activeTheme = useTheme() as Theme;\n const preset = PRESETS[variant];\n\n if (!preset) {\n return (\n <Columns cols={2}>\n <Button\n onClick={() => {\n clearPresetVars();\n setTheme(activeTheme.isDark ? themeDark : theme);\n }}\n fullWidth\n >\n Reset to Default\n </Button>\n </Columns>\n );\n }\n\n return (\n <Columns cols={2}>\n <Button\n onClick={() => {\n applyPresetVars(preset.light);\n setTheme(presetTheme(preset.light, false));\n }}\n icon=\"sun\"\n fullWidth\n >\n Demo {preset.label} Light\n </Button>\n <Button\n outline\n onClick={() => {\n applyPresetVars(preset.dark);\n setTheme(presetTheme(preset.dark, true));\n }}\n icon=\"moon-star\"\n fullWidth\n >\n Demo {preset.label} Dark\n </Button>\n </Columns>\n );\n}\n\nexport { DemoTheme };\n";
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export const demoThemeTemplate = `"use client";
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
import { useTheme } from "styled-components";
|
|
4
|
+
import { ThemeContext } from "cherry-styled-components";
|
|
5
|
+
import {
|
|
6
|
+
buildColors,
|
|
7
|
+
colorsLight,
|
|
8
|
+
colorsDark,
|
|
9
|
+
theme,
|
|
10
|
+
themeDark,
|
|
11
|
+
Theme,
|
|
12
|
+
} from "@/app/theme";
|
|
2
13
|
import { Button } from "@/components/layout/Button";
|
|
3
|
-
import { useThemeOverride } from "@/components/layout/ClientThemeProvider";
|
|
4
14
|
import { Columns } from "@/components/layout/Columns";
|
|
5
15
|
|
|
6
16
|
interface DemoThemeProps {
|
|
@@ -9,11 +19,13 @@ interface DemoThemeProps {
|
|
|
9
19
|
|
|
10
20
|
type Palette = Record<string, string>;
|
|
11
21
|
|
|
12
|
-
// Each preset is light + dark color overrides
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
22
|
+
// Each preset is light + dark color overrides, mirroring what a user editing
|
|
23
|
+
// theme.json would see. Applying one does two things: swaps in a theme object
|
|
24
|
+
// rebuilt with the overridden palette (Cherry's setTheme, which also persists
|
|
25
|
+
// the mode), and mirrors the overrides onto the --color-* CSS custom
|
|
26
|
+
// properties on <html> for the plain-CSS consumers that read variables
|
|
27
|
+
// (e.g. Callout's and Code's :root.dark blocks). Reset clears both so the
|
|
28
|
+
// defaults take over again.
|
|
17
29
|
const PRESETS: Record<
|
|
18
30
|
DemoThemeProps["variant"],
|
|
19
31
|
{ light: Palette; dark: Palette; label: string }
|
|
@@ -61,28 +73,47 @@ const PRESETS: Record<
|
|
|
61
73
|
|
|
62
74
|
const PRESET_KEYS = ["primaryLight", "primary", "primaryDark"] as const;
|
|
63
75
|
|
|
64
|
-
function
|
|
76
|
+
function applyPresetVars(palette: Palette) {
|
|
65
77
|
const root = document.documentElement;
|
|
66
78
|
for (const key of PRESET_KEYS) {
|
|
67
79
|
if (palette[key]) root.style.setProperty(\`--color-\${key}\`, palette[key]);
|
|
68
80
|
}
|
|
69
81
|
}
|
|
70
82
|
|
|
71
|
-
function
|
|
83
|
+
function clearPresetVars() {
|
|
72
84
|
const root = document.documentElement;
|
|
73
85
|
for (const key of PRESET_KEYS) {
|
|
74
86
|
root.style.removeProperty(\`--color-\${key}\`);
|
|
75
87
|
}
|
|
76
88
|
}
|
|
77
89
|
|
|
90
|
+
// Rebuilds a mode's theme with the preset palette merged in, so the semantic
|
|
91
|
+
// tokens (accent, accentStrong, …) derive from the preset colors exactly like
|
|
92
|
+
// they would from a theme.json override.
|
|
93
|
+
function presetTheme(palette: Palette, dark: boolean): Theme {
|
|
94
|
+
const base = dark ? themeDark : theme;
|
|
95
|
+
const paletteBase = dark ? colorsDark : colorsLight;
|
|
96
|
+
return {
|
|
97
|
+
...base,
|
|
98
|
+
colors: buildColors({ ...paletteBase, ...palette }, dark),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
78
102
|
function DemoTheme({ variant }: DemoThemeProps) {
|
|
79
|
-
const {
|
|
103
|
+
const { setTheme } = useContext(ThemeContext);
|
|
104
|
+
const activeTheme = useTheme() as Theme;
|
|
80
105
|
const preset = PRESETS[variant];
|
|
81
106
|
|
|
82
107
|
if (!preset) {
|
|
83
108
|
return (
|
|
84
109
|
<Columns cols={2}>
|
|
85
|
-
<Button
|
|
110
|
+
<Button
|
|
111
|
+
onClick={() => {
|
|
112
|
+
clearPresetVars();
|
|
113
|
+
setTheme(activeTheme.isDark ? themeDark : theme);
|
|
114
|
+
}}
|
|
115
|
+
fullWidth
|
|
116
|
+
>
|
|
86
117
|
Reset to Default
|
|
87
118
|
</Button>
|
|
88
119
|
</Columns>
|
|
@@ -93,8 +124,8 @@ function DemoTheme({ variant }: DemoThemeProps) {
|
|
|
93
124
|
<Columns cols={2}>
|
|
94
125
|
<Button
|
|
95
126
|
onClick={() => {
|
|
96
|
-
|
|
97
|
-
|
|
127
|
+
applyPresetVars(preset.light);
|
|
128
|
+
setTheme(presetTheme(preset.light, false));
|
|
98
129
|
}}
|
|
99
130
|
icon="sun"
|
|
100
131
|
fullWidth
|
|
@@ -104,8 +135,8 @@ function DemoTheme({ variant }: DemoThemeProps) {
|
|
|
104
135
|
<Button
|
|
105
136
|
outline
|
|
106
137
|
onClick={() => {
|
|
107
|
-
|
|
108
|
-
|
|
138
|
+
applyPresetVars(preset.dark);
|
|
139
|
+
setTheme(presetTheme(preset.dark, true));
|
|
109
140
|
}}
|
|
110
141
|
icon="moon-star"
|
|
111
142
|
fullWidth
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const headerTemplate = "\"use client\";\nimport React from \"react\";\nimport { useCallback, useContext, useRef, useState } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport Link from \"next/link\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { useOnClickOutside } from \"
|
|
1
|
+
export declare const headerTemplate = "\"use client\";\nimport React from \"react\";\nimport { useCallback, useContext, useRef, useState } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport Link from \"next/link\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { useOnClickOutside } from \"cherry-styled-components\";\nimport { Search } from \"lucide-react\";\nimport { Logo } from \"@/components/layout/Pictograms\";\nimport { ChatContext, ChatButtonCTA } from \"@/components/Chat\";\nimport {\n SearchContext,\n SearchKbd,\n StyledSearchButton,\n} from \"@/components/SearchDocs\";\nimport themeJson from \"@/theme.json\";\n\nconst customThemeJson = themeJson as typeof themeJson & {\n logo?: { dark: string; light: string };\n};\n\nconst StyledHeader = styled.header<{ theme: Theme; $hasChildren: boolean }>`\n position: sticky;\n top: 0;\n margin: 0;\n z-index: 1000;\n width: 100%;\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n\n ${({ $hasChildren }) =>\n !$hasChildren &&\n css`\n ${mq(\"lg\")} {\n padding-bottom: 16px;\n padding-top: 16px;\n }\n `}\n\n &::before,\n &::after {\n display: block;\n content: \"\";\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n background: ${({ theme }) => theme.colors.light};\n z-index: -2;\n }\n\n &::after {\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 5%, transparent)`};\n z-index: -1;\n }\n\n & .logo {\n display: flex;\n\n & svg,\n & img {\n margin: auto;\n height: auto;\n width: fit-content;\n min-width: fit-content;\n max-width: 182px;\n max-height: 30px;\n\n & path[fill] {\n fill: ${({ theme }) => theme.colors.primary};\n }\n }\n }\n`;\n\nconst StyledHeaderInner = styled.div<{ $hasChildren: boolean }>`\n display: flex;\n align-items: center;\n justify-content: space-between;\n flex-wrap: wrap;\n padding: 16px 0 0 20px;\n\n ${({ $hasChildren }) =>\n !$hasChildren &&\n css`\n padding-bottom: 16px;\n `}\n\n ${mq(\"lg\")} {\n flex-wrap: nowrap;\n padding: 0 20px;\n }\n`;\n\nconst StyledLeftWrapper = styled.div`\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: fit-content;\n padding-right: 20px;\n\n ${mq(\"lg\")} {\n padding-right: 0;\n }\n`;\n\ninterface HeaderProps {\n children?: React.ReactNode;\n}\n\nfunction Header({ children }: HeaderProps) {\n const [isOptionActive, setIsOptionActive] = useState(false);\n const [isLangActive, setIsLangActive] = useState(false);\n\n const wrapperRef = useRef<HTMLSpanElement>(null);\n const elmRef = useRef<HTMLDivElement>(null);\n const langRef = useRef<HTMLSpanElement>(null);\n const closeMenu = useCallback(() => {\n setIsOptionActive(false);\n setIsLangActive(false);\n }, []);\n\n useOnClickOutside(\n [elmRef, wrapperRef],\n isOptionActive ? closeMenu : () => {},\n );\n useOnClickOutside([langRef, wrapperRef], isLangActive ? closeMenu : () => {});\n const { isChatActive } = useContext(ChatContext);\n const { openSearch } = useContext(SearchContext);\n\n return (\n <StyledHeader $hasChildren={children ? true : false} id=\"header\">\n <StyledHeaderInner $hasChildren={children ? true : false}>\n <Link href=\"/\" className=\"logo\" aria-label=\"Logo\">\n {customThemeJson.logo ? (\n <>\n {/* Both logos render; .light-only and .dark-only classes in\n GlobalStyles hide the inactive one based on the \"dark\" class\n on <html>. Avoids a JS-driven swap so no flash on first load. */}\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n className=\"light-only\"\n src={customThemeJson.logo.light}\n alt=\"Logo\"\n width=\"100\"\n height=\"100\"\n />\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n className=\"dark-only\"\n src={customThemeJson.logo.dark}\n alt=\"Logo\"\n width=\"100\"\n height=\"100\"\n />\n </>\n ) : (\n <Logo />\n )}\n </Link>\n {children}\n <StyledLeftWrapper>\n <StyledSearchButton onClick={openSearch} aria-label=\"Search docs\">\n <Search size={14} />\n <SearchKbd>⌘K</SearchKbd>\n </StyledSearchButton>\n {isChatActive && <ChatButtonCTA />}\n </StyledLeftWrapper>\n </StyledHeaderInner>\n </StyledHeader>\n );\n}\n\nexport { Header };\n";
|
|
@@ -4,7 +4,7 @@ import { useCallback, useContext, useRef, useState } from "react";
|
|
|
4
4
|
import styled, { css } from "styled-components";
|
|
5
5
|
import Link from "next/link";
|
|
6
6
|
import { mq, Theme } from "@/app/theme";
|
|
7
|
-
import { useOnClickOutside } from "
|
|
7
|
+
import { useOnClickOutside } from "cherry-styled-components";
|
|
8
8
|
import { Search } from "lucide-react";
|
|
9
9
|
import { Logo } from "@/components/layout/Pictograms";
|
|
10
10
|
import { ChatContext, ChatButtonCTA } from "@/components/Chat";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const siteGateComponentTemplate = "\"use client\";\nimport {
|
|
1
|
+
export declare const siteGateComponentTemplate = "\"use client\";\nimport { useState, type FormEvent } from \"react\";\nimport styled from \"styled-components\";\nimport { Input, ThemeToggle } from \"cherry-styled-components\";\nimport { Lock } from \"lucide-react\";\nimport { Theme } from \"@/app/theme\";\nimport { Button } from \"@/components/layout/Button\";\nimport { Callout } from \"@/components/layout/Callout\";\nimport { config } from \"@/utils/config\";\n\nconst StyledWrapper = styled.div<{ theme: Theme }>`\n min-height: 100dvh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n background: ${({ theme }) => theme.colors.light};\n`;\n\nconst StyledInner = styled.div`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n width: 100%;\n max-width: 380px;\n`;\n\nconst StyledFooter = styled.div`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n`;\n\nconst StyledBranding = styled.p<{ theme: Theme }>`\n margin: 0;\n font-size: 14px;\n color: ${({ theme }) => theme.colors.grayDark};\n\n & a {\n color: ${({ theme }) => theme.colors.accent};\n font-weight: 700;\n text-decoration: none;\n transition: all 0.3s ease;\n }\n\n & a:hover {\n color: ${({ theme }) => theme.colors.primary};\n }\n`;\n\nconst StyledCard = styled.div<{ theme: Theme }>`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n width: 100%;\n padding: 40px 32px;\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n box-shadow: ${({ theme }) => theme.shadows.sm};\n text-align: center;\n`;\n\nconst StyledIcon = styled.div<{ theme: Theme }>`\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 48px;\n height: 48px;\n border-radius: 50%;\n color: ${({ theme }) => theme.colors.accent};\n background: ${({ theme }) => theme.colors.grayLight};\n\n & svg {\n width: 22px;\n height: 22px;\n }\n`;\n\nconst StyledTitle = styled.h1<{ theme: Theme }>`\n margin: 0;\n font-size: 22px;\n color: ${({ theme }) => theme.colors.accent};\n`;\n\nconst StyledLede = styled.p<{ theme: Theme }>`\n margin: 0;\n font-size: 15px;\n color: ${({ theme }) => theme.colors.grayDark};\n`;\n\nconst StyledForm = styled.form`\n display: flex;\n flex-direction: column;\n gap: 20px;\n width: 100%;\n`;\n\nconst StyledAlert = styled.div`\n width: 100%;\n text-align: left;\n`;\n\nexport function SiteGate({ hideBranding }: { hideBranding?: boolean }) {\n const [password, setPassword] = useState(\"\");\n const [status, setStatus] = useState<\"idle\" | \"loading\" | \"error\">(\"idle\");\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n if (!password || status === \"loading\") return;\n\n setStatus(\"loading\");\n try {\n const res = await fetch(\"/api/gate\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ password }),\n });\n if (res.ok) {\n window.location.reload();\n return;\n }\n setStatus(\"error\");\n } catch {\n setStatus(\"error\");\n }\n }\n\n return (\n <StyledWrapper>\n <StyledInner>\n <StyledCard>\n <StyledIcon>\n <Lock />\n </StyledIcon>\n <StyledTitle>{config.name || \"Documentation\"}</StyledTitle>\n <StyledLede>This site is password protected.</StyledLede>\n <StyledForm onSubmit={handleSubmit} noValidate>\n <Input\n type=\"password\"\n name=\"password\"\n autoComplete=\"current-password\"\n placeholder=\"Enter password\"\n aria-label=\"Password\"\n autoFocus\n value={password}\n $error={status === \"error\"}\n $fullWidth\n onChange={(e) => {\n setPassword(e.target.value);\n if (status === \"error\") setStatus(\"idle\");\n }}\n />\n <Button\n type=\"submit\"\n fullWidth\n disabled={status === \"loading\" || !password}\n >\n {status === \"loading\" ? \"Unlocking...\" : \"Enter\"}\n </Button>\n </StyledForm>\n {status === \"error\" && (\n <StyledAlert>\n <Callout type=\"danger\">\n <p>Incorrect password. Try again.</p>\n </Callout>\n </StyledAlert>\n )}\n </StyledCard>\n <StyledFooter>\n <ThemeToggle />\n {!hideBranding && (\n <StyledBranding>\n Powered by <a href=\"https://doccupine.com\">Doccupine</a>\n </StyledBranding>\n )}\n </StyledFooter>\n </StyledInner>\n </StyledWrapper>\n );\n}\n";
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
export const siteGateComponentTemplate = `"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { useState, type FormEvent } from "react";
|
|
3
3
|
import styled from "styled-components";
|
|
4
|
-
import { Input } from "cherry-styled-components";
|
|
4
|
+
import { Input, ThemeToggle } from "cherry-styled-components";
|
|
5
5
|
import { Lock } from "lucide-react";
|
|
6
6
|
import { Theme } from "@/app/theme";
|
|
7
7
|
import { Button } from "@/components/layout/Button";
|
|
8
8
|
import { Callout } from "@/components/layout/Callout";
|
|
9
|
-
import {
|
|
10
|
-
ToggleTheme,
|
|
11
|
-
ToggleThemeLoading,
|
|
12
|
-
} from "@/components/layout/ThemeToggle";
|
|
13
9
|
import { config } from "@/utils/config";
|
|
14
10
|
|
|
15
11
|
const StyledWrapper = styled.div<{ theme: Theme }>\`
|
|
@@ -175,9 +171,7 @@ export function SiteGate({ hideBranding }: { hideBranding?: boolean }) {
|
|
|
175
171
|
)}
|
|
176
172
|
</StyledCard>
|
|
177
173
|
<StyledFooter>
|
|
178
|
-
<
|
|
179
|
-
<ToggleTheme />
|
|
180
|
-
</Suspense>
|
|
174
|
+
<ThemeToggle />
|
|
181
175
|
{!hideBranding && (
|
|
182
176
|
<StyledBranding>
|
|
183
177
|
Powered by <a href="https://doccupine.com">Doccupine</a>
|
|
@@ -17,13 +17,13 @@ export const packageJsonTemplate = JSON.stringify({
|
|
|
17
17
|
"@mdx-js/react": "^3.1.1",
|
|
18
18
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
19
19
|
"@posthog/react": "^1.10.3",
|
|
20
|
-
"cherry-styled-components": "^0.2.
|
|
20
|
+
"cherry-styled-components": "^0.2.5",
|
|
21
21
|
langchain: "^1.5.2",
|
|
22
22
|
"lucide-react": "^1.23.0",
|
|
23
23
|
minisearch: "^7.2.0",
|
|
24
24
|
next: "16.2.10",
|
|
25
25
|
"next-mdx-remote": "^6.0.0",
|
|
26
|
-
"posthog-js": "^1.396.
|
|
26
|
+
"posthog-js": "^1.396.6",
|
|
27
27
|
"posthog-node": "^5.39.4",
|
|
28
28
|
react: "19.2.7",
|
|
29
29
|
"react-dom": "19.2.7",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doccupine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.96",
|
|
4
4
|
"description": "Free and open-source documentation platform. Write MDX, get a production-ready site with AI chat, built-in components, and an MCP server - in one command.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|