doccupine 0.0.114 ā 0.0.116
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 +102 -67
- package/dist/lib/types.d.ts +4 -0
- package/dist/lib/utils.d.ts +16 -0
- package/dist/lib/utils.js +56 -0
- package/dist/templates/components/layout/Callout.d.ts +1 -1
- package/dist/templates/components/layout/Callout.js +1 -0
- package/dist/templates/components/layout/Code.d.ts +1 -1
- package/dist/templates/components/layout/Code.js +46 -74
- package/dist/templates/components/layout/GlobalStyles.d.ts +1 -1
- package/dist/templates/components/layout/GlobalStyles.js +1 -0
- package/dist/templates/components/layout/SharedStyles.d.ts +1 -1
- package/dist/templates/components/layout/SharedStyles.js +7 -4
- package/dist/templates/package.js +5 -5
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import chalk from "chalk";
|
|
|
9
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
|
-
import { findAvailablePort, generateSlug, getFullSlug, escapeTemplateContent, } from "./lib/utils.js";
|
|
12
|
+
import { findAvailablePort, generateSlug, getFullSlug, escapeTemplateContent, resolvePackageManager, } from "./lib/utils.js";
|
|
13
13
|
import { generateMetadataBlock, generateRuntimeOnlyMetadataBlock, generateJsonLdScript, } from "./lib/metadata.js";
|
|
14
14
|
import { nextConfigTemplate } from "./templates/next.config.js";
|
|
15
15
|
import { pnpmWorkspaceTemplate } from "./templates/pnpmWorkspace.js";
|
|
@@ -60,9 +60,6 @@ class MDXToNextJSGenerator {
|
|
|
60
60
|
await fs.ensureDir(this.outputDir);
|
|
61
61
|
this.sectionsConfig = await this.resolveSections();
|
|
62
62
|
this.analyticsConfig = await this.loadAnalyticsConfig();
|
|
63
|
-
if (this.sectionsConfig) {
|
|
64
|
-
console.log(chalk.blue(`š Found ${this.sectionsConfig.length} section(s): ${this.sectionsConfig.map((s) => s.label).join(", ")}`));
|
|
65
|
-
}
|
|
66
63
|
if (this.analyticsConfig) {
|
|
67
64
|
console.log(chalk.blue(`š Analytics enabled: ${this.analyticsConfig.provider}`));
|
|
68
65
|
}
|
|
@@ -72,8 +69,16 @@ class MDXToNextJSGenerator {
|
|
|
72
69
|
await this.copyFontConfig();
|
|
73
70
|
await this.copyAnalyticsConfig();
|
|
74
71
|
await this.copyPublicFiles();
|
|
72
|
+
// createStartingDocs() may have written the sample docs - which carry
|
|
73
|
+
// section frontmatter - after the initial resolveSections() ran against an
|
|
74
|
+
// empty watch dir. Re-resolve now that every MDX file is on disk so the
|
|
75
|
+
// build applies the correct sections in a single O(n) pass, instead of
|
|
76
|
+
// rediscovering them per file (the old O(n²) behavior).
|
|
77
|
+
this.sectionsConfig = await this.resolveSections();
|
|
78
|
+
if (this.sectionsConfig) {
|
|
79
|
+
console.log(chalk.blue(`š Found ${this.sectionsConfig.length} section(s): ${this.sectionsConfig.map((s) => s.label).join(", ")}`));
|
|
80
|
+
}
|
|
75
81
|
await this.processAllMDXFiles();
|
|
76
|
-
await this.generateSectionIndexPages();
|
|
77
82
|
console.log(chalk.green("ā
Initial setup complete!"));
|
|
78
83
|
console.log(chalk.cyan("š” To start the Next.js dev server:"));
|
|
79
84
|
console.log(chalk.white(` cd ${path.relative(process.cwd(), this.outputDir)}`));
|
|
@@ -260,7 +265,6 @@ class MDXToNextJSGenerator {
|
|
|
260
265
|
console.log(chalk.cyan("š Sections configuration changed"));
|
|
261
266
|
this.sectionsConfig = await this.resolveSections();
|
|
262
267
|
await this.processAllMDXFiles();
|
|
263
|
-
await this.generateSectionIndexPages();
|
|
264
268
|
}
|
|
265
269
|
async maybeUpdateSections() {
|
|
266
270
|
if (this.isReprocessing)
|
|
@@ -278,8 +282,9 @@ class MDXToNextJSGenerator {
|
|
|
278
282
|
this.sectionsConfig = newSections;
|
|
279
283
|
this.isReprocessing = true;
|
|
280
284
|
try {
|
|
285
|
+
// processAllMDXFiles() already refreshes section index pages via its
|
|
286
|
+
// aggregate pass, so no separate generateSectionIndexPages() here.
|
|
281
287
|
await this.processAllMDXFiles();
|
|
282
|
-
await this.generateSectionIndexPages();
|
|
283
288
|
}
|
|
284
289
|
finally {
|
|
285
290
|
this.isReprocessing = false;
|
|
@@ -681,36 +686,58 @@ class MDXToNextJSGenerator {
|
|
|
681
686
|
const files = await this.getAllMDXFiles();
|
|
682
687
|
return Promise.all(files.map((file) => this.parseMDXFile(file)));
|
|
683
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* Writes the generated page(s) for a single MDX file: the doc page and, for a
|
|
691
|
+
* section-index file, the section landing page. Deliberately does NOT run the
|
|
692
|
+
* site-wide aggregations (pages index, layout, sitemap, llms, section
|
|
693
|
+
* redirects) - the caller batches those so a bulk build runs them once at the
|
|
694
|
+
* end instead of once per file (which is what made large builds O(n²)).
|
|
695
|
+
*/
|
|
696
|
+
async writePageForFile(filePath) {
|
|
697
|
+
const fullPath = path.join(this.watchDir, filePath);
|
|
698
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
699
|
+
const { data: frontmatter, content: mdxContent } = matter(content);
|
|
700
|
+
const { sectionSlug, pageSlug } = this.determineSectionForFile(filePath, frontmatter);
|
|
701
|
+
const fullSlug = getFullSlug(pageSlug, sectionSlug);
|
|
702
|
+
const isIndex = filePath === "index.mdx" || filePath === "./index.mdx";
|
|
703
|
+
const isSectionIndex = this.sectionsConfig && pageSlug === "" && sectionSlug !== "";
|
|
704
|
+
if (isIndex) {
|
|
705
|
+
// The homepage is emitted by updatePagesIndex() in the aggregate pass, so
|
|
706
|
+
// there is no per-file page to write here.
|
|
707
|
+
console.log(chalk.blue("š Updating homepage with index.mdx content"));
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
const mdxFile = {
|
|
711
|
+
path: filePath,
|
|
712
|
+
content: mdxContent,
|
|
713
|
+
frontmatter,
|
|
714
|
+
slug: fullSlug,
|
|
715
|
+
};
|
|
716
|
+
await this.generatePageFromMDX(mdxFile);
|
|
717
|
+
}
|
|
718
|
+
if (isSectionIndex) {
|
|
719
|
+
await this.updateSectionIndex(sectionSlug, frontmatter, mdxContent);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Regenerates every file that depends on the full set of pages (pages index,
|
|
724
|
+
* root/site layout, sitemap, llms files, section redirects). Parses all MDX
|
|
725
|
+
* exactly once and threads the result through each generator, so one refresh
|
|
726
|
+
* is a single scan rather than one scan per generator.
|
|
727
|
+
*/
|
|
728
|
+
async refreshSiteAggregates() {
|
|
729
|
+
const pages = await this.buildAllPagesMeta();
|
|
730
|
+
await this.updatePagesIndex();
|
|
731
|
+
await this.updateRootLayout(pages);
|
|
732
|
+
await this.updateSitemap(pages);
|
|
733
|
+
await this.updateLlmsFiles(pages);
|
|
734
|
+
await this.generateSectionIndexPages(pages);
|
|
735
|
+
}
|
|
684
736
|
async handleFileChange(action, filePath) {
|
|
685
737
|
console.log(chalk.cyan(`š File ${action}: ${filePath}`));
|
|
686
|
-
const fullPath = path.join(this.watchDir, filePath);
|
|
687
738
|
try {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const { sectionSlug, pageSlug } = this.determineSectionForFile(filePath, frontmatter);
|
|
691
|
-
const fullSlug = getFullSlug(pageSlug, sectionSlug);
|
|
692
|
-
const isIndex = filePath === "index.mdx" || filePath === "./index.mdx";
|
|
693
|
-
const isSectionIndex = this.sectionsConfig && pageSlug === "" && sectionSlug !== "";
|
|
694
|
-
if (isIndex) {
|
|
695
|
-
console.log(chalk.blue("š Updating homepage with index.mdx content"));
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
const mdxFile = {
|
|
699
|
-
path: filePath,
|
|
700
|
-
content: mdxContent,
|
|
701
|
-
frontmatter,
|
|
702
|
-
slug: fullSlug,
|
|
703
|
-
};
|
|
704
|
-
await this.generatePageFromMDX(mdxFile);
|
|
705
|
-
}
|
|
706
|
-
if (isSectionIndex) {
|
|
707
|
-
await this.updateSectionIndex(sectionSlug, frontmatter, mdxContent);
|
|
708
|
-
}
|
|
709
|
-
await this.updatePagesIndex();
|
|
710
|
-
await this.updateRootLayout();
|
|
711
|
-
await this.updateSitemap();
|
|
712
|
-
await this.updateLlmsFiles();
|
|
713
|
-
await this.generateSectionIndexPages();
|
|
739
|
+
await this.writePageForFile(filePath);
|
|
740
|
+
await this.refreshSiteAggregates();
|
|
714
741
|
console.log(chalk.green(`ā
Generated page for: ${filePath}`));
|
|
715
742
|
await this.maybeUpdateSections();
|
|
716
743
|
}
|
|
@@ -744,9 +771,21 @@ class MDXToNextJSGenerator {
|
|
|
744
771
|
}
|
|
745
772
|
async processAllMDXFiles() {
|
|
746
773
|
const files = await this.getAllMDXFiles();
|
|
774
|
+
// Write each page first (the only genuinely per-file work), then run the
|
|
775
|
+
// site-wide aggregations a single time. Doing the aggregations per file
|
|
776
|
+
// re-scanned and re-parsed every MDX file on each iteration, which made a
|
|
777
|
+
// full build O(n²); batching them makes it O(n). A single bad file is
|
|
778
|
+
// logged and skipped so it never aborts the whole build.
|
|
747
779
|
for (const file of files) {
|
|
748
|
-
|
|
780
|
+
console.log(chalk.cyan(`š Processing: ${file}`));
|
|
781
|
+
try {
|
|
782
|
+
await this.writePageForFile(file);
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
console.error(chalk.red(`ā Error processing ${file}:`), error);
|
|
786
|
+
}
|
|
749
787
|
}
|
|
788
|
+
await this.refreshSiteAggregates();
|
|
750
789
|
}
|
|
751
790
|
async getAllMDXFiles() {
|
|
752
791
|
const files = [];
|
|
@@ -771,23 +810,23 @@ class MDXToNextJSGenerator {
|
|
|
771
810
|
const analyticsEnabled = this.analyticsConfig !== null;
|
|
772
811
|
return rootLayoutTemplate(fontConfig, analyticsEnabled);
|
|
773
812
|
}
|
|
774
|
-
async generateSiteLayout() {
|
|
775
|
-
const
|
|
776
|
-
return siteLayoutTemplate(
|
|
813
|
+
async generateSiteLayout(pages) {
|
|
814
|
+
const resolvedPages = pages ?? (await this.buildAllPagesMeta());
|
|
815
|
+
return siteLayoutTemplate(resolvedPages, this.sectionsConfig);
|
|
777
816
|
}
|
|
778
|
-
async generateSectionIndexPages() {
|
|
817
|
+
async generateSectionIndexPages(pages) {
|
|
779
818
|
if (!this.sectionsConfig || this.sectionsConfig.length === 0)
|
|
780
819
|
return;
|
|
781
|
-
const
|
|
820
|
+
const resolvedPages = pages ?? (await this.buildAllPagesMeta());
|
|
782
821
|
for (const section of this.sectionsConfig) {
|
|
783
822
|
if (section.slug === "")
|
|
784
823
|
continue;
|
|
785
824
|
// Check if a page already exists at the section root
|
|
786
|
-
const hasIndex =
|
|
825
|
+
const hasIndex = resolvedPages.some((p) => p.slug === section.slug);
|
|
787
826
|
if (hasIndex)
|
|
788
827
|
continue;
|
|
789
828
|
// Find the first page in this section
|
|
790
|
-
const sectionPages =
|
|
829
|
+
const sectionPages = resolvedPages
|
|
791
830
|
.filter((p) => p.section === section.slug)
|
|
792
831
|
.sort((a, b) => {
|
|
793
832
|
if (a.categoryOrder !== b.categoryOrder)
|
|
@@ -983,11 +1022,11 @@ export default function Page() {
|
|
|
983
1022
|
await fs.ensureDir(path.dirname(pagePath));
|
|
984
1023
|
await fs.writeFile(pagePath, indexContent, "utf8");
|
|
985
1024
|
}
|
|
986
|
-
async updateRootLayout() {
|
|
1025
|
+
async updateRootLayout(pages) {
|
|
987
1026
|
await fs.writeFile(path.join(this.outputDir, "app", "layout.tsx"), await this.generateRootLayout(), "utf8");
|
|
988
1027
|
const siteLayoutPath = path.join(this.outputDir, "app", "(site)", "layout.tsx");
|
|
989
1028
|
await fs.ensureDir(path.dirname(siteLayoutPath));
|
|
990
|
-
await fs.writeFile(siteLayoutPath, await this.generateSiteLayout(), "utf8");
|
|
1029
|
+
await fs.writeFile(siteLayoutPath, await this.generateSiteLayout(pages), "utf8");
|
|
991
1030
|
}
|
|
992
1031
|
async loadSiteUrl() {
|
|
993
1032
|
const configPath = path.join(this.rootDir, "config.json");
|
|
@@ -1033,7 +1072,7 @@ export default function Page() {
|
|
|
1033
1072
|
}
|
|
1034
1073
|
return entries;
|
|
1035
1074
|
}
|
|
1036
|
-
async updateSitemap() {
|
|
1075
|
+
async updateSitemap(pages) {
|
|
1037
1076
|
const sitemapPath = path.join(this.outputDir, "app", "sitemap.ts");
|
|
1038
1077
|
const siteUrl = await this.loadSiteUrl();
|
|
1039
1078
|
if (!siteUrl) {
|
|
@@ -1043,8 +1082,8 @@ export default function Page() {
|
|
|
1043
1082
|
}
|
|
1044
1083
|
return;
|
|
1045
1084
|
}
|
|
1046
|
-
const
|
|
1047
|
-
const entries = this.buildSitemapEntries(
|
|
1085
|
+
const resolvedPages = pages ?? (await this.buildAllPagesMeta());
|
|
1086
|
+
const entries = this.buildSitemapEntries(resolvedPages);
|
|
1048
1087
|
await fs.writeFile(sitemapPath, sitemapTemplate(entries), "utf8");
|
|
1049
1088
|
console.log(chalk.green(`šŗļø Generated sitemap.ts with ${entries.length} page(s) using ${siteUrl}`));
|
|
1050
1089
|
}
|
|
@@ -1116,17 +1155,17 @@ export default function Page() {
|
|
|
1116
1155
|
const json = JSON.stringify(payload, null, 2) + "\n";
|
|
1117
1156
|
await fs.writeFile(manifestPath, json, "utf8");
|
|
1118
1157
|
}
|
|
1119
|
-
async updateLlmsFiles() {
|
|
1158
|
+
async updateLlmsFiles(pages) {
|
|
1120
1159
|
const publicDir = path.join(this.outputDir, "public");
|
|
1121
1160
|
await fs.ensureDir(publicDir);
|
|
1122
1161
|
const { url: baseUrl, name, description } = await this.loadSiteMetadata();
|
|
1123
|
-
const
|
|
1124
|
-
const pagesWithBodies = await Promise.all(
|
|
1162
|
+
const resolvedPages = pages ?? (await this.buildAllPagesMeta());
|
|
1163
|
+
const pagesWithBodies = await Promise.all(resolvedPages.map((page) => this.readPageWithBody(page)));
|
|
1125
1164
|
const indexContent = llmsIndexTemplate({
|
|
1126
1165
|
siteName: name,
|
|
1127
1166
|
siteDescription: description,
|
|
1128
1167
|
baseUrl,
|
|
1129
|
-
pages,
|
|
1168
|
+
pages: resolvedPages,
|
|
1130
1169
|
sectionsConfig: this.sectionsConfig,
|
|
1131
1170
|
});
|
|
1132
1171
|
const fullContent = llmsFullTemplate({
|
|
@@ -1167,7 +1206,7 @@ export default function Page() {
|
|
|
1167
1206
|
}
|
|
1168
1207
|
this.generatedLlmsPagePaths = nextRelativePaths;
|
|
1169
1208
|
await this.writeLlmsManifest(nextRelativePaths);
|
|
1170
|
-
console.log(chalk.green(`š¤ Generated llms.txt and llms-full.txt with ${
|
|
1209
|
+
console.log(chalk.green(`š¤ Generated llms.txt and llms-full.txt with ${resolvedPages.length} page(s)${baseUrl ? ` using ${baseUrl}` : " (relative URLs)"}`));
|
|
1171
1210
|
}
|
|
1172
1211
|
async stop() {
|
|
1173
1212
|
if (this.watcher) {
|
|
@@ -1205,6 +1244,7 @@ program
|
|
|
1205
1244
|
.option("--port <port>", "Port for Next.js dev server", "3000")
|
|
1206
1245
|
.option("--verbose", "Show verbose output")
|
|
1207
1246
|
.option("--reset", "Reset configuration and prompt for new directories")
|
|
1247
|
+
.option("--package-manager <name>", "Package manager for the generated app: pnpm or npm (default: auto-detect)")
|
|
1208
1248
|
.action(async (options) => {
|
|
1209
1249
|
const configManager = new ConfigManager();
|
|
1210
1250
|
const config = await configManager.getConfig({
|
|
@@ -1215,18 +1255,13 @@ program
|
|
|
1215
1255
|
await generator.init();
|
|
1216
1256
|
let devServer = null;
|
|
1217
1257
|
console.log(chalk.blue("š¦ Installing dependencies..."));
|
|
1218
|
-
const { spawn
|
|
1219
|
-
//
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
catch {
|
|
1226
|
-
// pnpm not available, use npm
|
|
1227
|
-
}
|
|
1228
|
-
console.log(chalk.blue(`š¦ Using ${packageManager}...`));
|
|
1229
|
-
const install = spawn(packageManager, ["install"], {
|
|
1258
|
+
const { spawn } = await import("child_process");
|
|
1259
|
+
// Prefer pnpm (the generated app ships a pnpm workspace) and fall back to
|
|
1260
|
+
// npm. A --package-manager flag or a "packageManager" field in
|
|
1261
|
+
// doccupine.json overrides detection; the flag wins.
|
|
1262
|
+
const packageManager = resolvePackageManager(options.packageManager ?? config.packageManager);
|
|
1263
|
+
console.log(chalk.blue(`š¦ Using ${packageManager.name}...`));
|
|
1264
|
+
const install = spawn(packageManager.bin, ["install"], {
|
|
1230
1265
|
cwd: config.outputDir,
|
|
1231
1266
|
stdio: "inherit",
|
|
1232
1267
|
});
|
|
@@ -1237,7 +1272,7 @@ program
|
|
|
1237
1272
|
resolve(void 0);
|
|
1238
1273
|
}
|
|
1239
1274
|
else {
|
|
1240
|
-
reject(new Error(`${packageManager} install failed with code ${code}`));
|
|
1275
|
+
reject(new Error(`${packageManager.name} install failed with code ${code}`));
|
|
1241
1276
|
}
|
|
1242
1277
|
});
|
|
1243
1278
|
install.on("error", reject);
|
|
@@ -1248,10 +1283,10 @@ program
|
|
|
1248
1283
|
}
|
|
1249
1284
|
console.log(chalk.blue(`š Starting Next.js dev server on port ${port}...`));
|
|
1250
1285
|
const portStr = String(port);
|
|
1251
|
-
const devArgs = packageManager === "npm"
|
|
1286
|
+
const devArgs = packageManager.name === "npm"
|
|
1252
1287
|
? ["run", "dev", "--", "--port", portStr]
|
|
1253
1288
|
: ["run", "dev", "--port", portStr];
|
|
1254
|
-
devServer = spawn(packageManager, devArgs, {
|
|
1289
|
+
devServer = spawn(packageManager.bin, devArgs, {
|
|
1255
1290
|
cwd: config.outputDir,
|
|
1256
1291
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1257
1292
|
});
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export interface DoccupineConfig {
|
|
|
27
27
|
watchDir: string;
|
|
28
28
|
outputDir: string;
|
|
29
29
|
port: string;
|
|
30
|
+
/** Force the package manager used to install and run the generated app
|
|
31
|
+
* ("pnpm" or "npm"), overriding auto-detection. Also settable per run via
|
|
32
|
+
* the `--package-manager` flag, which takes precedence over this field. */
|
|
33
|
+
packageManager?: string;
|
|
30
34
|
}
|
|
31
35
|
export interface FontConfig {
|
|
32
36
|
[key: string]: any;
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
export type PackageManagerName = "pnpm" | "npm";
|
|
2
|
+
export interface ResolvedPackageManager {
|
|
3
|
+
name: PackageManagerName;
|
|
4
|
+
/** Absolute path when we can resolve one, otherwise the bare command name.
|
|
5
|
+
* Spawning the resolved path (not the bare name) is what lets pnpm work
|
|
6
|
+
* even when its install dir is missing from the process PATH. */
|
|
7
|
+
bin: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Chooses the package manager for the generated app. The app ships a pnpm
|
|
11
|
+
* workspace, so pnpm is preferred whenever it can be found; npm is the fallback.
|
|
12
|
+
* An explicit `override` (from `--package-manager` or doccupine.json) always
|
|
13
|
+
* wins. Auto-detection also trusts `npm_config_user_agent` when pnpm launched
|
|
14
|
+
* the CLI.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolvePackageManager(override?: string): ResolvedPackageManager;
|
|
1
17
|
export declare function findAvailablePort(startPort: number): Promise<number>;
|
|
2
18
|
export declare function generateSlug(filePath: string): string;
|
|
3
19
|
export declare function getFullSlug(pageSlug: string, sectionSlug: string): string;
|
package/dist/lib/utils.js
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
/**
|
|
6
|
+
* Locates a usable pnpm binary without relying solely on the caller's PATH.
|
|
7
|
+
* Standalone pnpm installs (e.g. ~/Library/pnpm) expose PNPM_HOME but only add
|
|
8
|
+
* their bin dir to PATH via an interactive shell rc, so a CLI launched from an
|
|
9
|
+
* IDE terminal or GUI often can't see `pnpm` on PATH. Returns null if pnpm
|
|
10
|
+
* genuinely isn't available.
|
|
11
|
+
*/
|
|
12
|
+
function findPnpmBin() {
|
|
13
|
+
const exe = process.platform === "win32" ? "pnpm.exe" : "pnpm";
|
|
14
|
+
const home = process.env.PNPM_HOME;
|
|
15
|
+
if (home) {
|
|
16
|
+
const candidate = path.join(home, exe);
|
|
17
|
+
if (fs.existsSync(candidate))
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
execSync("pnpm --version", { stdio: "ignore" });
|
|
22
|
+
return "pnpm";
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Chooses the package manager for the generated app. The app ships a pnpm
|
|
30
|
+
* workspace, so pnpm is preferred whenever it can be found; npm is the fallback.
|
|
31
|
+
* An explicit `override` (from `--package-manager` or doccupine.json) always
|
|
32
|
+
* wins. Auto-detection also trusts `npm_config_user_agent` when pnpm launched
|
|
33
|
+
* the CLI.
|
|
34
|
+
*/
|
|
35
|
+
export function resolvePackageManager(override) {
|
|
36
|
+
const requested = override?.trim().toLowerCase();
|
|
37
|
+
if (requested === "npm")
|
|
38
|
+
return { name: "npm", bin: "npm" };
|
|
39
|
+
if (requested === "pnpm") {
|
|
40
|
+
const bin = findPnpmBin();
|
|
41
|
+
if (bin)
|
|
42
|
+
return { name: "pnpm", bin };
|
|
43
|
+
console.warn(chalk.yellow("ā ļø packageManager is set to pnpm but pnpm could not be found; using npm."));
|
|
44
|
+
return { name: "npm", bin: "npm" };
|
|
45
|
+
}
|
|
46
|
+
if (requested) {
|
|
47
|
+
console.warn(chalk.yellow(`ā ļø Unknown packageManager "${override}" (expected "pnpm" or "npm"); auto-detecting instead.`));
|
|
48
|
+
}
|
|
49
|
+
const launchedByPnpm = (process.env.npm_config_user_agent || "").startsWith("pnpm");
|
|
50
|
+
if (launchedByPnpm)
|
|
51
|
+
return { name: "pnpm", bin: "pnpm" };
|
|
52
|
+
const bin = findPnpmBin();
|
|
53
|
+
if (bin)
|
|
54
|
+
return { name: "pnpm", bin };
|
|
55
|
+
return { name: "npm", bin: "npm" };
|
|
56
|
+
}
|
|
1
57
|
export async function findAvailablePort(startPort) {
|
|
2
58
|
const net = await import("net");
|
|
3
59
|
return new Promise((resolve) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const calloutTemplate = "\"use client\";\nimport { Theme } from \"@/app/theme\";\nimport { styledSmall } from \"cherry-styled-components\";\nimport styled, { css } from \"styled-components\";\nimport { Icon, IconProps } from \"@/components/layout/Icon\";\n\ntype CalloutType = \"note\" | \"info\" | \"warning\" | \"danger\" | \"success\";\n\nconst StyledCallout = styled.div<{ theme: Theme; $type?: CalloutType }>`\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n padding: 20px;\n margin: 0;\n ${({ theme }) => styledSmall(theme)}\n color: ${({ theme }) => theme.colors.grayDark};\n display: flex;\n\n & svg {\n vertical-align: middle;\n min-width: min-content;\n margin: 3px 10px 0 0;\n }\n\n /* Callout tints are alert-semantic (info-blue, warning-amber, danger-red,\n etc.) and intentionally independent of theme.json. Light-mode values are\n defined by default; dark-mode overrides live in :root.dark & blocks so\n the swap happens via the active <html> class with no re-render. */\n ${({ $type }) =>\n $type === \"note\" &&\n css`\n border-color: #0ea5e933;\n background: #f0f9ff80;\n\n & svg.lucide,\n & p {\n color: #0c4a6e;\n }\n\n :root.dark & {\n border-color: #0ea5e94d;\n background: #0ea5e91a;\n\n & svg.lucide,\n & p {\n color: #bae6fd;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"info\" &&\n css`\n border-color: #71717a33;\n background: #fafafa80;\n\n & svg.lucide,\n & .lucide,\n & p {\n color: #18181b;\n }\n\n :root.dark & {\n border-color: #71717a4d;\n background: #71717a1a;\n\n & svg.lucide,\n & .lucide,\n & p {\n color: #e4e4e7;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"warning\" &&\n css`\n border-color: #f59e0b33;\n background: #fffbeb80;\n\n & svg.lucide,\n & p {\n color: #78350f;\n }\n\n :root.dark & {\n border-color: #f59e0b4d;\n background: #f59e0b1a;\n\n & svg.lucide,\n & p {\n color: #fde68a;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"danger\" &&\n css`\n border-color: #ef444433;\n background: #fef2f280;\n\n & svg.lucide,\n & p {\n color: #7f1d1d;\n }\n\n :root.dark & {\n border-color: #ef44444d;\n background: #ef44441a;\n\n & svg.lucide,\n & p {\n color: #fecaca;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"success\" &&\n css`\n border-color: #10b98133;\n background: #ecfdf580;\n\n & svg.lucide,\n & p {\n color: #064e3b;\n }\n\n :root.dark & {\n border-color: #10b9814d;\n background: #10b9811a;\n\n & svg.lucide,\n & p {\n color: #a7f3d0;\n }\n }\n `}\n`;\n\nconst StyledChildren = styled.span`\n display: flex;\n flex-direction: column;\n gap: 10px;\n`;\n\ninterface CalloutProps extends React.HTMLAttributes<HTMLDivElement> {\n children: React.ReactNode;\n icon?: IconProps;\n type?: CalloutType;\n}\n\nfunction Callout({ children, type, icon }: CalloutProps) {\n const iconType =\n type === \"note\"\n ? \"CircleAlert\"\n : type === \"info\"\n ? \"Info\"\n : type === \"warning\"\n ? \"TriangleAlert\"\n : type === \"danger\"\n ? \"OctagonAlert\"\n : type === \"success\"\n ? \"Check\"\n : (icon as IconProps);\n return (\n <StyledCallout $type={type}>\n {iconType ? <Icon name={iconType} size={16} /> : null}\n <StyledChildren>{children}</StyledChildren>\n </StyledCallout>\n );\n}\n\nexport { Callout };\n";
|
|
1
|
+
export declare const calloutTemplate = "\"use client\";\nimport { Theme } from \"@/app/theme\";\nimport { styledSmall } from \"cherry-styled-components\";\nimport styled, { css } from \"styled-components\";\nimport { Icon, IconProps } from \"@/components/layout/Icon\";\n\ntype CalloutType = \"note\" | \"info\" | \"warning\" | \"danger\" | \"success\";\n\nconst StyledCallout = styled.div<{ theme: Theme; $type?: CalloutType }>`\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n padding: 20px;\n margin: 0;\n max-width: 100%;\n ${({ theme }) => styledSmall(theme)}\n color: ${({ theme }) => theme.colors.grayDark};\n display: flex;\n\n & svg {\n vertical-align: middle;\n min-width: min-content;\n margin: 3px 10px 0 0;\n }\n\n /* Callout tints are alert-semantic (info-blue, warning-amber, danger-red,\n etc.) and intentionally independent of theme.json. Light-mode values are\n defined by default; dark-mode overrides live in :root.dark & blocks so\n the swap happens via the active <html> class with no re-render. */\n ${({ $type }) =>\n $type === \"note\" &&\n css`\n border-color: #0ea5e933;\n background: #f0f9ff80;\n\n & svg.lucide,\n & p {\n color: #0c4a6e;\n }\n\n :root.dark & {\n border-color: #0ea5e94d;\n background: #0ea5e91a;\n\n & svg.lucide,\n & p {\n color: #bae6fd;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"info\" &&\n css`\n border-color: #71717a33;\n background: #fafafa80;\n\n & svg.lucide,\n & .lucide,\n & p {\n color: #18181b;\n }\n\n :root.dark & {\n border-color: #71717a4d;\n background: #71717a1a;\n\n & svg.lucide,\n & .lucide,\n & p {\n color: #e4e4e7;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"warning\" &&\n css`\n border-color: #f59e0b33;\n background: #fffbeb80;\n\n & svg.lucide,\n & p {\n color: #78350f;\n }\n\n :root.dark & {\n border-color: #f59e0b4d;\n background: #f59e0b1a;\n\n & svg.lucide,\n & p {\n color: #fde68a;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"danger\" &&\n css`\n border-color: #ef444433;\n background: #fef2f280;\n\n & svg.lucide,\n & p {\n color: #7f1d1d;\n }\n\n :root.dark & {\n border-color: #ef44444d;\n background: #ef44441a;\n\n & svg.lucide,\n & p {\n color: #fecaca;\n }\n }\n `}\n\n ${({ $type }) =>\n $type === \"success\" &&\n css`\n border-color: #10b98133;\n background: #ecfdf580;\n\n & svg.lucide,\n & p {\n color: #064e3b;\n }\n\n :root.dark & {\n border-color: #10b9814d;\n background: #10b9811a;\n\n & svg.lucide,\n & p {\n color: #a7f3d0;\n }\n }\n `}\n`;\n\nconst StyledChildren = styled.span`\n display: flex;\n flex-direction: column;\n gap: 10px;\n`;\n\ninterface CalloutProps extends React.HTMLAttributes<HTMLDivElement> {\n children: React.ReactNode;\n icon?: IconProps;\n type?: CalloutType;\n}\n\nfunction Callout({ children, type, icon }: CalloutProps) {\n const iconType =\n type === \"note\"\n ? \"CircleAlert\"\n : type === \"info\"\n ? \"Info\"\n : type === \"warning\"\n ? \"TriangleAlert\"\n : type === \"danger\"\n ? \"OctagonAlert\"\n : type === \"success\"\n ? \"Check\"\n : (icon as IconProps);\n return (\n <StyledCallout $type={type}>\n {iconType ? <Icon name={iconType} size={16} /> : null}\n <StyledChildren>{children}</StyledChildren>\n </StyledCallout>\n );\n}\n\nexport { Callout };\n";
|
|
@@ -12,6 +12,7 @@ const StyledCallout = styled.div<{ theme: Theme; $type?: CalloutType }>\`
|
|
|
12
12
|
border-radius: \${({ theme }) => theme.spacing.radius.lg};
|
|
13
13
|
padding: 20px;
|
|
14
14
|
margin: 0;
|
|
15
|
+
max-width: 100%;
|
|
15
16
|
\${({ theme }) => styledSmall(theme)}
|
|
16
17
|
color: \${({ theme }) => theme.colors.grayDark};
|
|
17
18
|
display: flex;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const codeTemplate = "\"use client\";\nimport { useState, useCallback, useMemo, useId } 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\";\nimport { thinScrollbar } from \"@/components/layout/SharedStyled\";\n\ninterface CodeProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"theme\"\n> {\n code: string;\n language?: string;\n title?: string;\n theme?: Theme;\n}\n\ninterface CodeTab {\n label: string;\n code: string;\n language?: string;\n}\n\ninterface CodeTabsProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"theme\"\n> {\n tabs: CodeTab[];\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 position: relative;\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/* Centered file name in the TopBar. Sits absolutely over the flex row so the\n dots and copy button keep their edge alignment. Monospace + muted GitHub\n grays, swapped via :root.dark like the rest of the block. pointer-events off\n so clicks fall through to nothing and never steal focus from the buttons. */\nconst TopBarTitle = styled.span<{ theme: Theme }>`\n position: absolute;\n left: 50%;\n transform: translateX(-50%);\n max-width: 60%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n pointer-events: none;\n font-family: ${({ theme }) => theme.fonts.mono};\n font-size: 12px;\n line-height: 2;\n color: #57606a;\n\n :root.dark & {\n color: #8b949e;\n }\n`;\n\n/* Segmented control living in the TopBar for CodeTabs. Scrolls horizontally\n with the thin scrollbar when the labels overflow the 33px bar. */\nconst TabList = styled.div<{ theme: Theme }>`\n display: flex;\n align-items: center;\n gap: 2px;\n min-width: 0;\n overflow-x: auto;\n ${thinScrollbar};\n margin-left: -6px;\n`;\n\n/* Individual tab button. Active tab reads as part of the window: a light body\n fill (#ffffff) that echoes the code area, with a soft border. Inactive tabs\n are muted and transparent. resetButton strips native styling; focus-visible\n draws an inset brand primary ring on a pseudo-element so the scrolling\n TabList can't clip it. All colors stay fixed and swap purely via :root.dark,\n matching the no-re-render palette approach. */\nconst CodeTab = styled.button<{ theme: Theme; $active: boolean }>`\n ${resetButton}\n flex: 0 0 auto;\n position: relative;\n font-family: ${({ theme }) => theme.fonts.mono};\n font-size: 12px;\n line-height: 1;\n padding: 5px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n cursor: pointer;\n white-space: nowrap;\n transition:\n color 0.15s ease,\n background 0.15s ease,\n border-color 0.15s ease;\n border: solid 1px\n ${({ $active }) => ($active ? \"rgba(0, 0, 0, 0.1)\" : \"transparent\")};\n background: ${({ $active }) => ($active ? \"#ffffff\" : \"transparent\")};\n color: ${({ $active }) => ($active ? \"#24292f\" : \"#57606a\")};\n\n &:hover {\n color: #24292f;\n background: ${({ $active }) =>\n $active ? \"#ffffff\" : \"rgba(0, 0, 0, 0.04)\"};\n }\n\n &:focus-visible {\n outline: none;\n }\n\n &:focus-visible::after {\n content: \"\";\n position: absolute;\n inset: 2px;\n border: solid 2px ${({ theme }) => theme.colors.primary};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n pointer-events: none;\n }\n\n :root.dark & {\n border-color: ${({ $active }) =>\n $active ? \"rgba(255, 255, 255, 0.15)\" : \"transparent\"};\n background: ${({ $active }) => ($active ? \"#161b22\" : \"transparent\")};\n color: ${({ $active }) => ($active ? \"#e6edf3\" : \"#8b949e\")};\n\n &:hover {\n color: #e6edf3;\n background: ${({ $active }) =>\n $active ? \"#161b22\" : \"rgba(255, 255, 255, 0.06)\"};\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 min-width: min-content;\n width: 100%;\n display: block;\n padding-right: 20px;\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 ${thinScrollbar};\n ${({ theme }) => styledCode(theme)};\n ${lightSyntaxHighlight};\n\n /* Diff lines: highlight.js wraps each +/- line in a single span, so\n stretching it to the full row reads like a GitHub diff. inline-block keeps\n the trailing newline as the line break instead of adding an empty row. */\n & .hljs-addition,\n & .hljs-deletion {\n display: inline-table;\n width: 100%;\n }\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({\n code,\n language = \"javascript\",\n title,\n theme,\n className,\n}: 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 {title ? <TopBarTitle theme={theme}>{title}</TopBarTitle> : null}\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\n/* Multi-variant code block (npm / pnpm / yarn, etc). Tabs replace the macOS\n dots in the TopBar as a keyboard-accessible tablist; the copy button copies\n whichever tab is active and resets its copied state when the tab changes. */\nfunction CodeTabs({ tabs, theme, className }: CodeTabsProps) {\n const baseId = useId();\n const [active, setActive] = useState(0);\n const [copied, setCopied] = useState(false);\n\n const safeActive = active < tabs.length ? active : 0;\n const activeTab = tabs[safeActive];\n\n const highlightedCode = useMemo(\n () =>\n activeTab\n ? highlightCode(activeTab.code, activeTab.language ?? \"bash\")\n : \"\",\n [activeTab],\n );\n\n const handleSelect = useCallback((index: number) => {\n setActive(index);\n setCopied(false);\n }, []);\n\n const handleCopy = useCallback(async () => {\n if (!activeTab) return;\n try {\n await navigator.clipboard.writeText(activeTab.code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (err) {\n console.error(\"Failed to copy code:\", err);\n }\n }, [activeTab]);\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLButtonElement>) => {\n let next = safeActive;\n if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n next = (safeActive + 1) % tabs.length;\n } else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n next = (safeActive - 1 + tabs.length) % tabs.length;\n } else if (event.key === \"Home\") {\n next = 0;\n } else if (event.key === \"End\") {\n next = tabs.length - 1;\n } else {\n return;\n }\n event.preventDefault();\n handleSelect(next);\n const target = document.getElementById(`${baseId}-tab-${next}`);\n if (target) target.focus();\n },\n [baseId, handleSelect, safeActive, tabs.length],\n );\n\n if (!tabs || tabs.length === 0) return null;\n\n return (\n <CodeWrapper\n className={`${className ?? \"\"} code-wrapper`.trim()}\n theme={theme}\n >\n <TopBar theme={theme}>\n <TabList role=\"tablist\" aria-label=\"Code variants\">\n {tabs.map((tab, index) => (\n <CodeTab\n key={`${tab.label}-${index}`}\n type=\"button\"\n role=\"tab\"\n id={`${baseId}-tab-${index}`}\n aria-selected={index === safeActive}\n aria-controls={`${baseId}-panel`}\n tabIndex={index === safeActive ? 0 : -1}\n onClick={() => handleSelect(index)}\n onKeyDown={handleKeyDown}\n $active={index === safeActive}\n theme={theme}\n >\n {tab.label}\n </CodeTab>\n ))}\n </TabList>\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 id={`${baseId}-panel`}\n role=\"tabpanel\"\n aria-labelledby={`${baseId}-tab-${safeActive}`}\n dangerouslySetInnerHTML={{ __html: highlightedCode }}\n theme={theme}\n className=\"code-wrapper-body\"\n />\n </CodeWrapper>\n );\n}\n\nexport { Code, CodeTabs };\n";
|
|
1
|
+
export declare const codeTemplate = "\"use client\";\nimport { useState, useCallback, useMemo, useId } 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\";\nimport { thinScrollbar } from \"@/components/layout/SharedStyled\";\n\ninterface CodeProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"theme\"\n> {\n code: string;\n language?: string;\n title?: string;\n theme?: Theme;\n}\n\ninterface CodeTab {\n label: string;\n code: string;\n language?: string;\n}\n\ninterface CodeTabsProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"theme\"\n> {\n tabs: CodeTab[];\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 ${({ theme }) => theme.colors.grayLight};\n background: ${({ theme }) => theme.colors.light};\n`;\n\n/* Code block uses a fixed GitHub-style palette for the syntax tokens in both\n modes so highlighting stays legible regardless of brand colors. The dark-mode\n surface, however, matches the left sidebar's translucent brand tint\n (color-mix of theme.colors.primaryLight over the black page) instead of\n GitHub's #0d1117, so the window sits on the same background as the nav. Dark\n variants live in :root.dark & blocks so the swap happens via the active\n <html> class with no re-render. */\nconst TopBar = styled.div<{ theme: Theme }>`\n position: relative;\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 5%, transparent)`};\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 ${({ theme }) => theme.colors.grayLight};\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\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). Colors come from theme\n tokens (grayLight border, success/grayDark icon) that swap for dark mode via\n the theme prop, so no :root.dark & override is needed and the copied state\n reads consistently in both modes. */\nconst CopyButton = styled.button<{ theme: Theme; $copied: boolean }>`\n ${resetButton}\n ${interactiveStyles}\n background: ${({ theme }) => theme.colors.light};\n border-color: ${({ theme }) => theme.colors.grayLight};\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 margin: 0;\n color: ${({ theme, $copied }) =>\n $copied ? theme.colors.success : theme.colors.grayDark};\n }\n`;\n\n/* Centered file name in the TopBar. Sits absolutely over the flex row so the\n dots and copy button keep their edge alignment. Monospace + a muted grayDark\n token that swaps for dark mode via the theme prop. pointer-events off so\n clicks fall through to nothing and never steal focus from the buttons. */\nconst TopBarTitle = styled.span<{ theme: Theme }>`\n position: absolute;\n left: 50%;\n transform: translateX(-50%);\n max-width: 60%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n pointer-events: none;\n font-family: ${({ theme }) => theme.fonts.mono};\n font-size: 12px;\n line-height: 2;\n color: ${({ theme }) => theme.colors.grayDark};\n`;\n\n/* Segmented control living in the TopBar for CodeTabs. Scrolls horizontally\n with the thin scrollbar when the labels overflow the 33px bar. */\nconst TabList = styled.div<{ theme: Theme }>`\n display: flex;\n align-items: center;\n gap: 2px;\n min-width: 0;\n overflow-x: auto;\n ${thinScrollbar};\n margin-left: -6px;\n`;\n\n/* Individual tab button. The active tab reads as part of the window: a\n theme.colors.light fill that echoes the code body, with a soft grayLight\n border. Inactive tabs are muted (grayDark) and transparent. resetButton\n strips native styling; focus-visible draws an inset brand primary ring on a\n pseudo-element so the scrolling TabList can't clip it. Colors come from theme\n tokens that swap for dark mode via the theme prop, so no :root.dark &\n override is needed. */\nconst CodeTab = styled.button<{ theme: Theme; $active: boolean }>`\n ${resetButton}\n flex: 0 0 auto;\n position: relative;\n font-family: ${({ theme }) => theme.fonts.mono};\n font-size: 12px;\n line-height: 1;\n padding: 5px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n cursor: pointer;\n white-space: nowrap;\n transition:\n color 0.15s ease,\n background 0.15s ease,\n border-color 0.15s ease;\n border: solid 1px\n ${({ theme, $active }) =>\n $active ? theme.colors.grayLight : \"transparent\"};\n background: ${({ theme, $active }) =>\n $active ? theme.colors.light : \"transparent\"};\n color: ${({ theme, $active }) =>\n $active ? theme.colors.dark : theme.colors.grayDark};\n\n &:hover {\n color: ${({ theme }) => theme.colors.dark};\n background: ${({ theme, $active }) =>\n $active\n ? theme.colors.light\n : `color-mix(in srgb, ${theme.colors.dark} 4%, transparent)`};\n }\n\n &:focus-visible {\n outline: none;\n }\n\n &:focus-visible::after {\n content: \"\";\n position: absolute;\n inset: 2px;\n border: solid 2px ${({ theme }) => theme.colors.primary};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n pointer-events: none;\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 min-width: min-content;\n width: 100%;\n display: block;\n padding-right: 20px;\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: transparent;\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 ${thinScrollbar};\n ${({ theme }) => styledCode(theme)};\n ${lightSyntaxHighlight};\n\n /* Diff lines: highlight.js wraps each +/- line in a single span, so\n stretching it to the full row reads like a GitHub diff. inline-block keeps\n the trailing newline as the line break instead of adding an empty row. */\n & .hljs-addition,\n & .hljs-deletion {\n display: inline-table;\n width: 100%;\n }\n\n :root.dark & {\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 5%, transparent)`};\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({\n code,\n language = \"javascript\",\n title,\n theme,\n className,\n}: 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 {title ? <TopBarTitle theme={theme}>{title}</TopBarTitle> : null}\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\n/* Multi-variant code block (npm / pnpm / yarn, etc). Tabs replace the macOS\n dots in the TopBar as a keyboard-accessible tablist; the copy button copies\n whichever tab is active and resets its copied state when the tab changes. */\nfunction CodeTabs({ tabs, theme, className }: CodeTabsProps) {\n const baseId = useId();\n const [active, setActive] = useState(0);\n const [copied, setCopied] = useState(false);\n\n const safeActive = active < tabs.length ? active : 0;\n const activeTab = tabs[safeActive];\n\n const highlightedCode = useMemo(\n () =>\n activeTab\n ? highlightCode(activeTab.code, activeTab.language ?? \"bash\")\n : \"\",\n [activeTab],\n );\n\n const handleSelect = useCallback((index: number) => {\n setActive(index);\n setCopied(false);\n }, []);\n\n const handleCopy = useCallback(async () => {\n if (!activeTab) return;\n try {\n await navigator.clipboard.writeText(activeTab.code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (err) {\n console.error(\"Failed to copy code:\", err);\n }\n }, [activeTab]);\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLButtonElement>) => {\n let next = safeActive;\n if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n next = (safeActive + 1) % tabs.length;\n } else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n next = (safeActive - 1 + tabs.length) % tabs.length;\n } else if (event.key === \"Home\") {\n next = 0;\n } else if (event.key === \"End\") {\n next = tabs.length - 1;\n } else {\n return;\n }\n event.preventDefault();\n handleSelect(next);\n const target = document.getElementById(`${baseId}-tab-${next}`);\n if (target) target.focus();\n },\n [baseId, handleSelect, safeActive, tabs.length],\n );\n\n if (!tabs || tabs.length === 0) return null;\n\n return (\n <CodeWrapper\n className={`${className ?? \"\"} code-wrapper`.trim()}\n theme={theme}\n >\n <TopBar theme={theme}>\n <TabList role=\"tablist\" aria-label=\"Code variants\">\n {tabs.map((tab, index) => (\n <CodeTab\n key={`${tab.label}-${index}`}\n type=\"button\"\n role=\"tab\"\n id={`${baseId}-tab-${index}`}\n aria-selected={index === safeActive}\n aria-controls={`${baseId}-panel`}\n tabIndex={index === safeActive ? 0 : -1}\n onClick={() => handleSelect(index)}\n onKeyDown={handleKeyDown}\n $active={index === safeActive}\n theme={theme}\n >\n {tab.label}\n </CodeTab>\n ))}\n </TabList>\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 id={`${baseId}-panel`}\n role=\"tabpanel\"\n aria-labelledby={`${baseId}-tab-${safeActive}`}\n dangerouslySetInnerHTML={{ __html: highlightedCode }}\n theme={theme}\n className=\"code-wrapper-body\"\n />\n </CodeWrapper>\n );\n}\n\nexport { Code, CodeTabs };\n";
|
|
@@ -44,23 +44,24 @@ const CodeWrapper = styled.span<{ theme: Theme }>\`
|
|
|
44
44
|
display: block;
|
|
45
45
|
width: 100%;
|
|
46
46
|
border-radius: \${({ theme }) => theme.spacing.radius.lg};
|
|
47
|
-
border: solid 1px
|
|
48
|
-
|
|
49
|
-
:root.dark & {
|
|
50
|
-
border-color: rgba(255, 255, 255, 0.2);
|
|
51
|
-
}
|
|
47
|
+
border: solid 1px \${({ theme }) => theme.colors.grayLight};
|
|
48
|
+
background: \${({ theme }) => theme.colors.light};
|
|
52
49
|
\`;
|
|
53
50
|
|
|
54
|
-
/* Code block uses a fixed GitHub-style palette
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
/* Code block uses a fixed GitHub-style palette for the syntax tokens in both
|
|
52
|
+
modes so highlighting stays legible regardless of brand colors. The dark-mode
|
|
53
|
+
surface, however, matches the left sidebar's translucent brand tint
|
|
54
|
+
(color-mix of theme.colors.primaryLight over the black page) instead of
|
|
55
|
+
GitHub's #0d1117, so the window sits on the same background as the nav. Dark
|
|
56
|
+
variants live in :root.dark & blocks so the swap happens via the active
|
|
57
|
+
<html> class with no re-render. */
|
|
58
58
|
const TopBar = styled.div<{ theme: Theme }>\`
|
|
59
59
|
position: relative;
|
|
60
|
-
background:
|
|
60
|
+
background: \${({ theme }) =>
|
|
61
|
+
\`color-mix(in srgb, \${theme.colors.primaryLight} 5%, transparent)\`};
|
|
61
62
|
border-top-left-radius: \${({ theme }) => theme.spacing.radius.lg};
|
|
62
63
|
border-top-right-radius: \${({ theme }) => theme.spacing.radius.lg};
|
|
63
|
-
border-bottom: solid 1px
|
|
64
|
+
border-bottom: solid 1px \${({ theme }) => theme.colors.grayLight};
|
|
64
65
|
height: 33px;
|
|
65
66
|
width: 100%;
|
|
66
67
|
display: flex;
|
|
@@ -68,11 +69,6 @@ const TopBar = styled.div<{ theme: Theme }>\`
|
|
|
68
69
|
align-items: center;
|
|
69
70
|
gap: 5px;
|
|
70
71
|
padding: 0 10px;
|
|
71
|
-
|
|
72
|
-
:root.dark & {
|
|
73
|
-
background: #0d1117;
|
|
74
|
-
border-bottom-color: rgba(255, 255, 255, 0.1);
|
|
75
|
-
}
|
|
76
72
|
\`;
|
|
77
73
|
|
|
78
74
|
const DotsContainer = styled.div\`
|
|
@@ -92,16 +88,15 @@ const Dot = styled.span<{ theme: Theme }>\`
|
|
|
92
88
|
\`;
|
|
93
89
|
|
|
94
90
|
/* Icon-only copy button. interactiveStyles supplies the border highlight on
|
|
95
|
-
hover plus the focus/active rings (no scale effect)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
91
|
+
hover plus the focus/active rings (no scale effect). Colors come from theme
|
|
92
|
+
tokens (grayLight border, success/grayDark icon) that swap for dark mode via
|
|
93
|
+
the theme prop, so no :root.dark & override is needed and the copied state
|
|
94
|
+
reads consistently in both modes. */
|
|
99
95
|
const CopyButton = styled.button<{ theme: Theme; $copied: boolean }>\`
|
|
100
96
|
\${resetButton}
|
|
101
97
|
\${interactiveStyles}
|
|
102
|
-
background: \${({
|
|
103
|
-
|
|
104
|
-
border-color: \${({ $copied }) => ($copied ? "#2da44e" : "rgba(0, 0, 0, 0.1)")};
|
|
98
|
+
background: \${({ theme }) => theme.colors.light};
|
|
99
|
+
border-color: \${({ theme }) => theme.colors.grayLight};
|
|
105
100
|
border-radius: \${({ theme }) => theme.spacing.radius.xs};
|
|
106
101
|
padding: 4px;
|
|
107
102
|
display: flex;
|
|
@@ -110,29 +105,16 @@ const CopyButton = styled.button<{ theme: Theme; $copied: boolean }>\`
|
|
|
110
105
|
margin-right: -6px;
|
|
111
106
|
|
|
112
107
|
& svg.lucide {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
:root.dark & {
|
|
117
|
-
background: \${({ $copied }) =>
|
|
118
|
-
$copied ? "rgba(126, 231, 135, 0.2)" : "transparent"};
|
|
119
|
-
border-color: \${({ $copied }) =>
|
|
120
|
-
$copied ? "#7ee787" : "rgba(255, 255, 255, 0.1)"};
|
|
121
|
-
|
|
122
|
-
& svg.lucide {
|
|
123
|
-
color: \${({ $copied }) => ($copied ? "#7ee787" : "#c9d1d9")};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
&:hover {
|
|
127
|
-
border-color: \${({ theme }) => theme.colors.primary};
|
|
128
|
-
}
|
|
108
|
+
margin: 0;
|
|
109
|
+
color: \${({ theme, $copied }) =>
|
|
110
|
+
$copied ? theme.colors.success : theme.colors.grayDark};
|
|
129
111
|
}
|
|
130
112
|
\`;
|
|
131
113
|
|
|
132
114
|
/* Centered file name in the TopBar. Sits absolutely over the flex row so the
|
|
133
|
-
dots and copy button keep their edge alignment. Monospace + muted
|
|
134
|
-
|
|
135
|
-
|
|
115
|
+
dots and copy button keep their edge alignment. Monospace + a muted grayDark
|
|
116
|
+
token that swaps for dark mode via the theme prop. pointer-events off so
|
|
117
|
+
clicks fall through to nothing and never steal focus from the buttons. */
|
|
136
118
|
const TopBarTitle = styled.span<{ theme: Theme }>\`
|
|
137
119
|
position: absolute;
|
|
138
120
|
left: 50%;
|
|
@@ -145,11 +127,7 @@ const TopBarTitle = styled.span<{ theme: Theme }>\`
|
|
|
145
127
|
font-family: \${({ theme }) => theme.fonts.mono};
|
|
146
128
|
font-size: 12px;
|
|
147
129
|
line-height: 2;
|
|
148
|
-
color:
|
|
149
|
-
|
|
150
|
-
:root.dark & {
|
|
151
|
-
color: #8b949e;
|
|
152
|
-
}
|
|
130
|
+
color: \${({ theme }) => theme.colors.grayDark};
|
|
153
131
|
\`;
|
|
154
132
|
|
|
155
133
|
/* Segmented control living in the TopBar for CodeTabs. Scrolls horizontally
|
|
@@ -164,12 +142,13 @@ const TabList = styled.div<{ theme: Theme }>\`
|
|
|
164
142
|
margin-left: -6px;
|
|
165
143
|
\`;
|
|
166
144
|
|
|
167
|
-
/* Individual tab button.
|
|
168
|
-
fill
|
|
169
|
-
are muted and transparent. resetButton
|
|
170
|
-
draws an inset brand primary ring on a
|
|
171
|
-
TabList can't clip it.
|
|
172
|
-
|
|
145
|
+
/* Individual tab button. The active tab reads as part of the window: a
|
|
146
|
+
theme.colors.light fill that echoes the code body, with a soft grayLight
|
|
147
|
+
border. Inactive tabs are muted (grayDark) and transparent. resetButton
|
|
148
|
+
strips native styling; focus-visible draws an inset brand primary ring on a
|
|
149
|
+
pseudo-element so the scrolling TabList can't clip it. Colors come from theme
|
|
150
|
+
tokens that swap for dark mode via the theme prop, so no :root.dark &
|
|
151
|
+
override is needed. */
|
|
173
152
|
const CodeTab = styled.button<{ theme: Theme; $active: boolean }>\`
|
|
174
153
|
\${resetButton}
|
|
175
154
|
flex: 0 0 auto;
|
|
@@ -186,14 +165,19 @@ const CodeTab = styled.button<{ theme: Theme; $active: boolean }>\`
|
|
|
186
165
|
background 0.15s ease,
|
|
187
166
|
border-color 0.15s ease;
|
|
188
167
|
border: solid 1px
|
|
189
|
-
\${({ $active }) =>
|
|
190
|
-
|
|
191
|
-
|
|
168
|
+
\${({ theme, $active }) =>
|
|
169
|
+
$active ? theme.colors.grayLight : "transparent"};
|
|
170
|
+
background: \${({ theme, $active }) =>
|
|
171
|
+
$active ? theme.colors.light : "transparent"};
|
|
172
|
+
color: \${({ theme, $active }) =>
|
|
173
|
+
$active ? theme.colors.dark : theme.colors.grayDark};
|
|
192
174
|
|
|
193
175
|
&:hover {
|
|
194
|
-
color:
|
|
195
|
-
background: \${({ $active }) =>
|
|
196
|
-
$active
|
|
176
|
+
color: \${({ theme }) => theme.colors.dark};
|
|
177
|
+
background: \${({ theme, $active }) =>
|
|
178
|
+
$active
|
|
179
|
+
? theme.colors.light
|
|
180
|
+
: \`color-mix(in srgb, \${theme.colors.dark} 4%, transparent)\`};
|
|
197
181
|
}
|
|
198
182
|
|
|
199
183
|
&:focus-visible {
|
|
@@ -208,19 +192,6 @@ const CodeTab = styled.button<{ theme: Theme; $active: boolean }>\`
|
|
|
208
192
|
border-radius: \${({ theme }) => theme.spacing.radius.xs};
|
|
209
193
|
pointer-events: none;
|
|
210
194
|
}
|
|
211
|
-
|
|
212
|
-
:root.dark & {
|
|
213
|
-
border-color: \${({ $active }) =>
|
|
214
|
-
$active ? "rgba(255, 255, 255, 0.15)" : "transparent"};
|
|
215
|
-
background: \${({ $active }) => ($active ? "#161b22" : "transparent")};
|
|
216
|
-
color: \${({ $active }) => ($active ? "#e6edf3" : "#8b949e")};
|
|
217
|
-
|
|
218
|
-
&:hover {
|
|
219
|
-
color: #e6edf3;
|
|
220
|
-
background: \${({ $active }) =>
|
|
221
|
-
$active ? "#161b22" : "rgba(255, 255, 255, 0.06)"};
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
195
|
\`;
|
|
225
196
|
|
|
226
197
|
/* GitHub Light syntax highlighting by default; GitHub Dark in :root.dark.
|
|
@@ -313,7 +284,7 @@ const lightSyntaxHighlight = css\`
|
|
|
313
284
|
const darkSyntaxHighlight = css\`
|
|
314
285
|
& .hljs {
|
|
315
286
|
color: #c9d1d9;
|
|
316
|
-
background:
|
|
287
|
+
background: transparent;
|
|
317
288
|
}
|
|
318
289
|
& .hljs-doctag,
|
|
319
290
|
& .hljs-keyword,
|
|
@@ -415,7 +386,8 @@ const Body = styled.div<{ theme: Theme }>\`
|
|
|
415
386
|
}
|
|
416
387
|
|
|
417
388
|
:root.dark & {
|
|
418
|
-
background:
|
|
389
|
+
background: \${({ theme }) =>
|
|
390
|
+
\`color-mix(in srgb, \${theme.colors.primaryLight} 5%, transparent)\`};
|
|
419
391
|
color: #ffffff;
|
|
420
392
|
\${darkSyntaxHighlight};
|
|
421
393
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const globalStylesTemplate = "\"use client\";\nimport { createGlobalStyle } from \"styled-components\";\nimport {\n colorsLight,\n colorsDark,\n shadowsLight,\n shadowsDark,\n} from \"@/app/theme\";\n\n// Build \"name: value;\" lines for every entry in a record. Used to emit\n// :root and :root.dark blocks from the resolved hex objects in theme.ts.\n// Custom theme.json overrides flow through automatically because they are\n// already merged into colorsLight / colorsDark.\nfunction toCssVars<T extends object>(prefix: string, record: T): string {\n return Object.entries(record)\n .map(([k, v]) => ` --${prefix}-${k}: ${v};`)\n .join(\"\\n\");\n}\n\nconst lightVars = [\n toCssVars(\"color\", colorsLight),\n toCssVars(\"shadow\", shadowsLight),\n // Semantic tokens \u2014 derived from the brand palette per mode. See the\n // SemanticColors interface in theme.ts for the intent of each.\n // These reference the palette via var() (not baked hex) so that any\n // runtime override of --color-primary* (e.g. the DemoTheme presets)\n // cascades into the semantic tokens automatically.\n ` --color-accent: var(--color-primaryDark);`,\n // accentStrong = accent shifted ~10% toward black (light mode) or white (dark\n // mode). color-mix in sRGB is not identical to polished's HSL darken/lighten\n // but the visual difference at 10% on a UI accent is imperceptible.\n ` --color-accentStrong: color-mix(in srgb, var(--color-primaryDark) 90%, black);`,\n ` --color-accentMuted: var(--color-primary);`,\n ` --color-surface: var(--color-light);`,\n].join(\"\\n\");\n\nconst darkVars = [\n toCssVars(\"color\", colorsDark),\n toCssVars(\"shadow\", shadowsDark),\n ` --color-accent: var(--color-primaryLight);`,\n ` --color-accentStrong: color-mix(in srgb, var(--color-primaryLight) 90%, white);`,\n ` --color-accentMuted: var(--color-grayDark);`,\n ` --color-surface: var(--color-dark);`,\n].join(\"\\n\");\n\nconst GlobalStyles = createGlobalStyle`\n:root {\n${lightVars}\n}\n\n:root.dark {\n${darkVars}\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n min-height: 100%;\n background-color: var(--color-light);\n scroll-padding-top: 80px;\n}\n\nhtml:has(:target) {\n scroll-behavior: smooth;\n}\n\nbody {\n font-family: \"Inter\", sans-serif;\n -moz-osx-font-smoothing: grayscale;\n -webkit-text-size-adjust: 100%;\n -webkit-font-smoothing: antialiased;\n}\n\n:root {\n interpolate-size: allow-keywords;\n}\n\n* {\n box-sizing: border-box;\n min-width: 0;\n}\n\nhr {\n border: none;\n border-bottom: solid 1px var(--color-grayLight);\n margin: 10px 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace;\n}\n\npre,\ncode,\nkbd,\nsamp,\nblockquote,\np,\na,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nul li,\nol li {\n margin: 0;\n padding: 0;\n color: var(--color-dark);\n}\n\na {\n color: var(--color-primary);\n}\n\n/* Keyboard-focus ring, defined once globally so links don't each re-declare it.\n :focus-visible keeps it keyboard-only (no ring on mouse click); box-shadow\n gives a soft primaryLight glow that follows the element's border-radius. The\n 6px radius mirrors theme.spacing.radius.xs so the ring rounds on links that\n have no radius of their own. Cherry link-buttons (.button-link) are excluded\n via :where() so the exclusion adds no specificity - this keeps the selector\n at (0,1,1) so bespoke per-component :focus-visible rules (inline content\n links, the section bar, the sidebar group rows) still override it. */\na:focus-visible:where(:not(.button-link)) {\n outline: none;\n border-radius: 6px;\n box-shadow: 0 0 0 2px var(--color-primaryLight);\n}\n\nol,\nul {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\nfigure {\n margin: 0;\n}\n\nfieldset {\n appearance: none;\n border: none;\n}\n\nbutton,\ninput,\na,\nimg,\nsvg,\nsvg * {\n transition: all 0.3s ease;\n}\n\nstrong,\nb {\n font-weight: 700;\n}\n\n.full-width {\n width: 100%;\n}\n\n/* Mode-conditional visibility helpers. ThemeToggle uses these to swap Sun\n and Moon icons; Header uses them to swap light/dark logos. Pure CSS so\n the swap happens via the active <html> class without JS or re-render. */\n:root.dark .light-only {\n display: none !important;\n}\n:root:not(.dark) .dark-only {\n display: none !important;\n}\n`;\n\nexport { GlobalStyles };\n";
|
|
1
|
+
export declare const globalStylesTemplate = "\"use client\";\nimport { createGlobalStyle } from \"styled-components\";\nimport {\n colorsLight,\n colorsDark,\n shadowsLight,\n shadowsDark,\n} from \"@/app/theme\";\n\n// Build \"name: value;\" lines for every entry in a record. Used to emit\n// :root and :root.dark blocks from the resolved hex objects in theme.ts.\n// Custom theme.json overrides flow through automatically because they are\n// already merged into colorsLight / colorsDark.\nfunction toCssVars<T extends object>(prefix: string, record: T): string {\n return Object.entries(record)\n .map(([k, v]) => ` --${prefix}-${k}: ${v};`)\n .join(\"\\n\");\n}\n\nconst lightVars = [\n toCssVars(\"color\", colorsLight),\n toCssVars(\"shadow\", shadowsLight),\n // Semantic tokens \u2014 derived from the brand palette per mode. See the\n // SemanticColors interface in theme.ts for the intent of each.\n // These reference the palette via var() (not baked hex) so that any\n // runtime override of --color-primary* (e.g. the DemoTheme presets)\n // cascades into the semantic tokens automatically.\n ` --color-accent: var(--color-primaryDark);`,\n // accentStrong = accent shifted ~10% toward black (light mode) or white (dark\n // mode). color-mix in sRGB is not identical to polished's HSL darken/lighten\n // but the visual difference at 10% on a UI accent is imperceptible.\n ` --color-accentStrong: color-mix(in srgb, var(--color-primaryDark) 90%, black);`,\n ` --color-accentMuted: var(--color-primary);`,\n ` --color-surface: var(--color-light);`,\n].join(\"\\n\");\n\nconst darkVars = [\n toCssVars(\"color\", colorsDark),\n toCssVars(\"shadow\", shadowsDark),\n ` --color-accent: var(--color-primaryLight);`,\n ` --color-accentStrong: color-mix(in srgb, var(--color-primaryLight) 90%, white);`,\n ` --color-accentMuted: var(--color-grayDark);`,\n ` --color-surface: var(--color-dark);`,\n].join(\"\\n\");\n\nconst GlobalStyles = createGlobalStyle`\n:root {\n${lightVars}\n}\n\n:root.dark {\n${darkVars}\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n min-height: 100%;\n background-color: var(--color-light);\n scroll-padding-top: 80px;\n}\n\nhtml:has(:target) {\n scroll-behavior: smooth;\n}\n\nbody {\n font-family: \"Inter\", sans-serif;\n -moz-osx-font-smoothing: grayscale;\n -webkit-text-size-adjust: 100%;\n -webkit-font-smoothing: antialiased;\n}\n\n:root {\n interpolate-size: allow-keywords;\n}\n\n* {\n box-sizing: border-box;\n min-width: 0;\n}\n\nhr {\n border: none;\n border-bottom: solid 1px var(--color-grayLight);\n margin: 10px 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace;\n}\n\npre,\ncode,\nkbd,\nsamp,\nblockquote,\np,\na,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nul li,\nol li {\n margin: 0;\n padding: 0;\n color: var(--color-dark);\n}\n\na {\n color: var(--color-primary);\n}\n\n/* Keyboard-focus ring, defined once globally so links don't each re-declare it.\n :focus-visible keeps it keyboard-only (no ring on mouse click); box-shadow\n gives a soft primaryLight glow that follows the element's border-radius. The\n 6px radius mirrors theme.spacing.radius.xs so the ring rounds on links that\n have no radius of their own. Cherry link-buttons (.button-link) are excluded\n via :where() so the exclusion adds no specificity - this keeps the selector\n at (0,1,1) so bespoke per-component :focus-visible rules (inline content\n links, the section bar, the sidebar group rows) still override it. */\na:focus-visible:where(:not(.button-link)) {\n outline: none;\n border-radius: 6px;\n box-shadow: 0 0 0 2px var(--color-primaryLight);\n}\n\nol,\nul {\n list-style: none;\n margin: 0;\n padding: 0;\n max-width: 100%;\n}\n\nfigure {\n margin: 0;\n}\n\nfieldset {\n appearance: none;\n border: none;\n}\n\nbutton,\ninput,\na,\nimg,\nsvg,\nsvg * {\n transition: all 0.3s ease;\n}\n\nstrong,\nb {\n font-weight: 700;\n}\n\n.full-width {\n width: 100%;\n}\n\n/* Mode-conditional visibility helpers. ThemeToggle uses these to swap Sun\n and Moon icons; Header uses them to swap light/dark logos. Pure CSS so\n the swap happens via the active <html> class without JS or re-render. */\n:root.dark .light-only {\n display: none !important;\n}\n:root:not(.dark) .dark-only {\n display: none !important;\n}\n`;\n\nexport { GlobalStyles };\n";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const sharedStyledTemplate = "\"use client\";\nimport { styledSmall, styledText } from \"cherry-styled-components\";\nimport { mq, Theme } from \"@/app/theme\";\nimport styled, { css } from \"styled-components\";\n\n/** Slim, theme-aware scrollbar for internal scroll areas (code blocks,\n modals, tables) so the chunky native bar doesn't stand out, especially\n in dark mode. */\nexport const thinScrollbar = css<{ theme: Theme }>`\n scrollbar-width: thin;\n scrollbar-color: ${({ theme }) => theme.colors.grayLight} transparent;\n`;\n\nexport const interactiveStyles = css<{ theme: Theme }>`\n transition: all 0.3s ease;\n border: solid 1px transparent;\n box-shadow: 0 0 0 0px ${({ theme }) => theme.colors.primary};\n\n &:hover {\n border-color: ${({ theme }) => theme.colors.primary};\n }\n\n &:focus {\n border-color: ${({ theme }) => theme.colors.primary};\n box-shadow: 0 0 0 4px ${({ theme }) => theme.colors.primaryLight};\n }\n\n &:active {\n box-shadow: 0 0 0 2px ${({ theme }) => theme.colors.primaryLight};\n }\n`;\n\nexport const styledAnchor = css<{ theme: Theme }>`\n & a:not([class]):not(:has(img)) {\n color: inherit;\n transition: all 0.3s ease;\n text-decoration: none;\n box-shadow: 0 2px 0 0 ${({ theme }) => theme.colors.primary};\n\n &:hover {\n color: ${({ theme }) => theme.colors.accent};\n box-shadow: 0 1px 0 0 ${({ theme }) => theme.colors.primary};\n }\n\n /* Same primaryLight focus glow as the global a:focus-visible ring, but\n layered over the link's own box-shadow underline (same property) so the\n underline is preserved. */\n &:focus-visible {\n outline: none;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n box-shadow:\n 0 2px 0 0 ${({ theme }) => theme.colors.primary},\n 0 0 0 2px ${({ theme }) => theme.colors.primaryLight};\n }\n }\n`;\n\nexport const stylesLists = css<{ theme: Theme }>`\n & ul,\n & ol {\n & li {\n & > .code-wrapper {\n margin: 10px 0;\n }\n }\n }\n\n & ul {\n list-style: none;\n padding: 0;\n margin: 0;\n\n & li {\n text-indent: 0;\n display: block;\n position: relative;\n padding: 0 0 0 15px;\n margin: 0;\n ${({ theme }) => styledText(theme)};\n min-height: 23px;\n\n ${mq(\"lg\")} {\n min-height: 27px;\n }\n\n &::before {\n content: \"\";\n display: block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: ${({ theme }) => theme.colors.primary};\n position: absolute;\n top: 8px;\n left: 2px;\n\n ${mq(\"lg\")} {\n top: 10px;\n }\n }\n }\n }\n\n & ol {\n padding: 0;\n margin: 0;\n\n & ul {\n padding-left: 15px;\n }\n\n & > li {\n position: relative;\n padding: 0;\n counter-increment: item;\n margin: 0;\n ${({ theme }) => styledText(theme)};\n\n &::before {\n content: counter(item) \".\";\n
|
|
1
|
+
export declare const sharedStyledTemplate = "\"use client\";\nimport { styledSmall, styledText } from \"cherry-styled-components\";\nimport { mq, Theme } from \"@/app/theme\";\nimport styled, { css } from \"styled-components\";\n\n/** Slim, theme-aware scrollbar for internal scroll areas (code blocks,\n modals, tables) so the chunky native bar doesn't stand out, especially\n in dark mode. */\nexport const thinScrollbar = css<{ theme: Theme }>`\n scrollbar-width: thin;\n scrollbar-color: ${({ theme }) => theme.colors.grayLight} transparent;\n`;\n\nexport const interactiveStyles = css<{ theme: Theme }>`\n transition: all 0.3s ease;\n border: solid 1px transparent;\n box-shadow: 0 0 0 0px ${({ theme }) => theme.colors.primary};\n\n &:hover {\n border-color: ${({ theme }) => theme.colors.primary};\n }\n\n &:focus {\n border-color: ${({ theme }) => theme.colors.primary};\n box-shadow: 0 0 0 4px ${({ theme }) => theme.colors.primaryLight};\n }\n\n &:active {\n box-shadow: 0 0 0 2px ${({ theme }) => theme.colors.primaryLight};\n }\n`;\n\nexport const styledAnchor = css<{ theme: Theme }>`\n & a:not([class]):not(:has(img)) {\n color: inherit;\n transition: all 0.3s ease;\n text-decoration: none;\n box-shadow: 0 2px 0 0 ${({ theme }) => theme.colors.primary};\n\n &:hover {\n color: ${({ theme }) => theme.colors.accent};\n box-shadow: 0 1px 0 0 ${({ theme }) => theme.colors.primary};\n }\n\n /* Same primaryLight focus glow as the global a:focus-visible ring, but\n layered over the link's own box-shadow underline (same property) so the\n underline is preserved. */\n &:focus-visible {\n outline: none;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n box-shadow:\n 0 2px 0 0 ${({ theme }) => theme.colors.primary},\n 0 0 0 2px ${({ theme }) => theme.colors.primaryLight};\n }\n }\n`;\n\nexport const stylesLists = css<{ theme: Theme }>`\n & ul,\n & ol {\n & li {\n & > .code-wrapper {\n margin: 10px 0;\n }\n\n & p {\n display: inline;\n }\n }\n }\n\n & ul {\n list-style: none;\n padding: 0;\n margin: 0;\n\n & li {\n text-indent: 0;\n display: block;\n position: relative;\n padding: 0 0 0 15px;\n margin: 0;\n ${({ theme }) => styledText(theme)};\n min-height: 23px;\n\n ${mq(\"lg\")} {\n min-height: 27px;\n }\n\n &::before {\n content: \"\";\n display: block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: ${({ theme }) => theme.colors.primary};\n position: absolute;\n top: 8px;\n left: 2px;\n\n ${mq(\"lg\")} {\n top: 10px;\n }\n }\n }\n }\n\n & ol {\n padding: 0;\n margin: 0;\n\n & ul {\n padding-left: 15px;\n }\n\n & > li {\n position: relative;\n padding: 0 0 0 24px;\n counter-increment: item;\n margin: 0;\n ${({ theme }) => styledText(theme)};\n\n &::before {\n content: counter(item) \".\";\n position: absolute;\n left: 2px;\n font-weight: 700;\n color: ${({ theme }) => theme.colors.primary};\n }\n }\n }\n`;\n\nexport const styledTable = css<{ theme: Theme }>`\n & .table-wrapper {\n overflow-x: auto;\n width: 100%;\n ${thinScrollbar};\n }\n\n & table {\n margin: 0;\n padding: 0;\n border-collapse: collapse;\n width: 100%;\n text-align: left;\n\n & tr {\n margin: 0;\n padding: 0;\n }\n\n & th {\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n padding: 10px 10px 10px 0;\n ${({ theme }) => styledSmall(theme)};\n font-weight: 600;\n color: ${({ theme }) => theme.colors.dark};\n }\n\n & td {\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n padding: 10px 10px 10px 0;\n color: ${({ theme }) => theme.colors.grayDark};\n ${({ theme }) => styledSmall(theme)};\n }\n }\n`;\n\nexport const StyledSmallButton = styled.button<{ theme: Theme }>`\n ${interactiveStyles};\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n color: ${({ theme }) => theme.colors.accent};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n padding: 6px 8px;\n font-size: 12px;\n font-family: inherit;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s ease;\n display: flex;\n align-items: center;\n gap: 6px;\n margin-right: -6px;\n\n & svg.lucide {\n color: inherit;\n }\n`;\n";
|
|
@@ -62,6 +62,10 @@ export const stylesLists = css<{ theme: Theme }>\`
|
|
|
62
62
|
& > .code-wrapper {
|
|
63
63
|
margin: 10px 0;
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
& p {
|
|
67
|
+
display: inline;
|
|
68
|
+
}
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -111,18 +115,17 @@ export const stylesLists = css<{ theme: Theme }>\`
|
|
|
111
115
|
|
|
112
116
|
& > li {
|
|
113
117
|
position: relative;
|
|
114
|
-
padding: 0;
|
|
118
|
+
padding: 0 0 0 24px;
|
|
115
119
|
counter-increment: item;
|
|
116
120
|
margin: 0;
|
|
117
121
|
\${({ theme }) => styledText(theme)};
|
|
118
122
|
|
|
119
123
|
&::before {
|
|
120
124
|
content: counter(item) ".";
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
position: absolute;
|
|
126
|
+
left: 2px;
|
|
123
127
|
font-weight: 700;
|
|
124
128
|
color: \${({ theme }) => theme.colors.primary};
|
|
125
|
-
min-width: max-content;
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
}
|
|
@@ -12,19 +12,19 @@ export const packageJsonTemplate = JSON.stringify({
|
|
|
12
12
|
},
|
|
13
13
|
dependencies: {
|
|
14
14
|
"@langchain/anthropic": "^1.5.1",
|
|
15
|
-
"@langchain/core": "^1.2.
|
|
15
|
+
"@langchain/core": "^1.2.2",
|
|
16
16
|
"@langchain/google-genai": "^2.2.0",
|
|
17
|
-
"@langchain/openai": "^1.5.
|
|
17
|
+
"@langchain/openai": "^1.5.5",
|
|
18
18
|
"@mdx-js/react": "^3.1.1",
|
|
19
19
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
20
20
|
"@posthog/react": "^1.10.3",
|
|
21
21
|
"cherry-styled-components": "^0.2.10",
|
|
22
22
|
langchain: "^1.5.3",
|
|
23
|
-
"lucide-react": "^1.
|
|
23
|
+
"lucide-react": "^1.24.0",
|
|
24
24
|
minisearch: "^7.2.0",
|
|
25
25
|
next: "16.2.10",
|
|
26
26
|
"next-mdx-remote": "^6.0.0",
|
|
27
|
-
"posthog-js": "^1.399.
|
|
27
|
+
"posthog-js": "^1.399.1",
|
|
28
28
|
"posthog-node": "^5.40.0",
|
|
29
29
|
react: "19.2.7",
|
|
30
30
|
"react-dom": "19.2.7",
|
|
@@ -50,7 +50,7 @@ export const packageJsonTemplate = JSON.stringify({
|
|
|
50
50
|
"eslint-plugin-react": "^7.37.5",
|
|
51
51
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
52
52
|
globals: "^17.7.0",
|
|
53
|
-
prettier: "^3.9.
|
|
53
|
+
prettier: "^3.9.5",
|
|
54
54
|
typescript: "npm:@typescript/typescript6@^6.0.2",
|
|
55
55
|
},
|
|
56
56
|
}, null, 2) + "\n";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doccupine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.116",
|
|
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": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@types/fs-extra": "^11.0.4",
|
|
49
49
|
"@types/node": "^26.1.1",
|
|
50
50
|
"@types/prompts": "^2.4.9",
|
|
51
|
-
"prettier": "^3.9.
|
|
51
|
+
"prettier": "^3.9.5",
|
|
52
52
|
"typescript": "^7.0.2",
|
|
53
53
|
"vitest": "^4.1.10"
|
|
54
54
|
},
|