Chris Tate 5 месяцев назад
Родитель
Сommit
4f36dfb625

+ 4 - 24
apps/web/app/docs/installation/page.tsx

@@ -1,6 +1,4 @@
-import Link from "next/link";
-import { Button } from "@/components/ui/button";
-import { Code } from "@/components/code";
+import { PackageInstall } from "@/components/package-install";
 
 export const metadata = {
   title: "Installation | json-render",
@@ -15,19 +13,7 @@ export default function InstallationPage() {
       </p>
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Install packages</h2>
-      <Code lang="bash">npm install @json-render/core @json-render/react</Code>
-
-      <p className="text-sm text-muted-foreground mb-4">
-        Or with other package managers:
-      </p>
-      <Code lang="bash">{`# pnpm
-pnpm add @json-render/core @json-render/react
-
-# yarn
-yarn add @json-render/core @json-render/react
-
-# bun
-bun add @json-render/core @json-render/react`}</Code>
+      <PackageInstall packages="@json-render/core @json-render/react" />
 
       <h2 className="text-xl font-semibold mt-12 mb-4">Peer Dependencies</h2>
       <p className="text-sm text-muted-foreground mb-4">
@@ -41,20 +27,14 @@ bun add @json-render/core @json-render/react`}</Code>
           <code className="text-foreground">zod</code> ^4.0.0
         </li>
       </ul>
-      <Code lang="bash">npm install react zod</Code>
+      <PackageInstall packages="react zod" />
 
       <h2 className="text-xl font-semibold mt-12 mb-4">For AI Integration</h2>
       <p className="text-sm text-muted-foreground mb-4">
         To use json-render with AI models, you&apos;ll also need the Vercel AI
         SDK:
       </p>
-      <Code lang="bash">npm install ai</Code>
-
-      <div className="flex gap-3 mt-12">
-        <Button size="sm" asChild>
-          <Link href="/docs/quick-start">Quick Start</Link>
-        </Button>
-      </div>
+      <PackageInstall packages="ai" />
     </article>
   );
 }

+ 0 - 7
apps/web/app/docs/quick-start/page.tsx

@@ -1,5 +1,4 @@
 import Link from "next/link";
-import { Button } from "@/components/ui/button";
 import { Code } from "@/components/code";
 
 export const metadata = {
@@ -201,12 +200,6 @@ export default function Page() {
           </Link>
         </li>
       </ul>
-
-      <div className="flex gap-3 mt-12">
-        <Button size="sm" asChild>
-          <Link href="/docs/catalog">Learn about Catalogs</Link>
-        </Button>
-      </div>
     </article>
   );
 }

+ 56 - 0
apps/web/components/code-tabs.tsx

@@ -0,0 +1,56 @@
+"use client";
+
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { CopyButton } from "./copy-button";
+
+interface CodeTabsProps {
+  tabs: {
+    label: string;
+    value: string;
+    code: string;
+    html: string;
+  }[];
+  defaultValue?: string;
+}
+
+export function CodeTabs({ tabs, defaultValue }: CodeTabsProps) {
+  const defaultTab = defaultValue ?? tabs[0]?.value;
+
+  return (
+    <div className="my-6 rounded-lg border border-border bg-neutral-100 dark:bg-[#0a0a0a] text-sm font-mono overflow-hidden">
+      <Tabs defaultValue={defaultTab} className="gap-0">
+        <div className="flex items-center justify-between border-b border-border px-4 py-2">
+          <TabsList className="h-7 bg-transparent p-0 gap-2">
+            {tabs.map((tab) => (
+              <TabsTrigger
+                key={tab.value}
+                value={tab.value}
+                className="h-6 px-2 text-xs data-[state=active]:bg-secondary data-[state=active]:shadow-none rounded"
+              >
+                {tab.label}
+              </TabsTrigger>
+            ))}
+          </TabsList>
+        </div>
+        {tabs.map((tab) => (
+          <TabsContent
+            key={tab.value}
+            value={tab.value}
+            className="relative group mt-0"
+          >
+            <div className="absolute top-3 right-3 z-10">
+              <CopyButton
+                text={tab.code}
+                className="opacity-0 group-hover:opacity-100 text-neutral-500 dark:text-neutral-400 bg-neutral-100 dark:bg-[#0a0a0a]"
+              />
+            </div>
+            <div
+              className="overflow-x-auto [&_pre]:bg-transparent! [&_pre]:m-0! [&_pre]:p-4! [&_code]:bg-transparent! [&_.shiki]:bg-transparent!"
+              dangerouslySetInnerHTML={{ __html: tab.html }}
+            />
+          </TabsContent>
+        ))}
+      </Tabs>
+    </div>
+  );
+}

+ 1 - 1
apps/web/components/docs-mobile-nav.tsx

@@ -61,7 +61,7 @@ export function DocsMobileNav() {
 
   return (
     <Sheet open={open} onOpenChange={setOpen}>
-      <SheetTrigger className="lg:hidden sticky top-[calc(3.5rem+1px)] z-40 w-full px-6 py-3 bg-background/80 backdrop-blur-sm border-b border-border flex items-center justify-between">
+      <SheetTrigger className="lg:hidden sticky top-[calc(3.5rem+1px)] z-40 w-full px-6 py-3 bg-background/80 backdrop-blur-sm border-b border-border flex items-center justify-between focus:outline-none">
         <div className="text-sm font-medium">{currentPage?.title}</div>
         <div className="w-8 h-8 flex items-center justify-center">
           <List className="h-4 w-4 text-muted-foreground" />

+ 171 - 0
apps/web/components/package-install.tsx

@@ -0,0 +1,171 @@
+import { codeToHtml } from "shiki";
+import { CodeTabs } from "./code-tabs";
+
+const vercelDarkTheme = {
+  name: "vercel-dark",
+  type: "dark" as const,
+  colors: {
+    "editor.background": "transparent",
+    "editor.foreground": "#EDEDED",
+  },
+  settings: [
+    {
+      scope: ["comment", "punctuation.definition.comment"],
+      settings: { foreground: "#666666" },
+    },
+    {
+      scope: ["string", "string.quoted", "string.template"],
+      settings: { foreground: "#50E3C2" },
+    },
+    {
+      scope: [
+        "constant.numeric",
+        "constant.language.boolean",
+        "constant.language.null",
+      ],
+      settings: { foreground: "#50E3C2" },
+    },
+    {
+      scope: ["keyword", "storage.type", "storage.modifier"],
+      settings: { foreground: "#FF0080" },
+    },
+    {
+      scope: ["keyword.operator", "keyword.control"],
+      settings: { foreground: "#FF0080" },
+    },
+    {
+      scope: ["entity.name.function", "support.function", "meta.function-call"],
+      settings: { foreground: "#7928CA" },
+    },
+    {
+      scope: ["variable", "variable.other", "variable.parameter"],
+      settings: { foreground: "#EDEDED" },
+    },
+    {
+      scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
+      settings: { foreground: "#FF0080" },
+    },
+    {
+      scope: ["punctuation", "meta.brace", "meta.bracket"],
+      settings: { foreground: "#888888" },
+    },
+    {
+      scope: [
+        "support.type.property-name",
+        "entity.name.tag.json",
+        "meta.object-literal.key",
+      ],
+      settings: { foreground: "#EDEDED" },
+    },
+    {
+      scope: ["entity.other.attribute-name"],
+      settings: { foreground: "#50E3C2" },
+    },
+    {
+      scope: ["support.type.primitive", "entity.name.type.primitive"],
+      settings: { foreground: "#50E3C2" },
+    },
+  ],
+};
+
+const vercelLightTheme = {
+  name: "vercel-light",
+  type: "light" as const,
+  colors: {
+    "editor.background": "transparent",
+    "editor.foreground": "#171717",
+  },
+  settings: [
+    {
+      scope: ["comment", "punctuation.definition.comment"],
+      settings: { foreground: "#6b7280" },
+    },
+    {
+      scope: ["string", "string.quoted", "string.template"],
+      settings: { foreground: "#067a6e" },
+    },
+    {
+      scope: [
+        "constant.numeric",
+        "constant.language.boolean",
+        "constant.language.null",
+      ],
+      settings: { foreground: "#067a6e" },
+    },
+    {
+      scope: ["keyword", "storage.type", "storage.modifier"],
+      settings: { foreground: "#d6409f" },
+    },
+    {
+      scope: ["keyword.operator", "keyword.control"],
+      settings: { foreground: "#d6409f" },
+    },
+    {
+      scope: ["entity.name.function", "support.function", "meta.function-call"],
+      settings: { foreground: "#6e56cf" },
+    },
+    {
+      scope: ["variable", "variable.other", "variable.parameter"],
+      settings: { foreground: "#171717" },
+    },
+    {
+      scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
+      settings: { foreground: "#d6409f" },
+    },
+    {
+      scope: ["punctuation", "meta.brace", "meta.bracket"],
+      settings: { foreground: "#6b7280" },
+    },
+    {
+      scope: [
+        "support.type.property-name",
+        "entity.name.tag.json",
+        "meta.object-literal.key",
+      ],
+      settings: { foreground: "#171717" },
+    },
+    {
+      scope: ["entity.other.attribute-name"],
+      settings: { foreground: "#067a6e" },
+    },
+    {
+      scope: ["support.type.primitive", "entity.name.type.primitive"],
+      settings: { foreground: "#067a6e" },
+    },
+  ],
+};
+
+interface PackageInstallProps {
+  packages: string;
+}
+
+const packageManagers = [
+  { label: "npm", value: "npm", command: "npm install" },
+  { label: "pnpm", value: "pnpm", command: "pnpm add" },
+  { label: "yarn", value: "yarn", command: "yarn add" },
+  { label: "bun", value: "bun", command: "bun add" },
+];
+
+export async function PackageInstall({ packages }: PackageInstallProps) {
+  const tabs = await Promise.all(
+    packageManagers.map(async (pm) => {
+      const code = `${pm.command} ${packages}`;
+      const html = await codeToHtml(code, {
+        lang: "bash",
+        themes: {
+          light: vercelLightTheme,
+          dark: vercelDarkTheme,
+        },
+        defaultColor: false,
+      });
+      return {
+        label: pm.label,
+        value: pm.value,
+        code,
+        html,
+      };
+    }),
+  );
+
+  return <CodeTabs tabs={tabs} />;
+}

+ 1 - 1
apps/web/components/ui/sheet.tsx

@@ -36,7 +36,7 @@ const SheetContent = React.forwardRef<
     <SheetPrimitive.Content
       ref={ref}
       className={cn(
-        "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
+        "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out focus:outline-none",
         "inset-y-0 left-0 h-full w-3/4 max-w-xs border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
         className,
       )}