create-unmint 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import chalk4 from "chalk";
5
+ import chalk5 from "chalk";
6
6
  import figlet from "figlet";
7
7
  import gradient from "gradient-string";
8
+ import fs7 from "fs-extra";
9
+ import path7 from "path";
8
10
 
9
11
  // src/commands/init.ts
10
12
  import fs2 from "fs-extra";
@@ -109,6 +111,66 @@ function getDefaultConfig(projectName) {
109
111
  installDeps: true
110
112
  };
111
113
  }
114
+ async function promptAddConfig(hasExistingDocs, customPath) {
115
+ const answers = await inquirer.prompt([
116
+ {
117
+ type: "input",
118
+ name: "docsRoute",
119
+ message: "Add Unmint docs at which route?",
120
+ default: customPath || (hasExistingDocs ? "/documentation" : "/docs"),
121
+ validate: (input) => {
122
+ if (!input.startsWith("/")) return "Route must start with /";
123
+ if (!/^\/[a-z0-9-/]*$/i.test(input)) {
124
+ return "Route can only contain letters, numbers, hyphens, and slashes";
125
+ }
126
+ return true;
127
+ }
128
+ },
129
+ {
130
+ type: "input",
131
+ name: "title",
132
+ message: "Docs title:",
133
+ default: "Documentation"
134
+ },
135
+ {
136
+ type: "input",
137
+ name: "description",
138
+ message: "Docs description:",
139
+ default: "Documentation for your project"
140
+ },
141
+ {
142
+ type: "list",
143
+ name: "accentColor",
144
+ message: "Accent color:",
145
+ choices: accentColors.map((c) => ({
146
+ name: c.name === "Cyan" ? `${c.name} ${chalk.dim("(default)")}` : c.name,
147
+ value: c.value
148
+ })),
149
+ default: "#0891b2"
150
+ },
151
+ {
152
+ type: "input",
153
+ name: "customAccent",
154
+ message: "Custom accent color (hex):",
155
+ when: (answers2) => answers2.accentColor === "custom",
156
+ validate: (input) => {
157
+ if (!/^#[0-9a-f]{6}$/i.test(input)) {
158
+ return "Please enter a valid hex color (e.g., #ff5733)";
159
+ }
160
+ return true;
161
+ }
162
+ }
163
+ ]);
164
+ if (answers.accentColor === "custom" && answers.customAccent) {
165
+ answers.accentColor = answers.customAccent;
166
+ }
167
+ return {
168
+ docsRoute: answers.docsRoute,
169
+ title: answers.title,
170
+ description: answers.description,
171
+ accentColor: answers.accentColor
172
+ };
173
+ }
112
174
 
113
175
  // src/scaffold.ts
114
176
  import fs from "fs-extra";
@@ -488,6 +550,588 @@ async function applyUpdates(projectDir, changes) {
488
550
  return results;
489
551
  }
490
552
 
553
+ // src/commands/add.ts
554
+ import chalk4 from "chalk";
555
+ import ora3 from "ora";
556
+ import path6 from "path";
557
+ import fs6 from "fs-extra";
558
+ import { execa as execa2 } from "execa";
559
+
560
+ // src/utils/detect-project.ts
561
+ import fs4 from "fs-extra";
562
+ import path4 from "path";
563
+ async function detectProject(cwd) {
564
+ const result = {
565
+ isExistingProject: false,
566
+ framework: "unknown",
567
+ useSrcDir: false,
568
+ appDir: "app",
569
+ hasExistingDocs: false,
570
+ hasFumadocs: false,
571
+ packageManager: "npm",
572
+ nextConfigPath: null,
573
+ globalsCssPath: null,
574
+ tailwindConfigPath: null
575
+ };
576
+ const packageJsonPath = path4.join(cwd, "package.json");
577
+ if (!await fs4.pathExists(packageJsonPath)) {
578
+ return result;
579
+ }
580
+ result.isExistingProject = true;
581
+ const nextConfigExtensions = ["js", "ts", "mjs"];
582
+ for (const ext of nextConfigExtensions) {
583
+ const configPath = path4.join(cwd, `next.config.${ext}`);
584
+ if (await fs4.pathExists(configPath)) {
585
+ result.nextConfigPath = configPath;
586
+ break;
587
+ }
588
+ }
589
+ if (!result.nextConfigPath) {
590
+ return result;
591
+ }
592
+ const srcAppPath = path4.join(cwd, "src/app");
593
+ const appPath = path4.join(cwd, "app");
594
+ if (await fs4.pathExists(srcAppPath)) {
595
+ result.useSrcDir = true;
596
+ result.appDir = "src/app";
597
+ result.framework = "next-app";
598
+ } else if (await fs4.pathExists(appPath)) {
599
+ result.useSrcDir = false;
600
+ result.appDir = "app";
601
+ result.framework = "next-app";
602
+ } else {
603
+ const pagesPath = path4.join(cwd, "pages");
604
+ const srcPagesPath = path4.join(cwd, "src/pages");
605
+ if (await fs4.pathExists(pagesPath) || await fs4.pathExists(srcPagesPath)) {
606
+ result.framework = "next-pages";
607
+ }
608
+ return result;
609
+ }
610
+ const docsPath = path4.join(cwd, result.appDir, "docs");
611
+ result.hasExistingDocs = await fs4.pathExists(docsPath);
612
+ try {
613
+ const pkg = await fs4.readJson(packageJsonPath);
614
+ const allDeps = {
615
+ ...pkg.dependencies,
616
+ ...pkg.devDependencies
617
+ };
618
+ result.hasFumadocs = "fumadocs-core" in allDeps || "fumadocs-mdx" in allDeps;
619
+ } catch {
620
+ }
621
+ result.packageManager = await detectPackageManager2(cwd);
622
+ const globalsCssPaths = [
623
+ path4.join(cwd, result.appDir, "globals.css"),
624
+ path4.join(cwd, "src/styles/globals.css"),
625
+ path4.join(cwd, "styles/globals.css")
626
+ ];
627
+ for (const cssPath of globalsCssPaths) {
628
+ if (await fs4.pathExists(cssPath)) {
629
+ result.globalsCssPath = cssPath;
630
+ break;
631
+ }
632
+ }
633
+ const tailwindConfigExtensions = ["ts", "js", "mjs"];
634
+ for (const ext of tailwindConfigExtensions) {
635
+ const configPath = path4.join(cwd, `tailwind.config.${ext}`);
636
+ if (await fs4.pathExists(configPath)) {
637
+ result.tailwindConfigPath = configPath;
638
+ break;
639
+ }
640
+ }
641
+ return result;
642
+ }
643
+ async function detectPackageManager2(cwd) {
644
+ if (await fs4.pathExists(path4.join(cwd, "pnpm-lock.yaml"))) {
645
+ return "pnpm";
646
+ }
647
+ if (await fs4.pathExists(path4.join(cwd, "yarn.lock"))) {
648
+ return "yarn";
649
+ }
650
+ if (await fs4.pathExists(path4.join(cwd, "bun.lockb"))) {
651
+ return "bun";
652
+ }
653
+ const userAgent = process.env.npm_config_user_agent || "";
654
+ if (userAgent.includes("pnpm")) return "pnpm";
655
+ if (userAgent.includes("yarn")) return "yarn";
656
+ if (userAgent.includes("bun")) return "bun";
657
+ return "npm";
658
+ }
659
+ function validateProjectForAdd(info) {
660
+ if (!info.isExistingProject) {
661
+ return {
662
+ valid: false,
663
+ error: 'No package.json found. Use "npx create-unmint my-docs" to create a new project.'
664
+ };
665
+ }
666
+ if (!info.nextConfigPath) {
667
+ return {
668
+ valid: false,
669
+ error: "No Next.js config found. This directory does not appear to be a Next.js project."
670
+ };
671
+ }
672
+ if (info.framework === "next-pages") {
673
+ return {
674
+ valid: false,
675
+ error: "Unmint requires App Router. Your project appears to use Pages Router."
676
+ };
677
+ }
678
+ if (info.framework === "unknown") {
679
+ return {
680
+ valid: false,
681
+ error: "Could not detect app directory. Make sure your project uses the Next.js App Router."
682
+ };
683
+ }
684
+ return { valid: true };
685
+ }
686
+
687
+ // src/utils/merge.ts
688
+ import fs5 from "fs-extra";
689
+ import path5 from "path";
690
+ var UNMINT_DEPENDENCIES = {
691
+ "fumadocs-core": "^16.4.7",
692
+ "fumadocs-mdx": "^14.2.5"
693
+ };
694
+ async function mergeDependencies(packageJsonPath, depsToAdd = UNMINT_DEPENDENCIES) {
695
+ const pkg = await fs5.readJson(packageJsonPath);
696
+ const added = [];
697
+ const skipped = [];
698
+ pkg.dependencies = pkg.dependencies || {};
699
+ for (const [name, version] of Object.entries(depsToAdd)) {
700
+ if (pkg.dependencies[name] || pkg.devDependencies?.[name]) {
701
+ skipped.push(name);
702
+ } else {
703
+ pkg.dependencies[name] = version;
704
+ added.push(name);
705
+ }
706
+ }
707
+ pkg.dependencies = Object.fromEntries(
708
+ Object.entries(pkg.dependencies).sort(([a], [b]) => a.localeCompare(b))
709
+ );
710
+ await fs5.writeJson(packageJsonPath, pkg, { spaces: 2 });
711
+ return { added, skipped };
712
+ }
713
+ function getUnmintCssVariables(accentColor, darkAccentColor) {
714
+ return `
715
+ /* Unmint Docs - Scoped accent colors */
716
+ .unmint-docs {
717
+ --accent: ${accentColor};
718
+ --accent-foreground: #ffffff;
719
+ --accent-muted: ${hexToRgba(accentColor, 0.1)};
720
+ }
721
+
722
+ .dark .unmint-docs {
723
+ --accent: ${darkAccentColor};
724
+ --accent-foreground: #0f172a;
725
+ --accent-muted: ${hexToRgba(darkAccentColor, 0.1)};
726
+ }
727
+
728
+ /* Syntax highlighting - Shiki integration for docs */
729
+ .unmint-docs pre code span {
730
+ color: var(--shiki-light);
731
+ }
732
+
733
+ .dark .unmint-docs pre code span {
734
+ color: var(--shiki-dark);
735
+ }
736
+ `;
737
+ }
738
+ async function mergeGlobalsCss(globalsCssPath, accentColor, darkAccentColor) {
739
+ const existing = await fs5.readFile(globalsCssPath, "utf-8");
740
+ if (existing.includes(".unmint-docs")) {
741
+ return false;
742
+ }
743
+ const cssToAdd = getUnmintCssVariables(accentColor, darkAccentColor);
744
+ await fs5.appendFile(globalsCssPath, cssToAdd);
745
+ return true;
746
+ }
747
+ async function wrapNextConfig(nextConfigPath) {
748
+ const existing = await fs5.readFile(nextConfigPath, "utf-8");
749
+ if (existing.includes("fumadocs-mdx") || existing.includes("createMDX")) {
750
+ return false;
751
+ }
752
+ const ext = path5.extname(nextConfigPath);
753
+ let modified;
754
+ if (ext === ".ts") {
755
+ modified = wrapTypescriptConfig(existing);
756
+ } else {
757
+ modified = wrapJavascriptConfig(existing);
758
+ }
759
+ await fs5.writeFile(nextConfigPath, modified);
760
+ return true;
761
+ }
762
+ function wrapTypescriptConfig(existing) {
763
+ const importLine = "import { createMDX } from 'fumadocs-mdx/next'\n";
764
+ const exportMatch = existing.match(/export\s+default\s+(\w+)/);
765
+ if (exportMatch) {
766
+ const configName = exportMatch[1];
767
+ let modified = importLine + existing;
768
+ modified = modified.replace(
769
+ /export\s+default\s+\w+/,
770
+ `const withMDX = createMDX()
771
+
772
+ export default withMDX(${configName})`
773
+ );
774
+ return modified;
775
+ }
776
+ if (existing.includes("export default {")) {
777
+ let modified = importLine + existing.replace(
778
+ "export default {",
779
+ "const nextConfig = {"
780
+ );
781
+ modified = modified.trimEnd() + "\n\nconst withMDX = createMDX()\n\nexport default withMDX(nextConfig)\n";
782
+ return modified;
783
+ }
784
+ return importLine + existing;
785
+ }
786
+ function wrapJavascriptConfig(existing) {
787
+ const isESM = existing.includes("export default") || existing.includes("import ");
788
+ if (isESM) {
789
+ const importLine = "import { createMDX } from 'fumadocs-mdx/next'\n";
790
+ const exportMatch = existing.match(/export\s+default\s+(\w+)/);
791
+ if (exportMatch) {
792
+ const configName = exportMatch[1];
793
+ let modified = importLine + existing;
794
+ modified = modified.replace(
795
+ /export\s+default\s+\w+/,
796
+ `const withMDX = createMDX()
797
+
798
+ export default withMDX(${configName})`
799
+ );
800
+ return modified;
801
+ }
802
+ if (existing.includes("export default {")) {
803
+ let modified = importLine + existing.replace(
804
+ "export default {",
805
+ "const nextConfig = {"
806
+ );
807
+ modified = modified.trimEnd() + "\n\nconst withMDX = createMDX()\n\nexport default withMDX(nextConfig)\n";
808
+ return modified;
809
+ }
810
+ return importLine + existing;
811
+ }
812
+ const requireLine = "const { createMDX } = require('fumadocs-mdx/next')\n";
813
+ if (existing.includes("module.exports")) {
814
+ let modified = requireLine + existing.replace(
815
+ /module\.exports\s*=\s*(\w+)/,
816
+ (_, configName) => `const withMDX = createMDX()
817
+
818
+ module.exports = withMDX(${configName})`
819
+ );
820
+ return modified;
821
+ }
822
+ return requireLine + existing;
823
+ }
824
+ function hexToRgba(hex, alpha) {
825
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
826
+ if (!result) return `rgba(0, 0, 0, ${alpha})`;
827
+ const r = parseInt(result[1], 16);
828
+ const g = parseInt(result[2], 16);
829
+ const b = parseInt(result[3], 16);
830
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
831
+ }
832
+ function lightenColor2(hex, percent = 30) {
833
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
834
+ if (!result) return hex;
835
+ let r = parseInt(result[1], 16);
836
+ let g = parseInt(result[2], 16);
837
+ let b = parseInt(result[3], 16);
838
+ r = Math.min(255, Math.floor(r + (255 - r) * (percent / 100)));
839
+ g = Math.min(255, Math.floor(g + (255 - g) * (percent / 100)));
840
+ b = Math.min(255, Math.floor(b + (255 - b) * (percent / 100)));
841
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
842
+ }
843
+ async function createSourceConfig(targetDir) {
844
+ const sourceConfigPath = path5.join(targetDir, "source.config.ts");
845
+ if (await fs5.pathExists(sourceConfigPath)) {
846
+ return;
847
+ }
848
+ const content = `import { defineConfig, defineDocs } from 'fumadocs-mdx/config'
849
+ import { rehypeCode } from 'fumadocs-core/mdx-plugins'
850
+
851
+ export const docs = defineDocs({
852
+ dir: 'content/docs',
853
+ })
854
+
855
+ export default defineConfig({
856
+ mdxOptions: {
857
+ rehypePlugins: [
858
+ [
859
+ rehypeCode,
860
+ {
861
+ themes: {
862
+ light: 'github-light',
863
+ dark: 'github-dark',
864
+ },
865
+ },
866
+ ],
867
+ ],
868
+ },
869
+ })
870
+ `;
871
+ await fs5.writeFile(sourceConfigPath, content);
872
+ }
873
+ async function createMdxComponents(targetDir, appDir) {
874
+ const mdxComponentsPath = path5.join(targetDir, "mdx-components.tsx");
875
+ if (await fs5.pathExists(mdxComponentsPath)) {
876
+ return;
877
+ }
878
+ const componentsPath = appDir.includes("src") ? "@/app/components/docs/mdx" : "@/app/components/docs/mdx";
879
+ const content = `import type { MDXComponents } from 'mdx/types'
880
+ import defaultComponents from 'fumadocs-ui/mdx'
881
+ import { Accordion } from '${componentsPath}/accordion'
882
+ import { Callout, Note, Tip, Warning, Info } from '${componentsPath}/callout'
883
+ import { Card, CardGroup } from '${componentsPath}/card'
884
+ import { CodeBlock } from '${componentsPath}/code-block'
885
+ import { Frame } from '${componentsPath}/frame'
886
+ import { Steps, Step } from '${componentsPath}/steps'
887
+ import { Tab, Tabs } from '${componentsPath}/tabs'
888
+
889
+ export function useMDXComponents(components: MDXComponents): MDXComponents {
890
+ return {
891
+ ...defaultComponents,
892
+ ...components,
893
+ Accordion,
894
+ Callout,
895
+ Note,
896
+ Tip,
897
+ Warning,
898
+ Info,
899
+ Card,
900
+ CardGroup,
901
+ CodeBlock,
902
+ Frame,
903
+ Steps,
904
+ Step,
905
+ Tab,
906
+ Tabs,
907
+ }
908
+ }
909
+ `;
910
+ await fs5.writeFile(mdxComponentsPath, content);
911
+ }
912
+
913
+ // src/commands/add.ts
914
+ async function add(options = {}) {
915
+ const cwd = process.cwd();
916
+ const spinner = ora3("Detecting project...").start();
917
+ const projectInfo = await detectProject(cwd);
918
+ const validation = validateProjectForAdd(projectInfo);
919
+ if (!validation.valid) {
920
+ spinner.fail(validation.error);
921
+ process.exit(1);
922
+ }
923
+ spinner.succeed(`Detected Next.js project (App Router)`);
924
+ console.log(chalk4.dim(` Using ${projectInfo.useSrcDir ? "src/app" : "app"} directory structure`));
925
+ console.log();
926
+ if (projectInfo.hasExistingDocs && !options.path) {
927
+ console.log(chalk4.yellow("\u26A0 A /docs route already exists in this project."));
928
+ console.log(chalk4.dim(" Use --path to specify a different route (e.g., --path /documentation)"));
929
+ console.log();
930
+ }
931
+ const config = options.yes ? getDefaultAddConfig(options.path) : await promptAddConfig(projectInfo.hasExistingDocs, options.path);
932
+ const docsRoutePath = path6.join(cwd, projectInfo.appDir, config.docsRoute.replace(/^\//, ""));
933
+ if (await fs6.pathExists(docsRoutePath)) {
934
+ console.log(chalk4.red(`
935
+ \u2716 The route ${config.docsRoute} already exists at ${docsRoutePath}`));
936
+ console.log(chalk4.dim(" Choose a different route or manually merge the directories."));
937
+ process.exit(1);
938
+ }
939
+ console.log();
940
+ console.log(chalk4.cyan(" Adding Unmint to your project..."));
941
+ console.log();
942
+ await copyDocsFiles(cwd, projectInfo, config);
943
+ await mergeConfigurations(cwd, projectInfo, config);
944
+ await installDependencies(cwd, projectInfo.packageManager);
945
+ printSuccessMessage(config, projectInfo.packageManager);
946
+ }
947
+ function getDefaultAddConfig(customPath) {
948
+ return {
949
+ docsRoute: customPath || "/docs",
950
+ title: "Documentation",
951
+ description: "Documentation for your project",
952
+ accentColor: "#0891b2"
953
+ };
954
+ }
955
+ async function copyDocsFiles(cwd, projectInfo, config) {
956
+ const spinner = ora3("Copying docs files...").start();
957
+ const templateDir = await resolveTemplateDir();
958
+ const appDir = projectInfo.appDir;
959
+ const routeName = config.docsRoute.replace(/^\//, "");
960
+ const libDir = projectInfo.useSrcDir ? "src/lib" : "lib";
961
+ const copyOperations = [
962
+ // Docs route
963
+ {
964
+ from: path6.join(templateDir, "app/docs"),
965
+ to: path6.join(cwd, appDir, routeName)
966
+ },
967
+ // Docs components
968
+ {
969
+ from: path6.join(templateDir, "app/components/docs"),
970
+ to: path6.join(cwd, appDir, "components/docs")
971
+ },
972
+ // Providers (theme provider)
973
+ {
974
+ from: path6.join(templateDir, "app/providers"),
975
+ to: path6.join(cwd, appDir, "providers")
976
+ },
977
+ // API routes
978
+ {
979
+ from: path6.join(templateDir, "app/api/search"),
980
+ to: path6.join(cwd, appDir, "api/search")
981
+ },
982
+ {
983
+ from: path6.join(templateDir, "app/api/og"),
984
+ to: path6.join(cwd, appDir, "api/og")
985
+ },
986
+ // Content (always at root)
987
+ {
988
+ from: path6.join(templateDir, "content/docs"),
989
+ to: path6.join(cwd, "content/docs")
990
+ },
991
+ // Lib files (in src/lib if using src directory)
992
+ {
993
+ from: path6.join(templateDir, "lib/docs-source.ts"),
994
+ to: path6.join(cwd, libDir, "docs-source.ts")
995
+ },
996
+ {
997
+ from: path6.join(templateDir, "lib/theme-config.ts"),
998
+ to: path6.join(cwd, libDir, "unmint-config.ts")
999
+ },
1000
+ // Logo files (copy to public directory if not present)
1001
+ {
1002
+ from: path6.join(templateDir, "public/logo.svg"),
1003
+ to: path6.join(cwd, "public/logo.svg")
1004
+ },
1005
+ {
1006
+ from: path6.join(templateDir, "public/logo.png"),
1007
+ to: path6.join(cwd, "public/logo.png")
1008
+ }
1009
+ ];
1010
+ await fs6.ensureDir(path6.join(cwd, "public"));
1011
+ await fs6.ensureDir(path6.join(cwd, libDir));
1012
+ for (const op of copyOperations) {
1013
+ if (await fs6.pathExists(op.from)) {
1014
+ await fs6.copy(op.from, op.to, { overwrite: false });
1015
+ }
1016
+ }
1017
+ const utilsPath = path6.join(cwd, libDir, "utils.ts");
1018
+ if (!await fs6.pathExists(utilsPath)) {
1019
+ await fs6.copy(
1020
+ path6.join(templateDir, "lib/utils.ts"),
1021
+ utilsPath
1022
+ );
1023
+ }
1024
+ if (projectInfo.useSrcDir) {
1025
+ const docsSourcePath = path6.join(cwd, libDir, "docs-source.ts");
1026
+ if (await fs6.pathExists(docsSourcePath)) {
1027
+ let content = await fs6.readFile(docsSourcePath, "utf-8");
1028
+ content = content.replace(
1029
+ /from ['"]\.\.\/\.source\/server['"]/,
1030
+ "from '../../.source/server'"
1031
+ );
1032
+ await fs6.writeFile(docsSourcePath, content);
1033
+ }
1034
+ }
1035
+ await updateDocsImports(cwd, appDir, routeName);
1036
+ await updateThemeConfig(cwd, config);
1037
+ spinner.succeed(`Added ${appDir}/${routeName}/ route with layout`);
1038
+ ora3().succeed(`Added docs components to ${appDir}/components/docs/`);
1039
+ ora3().succeed("Added content/docs/ directory with sample content");
1040
+ }
1041
+ async function resolveTemplateDir() {
1042
+ const bundledPath = path6.join(import.meta.dirname, "../../template");
1043
+ if (await fs6.pathExists(bundledPath)) {
1044
+ return bundledPath;
1045
+ }
1046
+ const devPath = path6.join(import.meta.dirname, "../../../template");
1047
+ if (await fs6.pathExists(devPath)) {
1048
+ return devPath;
1049
+ }
1050
+ throw new Error("Could not find template directory");
1051
+ }
1052
+ async function updateDocsImports(cwd, appDir, routeName) {
1053
+ const filesToUpdate = [
1054
+ path6.join(cwd, appDir, routeName, "layout.tsx"),
1055
+ path6.join(cwd, appDir, routeName, "[[...slug]]", "page.tsx"),
1056
+ path6.join(cwd, appDir, "components/docs/docs-sidebar.tsx"),
1057
+ path6.join(cwd, appDir, "components/docs/docs-header.tsx"),
1058
+ path6.join(cwd, appDir, "components/docs/mobile-sidebar.tsx"),
1059
+ path6.join(cwd, appDir, "components/docs/search-dialog.tsx"),
1060
+ path6.join(cwd, appDir, "api/og/route.tsx")
1061
+ ];
1062
+ for (const filePath of filesToUpdate) {
1063
+ if (await fs6.pathExists(filePath)) {
1064
+ let content = await fs6.readFile(filePath, "utf-8");
1065
+ if (content.includes("@/lib/theme-config")) {
1066
+ content = content.replace(
1067
+ /@\/lib\/theme-config/g,
1068
+ "@/lib/unmint-config"
1069
+ );
1070
+ await fs6.writeFile(filePath, content);
1071
+ }
1072
+ }
1073
+ }
1074
+ }
1075
+ async function updateThemeConfig(cwd, config) {
1076
+ const configPath = path6.join(cwd, "lib/unmint-config.ts");
1077
+ if (!await fs6.pathExists(configPath)) return;
1078
+ let content = await fs6.readFile(configPath, "utf-8");
1079
+ content = content.replace(
1080
+ /name:\s*['"][^'"]*['"]/,
1081
+ `name: '${config.title}'`
1082
+ );
1083
+ content = content.replace(
1084
+ /description:\s*['"][^'"]*['"]/,
1085
+ `description: '${config.description}'`
1086
+ );
1087
+ await fs6.writeFile(configPath, content);
1088
+ }
1089
+ async function mergeConfigurations(cwd, projectInfo, config) {
1090
+ const packageJsonPath = path6.join(cwd, "package.json");
1091
+ const { added } = await mergeDependencies(packageJsonPath);
1092
+ if (added.length > 0) {
1093
+ ora3().succeed(`Merged ${added.length} dependencies into package.json`);
1094
+ }
1095
+ if (projectInfo.globalsCssPath) {
1096
+ const darkAccent = lightenColor2(config.accentColor, 30);
1097
+ const merged = await mergeGlobalsCss(projectInfo.globalsCssPath, config.accentColor, darkAccent);
1098
+ if (merged) {
1099
+ ora3().succeed("Added CSS variables to globals.css");
1100
+ }
1101
+ }
1102
+ if (projectInfo.nextConfigPath) {
1103
+ const wrapped = await wrapNextConfig(projectInfo.nextConfigPath);
1104
+ if (wrapped) {
1105
+ ora3().succeed("Updated next.config for MDX support");
1106
+ }
1107
+ }
1108
+ await createSourceConfig(cwd);
1109
+ ora3().succeed("Created source.config.ts");
1110
+ await createMdxComponents(cwd, projectInfo.appDir);
1111
+ ora3().succeed("Created mdx-components.tsx");
1112
+ }
1113
+ async function installDependencies(cwd, packageManager) {
1114
+ const spinner = ora3("Installing dependencies...").start();
1115
+ try {
1116
+ const installCmd = packageManager === "npm" ? "install" : "install";
1117
+ await execa2(packageManager, [installCmd], { cwd, stdio: "pipe" });
1118
+ spinner.succeed(`Installed ${Object.keys(UNMINT_DEPENDENCIES).join(", ")}`);
1119
+ } catch (error) {
1120
+ spinner.warn("Could not install dependencies automatically");
1121
+ console.log(chalk4.dim(` Run "${packageManager} install" manually`));
1122
+ }
1123
+ }
1124
+ function printSuccessMessage(config, packageManager) {
1125
+ console.log();
1126
+ console.log(chalk4.green(" Success! Unmint docs added to your project."));
1127
+ console.log();
1128
+ console.log(" Next steps:");
1129
+ console.log(chalk4.cyan(` ${packageManager} run dev`));
1130
+ console.log();
1131
+ console.log(` Your docs will be at ${chalk4.cyan(`http://localhost:3000${config.docsRoute}`)}`);
1132
+ console.log();
1133
+ }
1134
+
491
1135
  // src/index.ts
492
1136
  var cyanGradient = gradient([
493
1137
  "#065f5f",
@@ -508,17 +1152,31 @@ function printBanner() {
508
1152
  });
509
1153
  console.log();
510
1154
  console.log(cyanGradient.multiline(banner));
511
- console.log(chalk4.dim(" Beautiful documentation, open source"));
1155
+ console.log(chalk5.dim(" Beautiful documentation, open source"));
512
1156
  console.log();
513
1157
  }
514
1158
  var program = new Command();
515
- program.name("create-unmint").description("Create and manage Unmint documentation projects").version("1.0.0");
516
- program.argument("[project-name]", "Name of the project to create").option("-y, --yes", "Skip prompts and use defaults").option("--update", "Update an existing Unmint project").option("--dry-run", "Show what would be updated without making changes").action(async (projectName, options) => {
1159
+ program.name("create-unmint").description("Create and manage Unmint documentation projects").version("1.2.0");
1160
+ program.argument("[project-name]", 'Name of the project to create (use "." for current directory)').option("-y, --yes", "Skip prompts and use defaults").option("--add", "Add Unmint docs to an existing Next.js project").option("--path <route>", "Custom route path for docs (e.g., /documentation)").option("--update", "Update an existing Unmint project").option("--dry-run", "Show what would be updated without making changes").action(async (projectName, options) => {
517
1161
  printBanner();
518
1162
  if (options.update) {
519
1163
  await update(options);
520
- } else {
521
- await init(projectName, options);
1164
+ return;
1165
+ }
1166
+ if (options.add) {
1167
+ await add({ yes: options.yes, path: options.path });
1168
+ return;
1169
+ }
1170
+ if (projectName === ".") {
1171
+ const cwd = process.cwd();
1172
+ const hasNextConfig = await fs7.pathExists(path7.join(cwd, "next.config.ts")) || await fs7.pathExists(path7.join(cwd, "next.config.js")) || await fs7.pathExists(path7.join(cwd, "next.config.mjs"));
1173
+ if (hasNextConfig) {
1174
+ console.log(chalk5.dim(" Detected existing Next.js project, using add mode..."));
1175
+ console.log();
1176
+ await add({ yes: options.yes, path: options.path });
1177
+ return;
1178
+ }
522
1179
  }
1180
+ await init(projectName, options);
523
1181
  });
524
1182
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-unmint",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "Create a new Unmint documentation project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,7 @@ export function DocsHeader({ tree }: DocsHeaderProps) {
33
33
  aria-label="Open menu"
34
34
  aria-expanded={isMobileMenuOpen}
35
35
  >
36
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
36
+ <svg aria-hidden="true" className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
37
37
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
38
38
  </svg>
39
39
  </button>
@@ -68,7 +68,7 @@ export function DocsHeader({ tree }: DocsHeaderProps) {
68
68
  className="p-2 text-muted-foreground hover:text-foreground transition-colors min-w-[44px] min-h-[44px] flex items-center justify-center"
69
69
  aria-label="GitHub"
70
70
  >
71
- <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
71
+ <svg aria-hidden="true" className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
72
72
  <path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
73
73
  </svg>
74
74
  </a>
@@ -25,7 +25,7 @@ export function DocsSidebar({ tree }: DocsSidebarProps) {
25
25
  className="flex items-center gap-3 py-1 text-sm text-[var(--accent)] font-medium hover:opacity-80 transition-opacity"
26
26
  >
27
27
  <span className="flex items-center justify-center w-7 h-7 rounded-md bg-[var(--accent-muted)]">
28
- <svg className="w-4 h-4 text-[var(--accent)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
28
+ <svg aria-hidden="true" className="w-4 h-4 text-[var(--accent)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
29
29
  <path strokeLinecap="round" strokeLinejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
30
30
  </svg>
31
31
  </span>
@@ -41,7 +41,7 @@ export function DocsSidebar({ tree }: DocsSidebarProps) {
41
41
  className="flex items-center gap-3 py-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
42
42
  >
43
43
  <span className="flex items-center justify-center w-7 h-7 rounded-md bg-gray-100 dark:bg-gray-800">
44
- <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
44
+ <svg aria-hidden="true" className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
45
45
  <path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
46
46
  </svg>
47
47
  </span>
@@ -56,7 +56,7 @@ export function DocsSidebar({ tree }: DocsSidebarProps) {
56
56
  className="flex items-center gap-3 py-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
57
57
  >
58
58
  <span className="flex items-center justify-center w-7 h-7 rounded-md bg-gray-100 dark:bg-gray-800">
59
- <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
59
+ <svg aria-hidden="true" className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
60
60
  <path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
61
61
  </svg>
62
62
  </span>
@@ -28,10 +28,12 @@ export function Accordion({ title, children, defaultOpen = false }: AccordionPro
28
28
  <div>
29
29
  <button
30
30
  onClick={() => setIsOpen(!isOpen)}
31
- className="flex w-full items-center justify-between px-4 py-4 text-left font-medium text-foreground hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
31
+ aria-expanded={isOpen}
32
+ className="flex w-full items-center justify-between px-4 py-4 text-left font-medium text-foreground hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-inset"
32
33
  >
33
34
  <span>{title}</span>
34
35
  <svg
36
+ aria-hidden="true"
35
37
  className={cn(
36
38
  'w-5 h-5 text-muted-foreground transition-transform duration-200',
37
39
  isOpen && 'rotate-180'
@@ -1,11 +1,12 @@
1
1
  'use client'
2
2
 
3
- import { useState, createContext, useContext } from 'react'
3
+ import { useState, createContext, useContext, Children, isValidElement, useId } from 'react'
4
4
  import { cn } from '@/lib/utils'
5
5
 
6
6
  interface TabsContextValue {
7
7
  activeTab: string
8
8
  setActiveTab: (tab: string) => void
9
+ tabsId: string
9
10
  }
10
11
 
11
12
  const TabsContext = createContext<TabsContextValue | null>(null)
@@ -17,10 +18,87 @@ interface TabsProps {
17
18
 
18
19
  export function Tabs({ children, defaultValue }: TabsProps) {
19
20
  const [activeTab, setActiveTab] = useState(defaultValue || '')
21
+ const tabsId = useId()
22
+
23
+ // Extract tab titles and content from children
24
+ const tabs: { title: string; content: React.ReactNode }[] = []
25
+ Children.forEach(children, (child) => {
26
+ if (isValidElement<TabProps>(child) && child.props.title) {
27
+ tabs.push({
28
+ title: child.props.title,
29
+ content: child.props.children,
30
+ })
31
+ }
32
+ })
33
+
34
+ // Set default active tab if not set
35
+ const currentActiveTab = activeTab || (tabs[0]?.title ?? '')
20
36
 
21
37
  return (
22
- <TabsContext.Provider value={{ activeTab, setActiveTab }}>
23
- <div className="my-6">{children}</div>
38
+ <TabsContext.Provider value={{ activeTab: currentActiveTab, setActiveTab, tabsId }}>
39
+ <div className="my-6">
40
+ {/* Tab list */}
41
+ <div role="tablist" aria-label="Tabs" className="flex border-b border-border">
42
+ {tabs.map((tab, index) => {
43
+ const isActive = currentActiveTab === tab.title
44
+ const tabId = `${tabsId}-tab-${index}`
45
+ const panelId = `${tabsId}-panel-${index}`
46
+
47
+ return (
48
+ <button
49
+ key={tab.title}
50
+ role="tab"
51
+ id={tabId}
52
+ aria-selected={isActive}
53
+ aria-controls={panelId}
54
+ tabIndex={isActive ? 0 : -1}
55
+ onClick={() => setActiveTab(tab.title)}
56
+ onKeyDown={(e) => {
57
+ if (e.key === 'ArrowRight') {
58
+ e.preventDefault()
59
+ const nextIndex = (index + 1) % tabs.length
60
+ setActiveTab(tabs[nextIndex].title)
61
+ } else if (e.key === 'ArrowLeft') {
62
+ e.preventDefault()
63
+ const prevIndex = (index - 1 + tabs.length) % tabs.length
64
+ setActiveTab(tabs[prevIndex].title)
65
+ }
66
+ }}
67
+ className={cn(
68
+ 'px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors',
69
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2',
70
+ isActive
71
+ ? 'border-[var(--accent)] text-[var(--accent)]'
72
+ : 'border-transparent text-muted-foreground hover:text-foreground'
73
+ )}
74
+ >
75
+ {tab.title}
76
+ </button>
77
+ )
78
+ })}
79
+ </div>
80
+
81
+ {/* Tab panels */}
82
+ {tabs.map((tab, index) => {
83
+ const isActive = currentActiveTab === tab.title
84
+ const tabId = `${tabsId}-tab-${index}`
85
+ const panelId = `${tabsId}-panel-${index}`
86
+
87
+ return (
88
+ <div
89
+ key={tab.title}
90
+ role="tabpanel"
91
+ id={panelId}
92
+ aria-labelledby={tabId}
93
+ hidden={!isActive}
94
+ tabIndex={0}
95
+ className={cn('pt-4 [&>pre]:mt-0', !isActive && 'hidden')}
96
+ >
97
+ {tab.content}
98
+ </div>
99
+ )
100
+ })}
101
+ </div>
24
102
  </TabsContext.Provider>
25
103
  )
26
104
  }
@@ -30,31 +108,9 @@ interface TabProps {
30
108
  children: React.ReactNode
31
109
  }
32
110
 
111
+ // Tab is now just a data container, rendering is handled by Tabs
33
112
  export function Tab({ title, children }: TabProps) {
34
- const context = useContext(TabsContext)
35
- if (!context) return null
36
-
37
- const { activeTab, setActiveTab } = context
38
- const isActive = activeTab === title || (!activeTab && title)
39
-
40
- return (
41
- <>
42
- <button
43
- onClick={() => setActiveTab(title)}
44
- className={cn(
45
- 'px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors',
46
- isActive
47
- ? 'border-[var(--accent)] text-[var(--accent)]'
48
- : 'border-transparent text-muted-foreground hover:text-foreground'
49
- )}
50
- >
51
- {title}
52
- </button>
53
- {isActive && (
54
- <div className="pt-4 [&>pre]:mt-0">
55
- {children}
56
- </div>
57
- )}
58
- </>
59
- )
113
+ // This component is used for data extraction only
114
+ // Actual rendering happens in Tabs component
115
+ return null
60
116
  }
@@ -135,7 +135,7 @@ export function MobileSidebar({ tree, isOpen, onClose }: MobileSidebarProps) {
135
135
  className="p-2 -mr-2 text-muted-foreground hover:text-foreground hover:bg-muted rounded-md transition-colors min-w-[44px] min-h-[44px] flex items-center justify-center"
136
136
  aria-label="Close menu"
137
137
  >
138
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
138
+ <svg aria-hidden="true" className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
139
139
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
140
140
  </svg>
141
141
  </button>
@@ -153,7 +153,7 @@ export function MobileSidebar({ tree, isOpen, onClose }: MobileSidebarProps) {
153
153
  className="flex items-center gap-3 py-2 px-2 text-sm text-[var(--accent)] font-medium hover:bg-[var(--accent-muted)] rounded-md transition-colors min-h-[44px]"
154
154
  >
155
155
  <span className="flex items-center justify-center w-7 h-7 rounded-md bg-[var(--accent-muted)]">
156
- <svg className="w-4 h-4 text-[var(--accent)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
156
+ <svg aria-hidden="true" className="w-4 h-4 text-[var(--accent)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
157
157
  <path strokeLinecap="round" strokeLinejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
158
158
  </svg>
159
159
  </span>
@@ -169,7 +169,7 @@ export function MobileSidebar({ tree, isOpen, onClose }: MobileSidebarProps) {
169
169
  className="flex items-center gap-3 py-2 px-2 text-sm text-muted-foreground hover:text-foreground hover:bg-muted rounded-md transition-colors min-h-[44px]"
170
170
  >
171
171
  <span className="flex items-center justify-center w-7 h-7 rounded-md bg-muted">
172
- <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
172
+ <svg aria-hidden="true" className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
173
173
  <path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
174
174
  </svg>
175
175
  </span>
@@ -184,7 +184,7 @@ export function MobileSidebar({ tree, isOpen, onClose }: MobileSidebarProps) {
184
184
  className="flex items-center gap-3 py-2 px-2 text-sm text-muted-foreground hover:text-foreground hover:bg-muted rounded-md transition-colors min-h-[44px]"
185
185
  >
186
186
  <span className="flex items-center justify-center w-7 h-7 rounded-md bg-muted">
187
- <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
187
+ <svg aria-hidden="true" className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
188
188
  <path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
189
189
  </svg>
190
190
  </span>
@@ -243,10 +243,12 @@ function MobileSidebarNode({ node, pathname, onNavigate }: MobileSidebarNodeProp
243
243
  <div>
244
244
  <button
245
245
  onClick={() => setIsExpanded(!isExpanded)}
246
- className="flex items-center justify-between w-full py-2 px-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded-md transition-colors min-h-[44px]"
246
+ aria-expanded={isExpanded}
247
+ className="flex items-center justify-between w-full py-2 px-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded-md transition-colors min-h-[44px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-inset"
247
248
  >
248
249
  <span>{node.name}</span>
249
250
  <svg
251
+ aria-hidden="true"
250
252
  className={cn('w-4 h-4 transition-transform', isExpanded && 'rotate-90')}
251
253
  fill="none"
252
254
  stroke="currentColor"
@@ -31,6 +31,7 @@ export function SearchTrigger() {
31
31
  <button
32
32
  type="button"
33
33
  onClick={() => setOpen(true)}
34
+ aria-haspopup="dialog"
34
35
  className={cn(
35
36
  'flex items-center gap-3 px-4 py-2.5 rounded-lg w-full max-w-md',
36
37
  'bg-muted/50 border border-border/50',
@@ -198,7 +199,7 @@ function SearchDialog({ onClose }: SearchDialogProps) {
198
199
  <p>Searching...</p>
199
200
  </div>
200
201
  ) : results.length > 0 ? (
201
- <ul ref={resultsRef} className="py-2">
202
+ <ul ref={resultsRef} role="listbox" aria-label="Search results" className="py-2">
202
203
  {results.map((result, index) => {
203
204
  // Build breadcrumb path
204
205
  const breadcrumbPath = result.breadcrumbs && result.breadcrumbs.length > 0
@@ -211,8 +212,11 @@ function SearchDialog({ onClose }: SearchDialogProps) {
211
212
  type="button"
212
213
  onClick={() => handleSelect(result.url)}
213
214
  onMouseEnter={() => setSelectedIndex(index)}
215
+ role="option"
216
+ aria-selected={selectedIndex === index}
214
217
  className={cn(
215
- 'w-full px-4 py-3 text-left transition-colors focus:outline-none',
218
+ 'w-full px-4 py-3 text-left transition-colors',
219
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--accent)]',
216
220
  selectedIndex === index
217
221
  ? 'bg-gray-100 dark:bg-gray-800'
218
222
  : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
@@ -265,6 +269,7 @@ function SearchDialog({ onClose }: SearchDialogProps) {
265
269
  function SearchIcon({ className }: { className?: string }) {
266
270
  return (
267
271
  <svg
272
+ aria-hidden="true"
268
273
  className={className}
269
274
  fill="none"
270
275
  viewBox="0 0 24 24"
@@ -12,21 +12,21 @@ export function ThemeToggle() {
12
12
  }, [])
13
13
 
14
14
  if (!mounted) {
15
- return <div className="w-9 h-9" />
15
+ return <div className="w-11 h-11" />
16
16
  }
17
17
 
18
18
  return (
19
19
  <button
20
20
  onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
21
- className="flex items-center justify-center w-9 h-9 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
21
+ className="flex items-center justify-center w-11 h-11 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
22
22
  aria-label="Toggle theme"
23
23
  >
24
24
  {theme === 'dark' ? (
25
- <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
25
+ <svg aria-hidden="true" className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
26
26
  <path strokeLinecap="round" strokeLinejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
27
27
  </svg>
28
28
  ) : (
29
- <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
29
+ <svg aria-hidden="true" className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
30
30
  <path strokeLinecap="round" strokeLinejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
31
31
  </svg>
32
32
  )}
@@ -13,7 +13,7 @@ export const siteConfig = {
13
13
 
14
14
  // Logo configuration
15
15
  logo: {
16
- src: '/logo.png',
16
+ src: '/logo.svg',
17
17
  alt: 'Unmint',
18
18
  width: 40,
19
19
  height: 40,