Skip to content

Commit

Permalink
feat: add button open in stackblitz
Browse files Browse the repository at this point in the history
  • Loading branch information
winchesHe committed Dec 30, 2024
1 parent 4f0ef58 commit 146f2c8
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 30 deletions.
47 changes: 19 additions & 28 deletions apps/docs/components/copy-button.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,34 @@
import {FC} from "react";
import {Button, ButtonProps} from "@nextui-org/react";
import {ButtonProps} from "@nextui-org/react";
import {useClipboard} from "@nextui-org/use-clipboard";
import {clsx} from "@nextui-org/shared-utils";

import {PreviewButton} from "./preview-button";

import {CheckLinearIcon, CopyLinearIcon} from "@/components/icons";

export interface CopyButtonProps extends ButtonProps {
value?: string;
}

export const CopyButton: FC<CopyButtonProps> = ({value, className, ...buttonProps}) => {
export const CopyButton = ({value, className, ...buttonProps}) => {
const {copy, copied} = useClipboard();

const icon = copied ? (
<CheckLinearIcon
className="opacity-0 scale-50 data-[visible=true]:opacity-100 data-[visible=true]:scale-100 transition-transform-opacity"
data-visible={copied}
size={16}
/>
) : (
<CopyLinearIcon
className="opacity-0 scale-50 data-[visible=true]:opacity-100 data-[visible=true]:scale-100 transition-transform-opacity"
data-visible={!copied}
size={16}
/>
);

const handleCopy = () => {
copy(value);
};

return (
<Button
isIconOnly
className={clsx(
"absolute z-50 right-3 text-zinc-300 top-8 border-1 border-transparent bg-transparent before:bg-white/10 before:content-[''] before:block before:z-[-1] before:absolute before:inset-0 before:backdrop-blur-md before:backdrop-saturate-100 before:rounded-lg",
className,
)}
size="sm"
variant="bordered"
onPress={handleCopy}
{...buttonProps}
>
<CheckLinearIcon
className="absolute opacity-0 scale-50 data-[visible=true]:opacity-100 data-[visible=true]:scale-100 transition-transform-opacity"
data-visible={copied}
size={16}
/>
<CopyLinearIcon
className="absolute opacity-0 scale-50 data-[visible=true]:opacity-100 data-[visible=true]:scale-100 transition-transform-opacity"
data-visible={!copied}
size={16}
/>
</Button>
);
return <PreviewButton className={className} icon={icon} onPress={handleCopy} {...buttonProps} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import * as intlDateUtils from "@internationalized/date";
import * as reactAriaI18n from "@react-aria/i18n";
import * as reactHookFormBase from "react-hook-form";
import {SandpackFiles} from "@codesandbox/sandpack-react/types";
import {Tooltip} from "@nextui-org/react";

import {BgGridContainer} from "@/components/bg-grid-container";
import {GradientBox, GradientBoxProps} from "@/components/gradient-box";
import {CopyButton} from "@/components/copy-button";
import {StackblitzButton} from "@/components/stackblitz-button";

export interface ReactLiveDemoProps {
code: string;
Expand All @@ -21,6 +23,7 @@ export interface ReactLiveDemoProps {
className?: string;
gradientColor?: GradientBoxProps["color"];
overflow?: "auto" | "visible" | "hidden";
typescriptStrict?: boolean;
}

// 🚨 Do not pass react-hook-form to scope, it will break the live preview since
Expand Down Expand Up @@ -49,11 +52,19 @@ export const ReactLiveDemo: React.FC<ReactLiveDemoProps> = ({
height,
className,
noInline,
typescriptStrict = false,
}) => {
const content = (
<>
{files?.[DEFAULT_FILE] && (
<div className="absolute top-[-28px] right-[-8px] z-50">
<div className="absolute top-[-26px] right-[3px] z-50 flex items-center">
<Tooltip closeDelay={0} content="Open in Stackblitz">
<StackblitzButton
className="before:hidden opacity-0 group-hover/code-demo:opacity-100 transition-opacity text-zinc-400"
files={files}
typescriptStrict={typescriptStrict}
/>
</Tooltip>
<CopyButton
className="before:hidden opacity-0 group-hover/code-demo:opacity-100 transition-opacity text-zinc-400"
value={files?.[DEFAULT_FILE] as string}
Expand Down
18 changes: 18 additions & 0 deletions apps/docs/components/icons/social.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,23 @@ const CodeSandboxIcon: React.FC<IconSvgProps> = ({width = "1em", height = "1em",
);
};

const StackblitzIcon: React.FC<IconSvgProps> = ({...props}) => {
return (
<svg
height={16}
viewBox="0 0 1024 1024"
width={16}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M848 359.3H627.7L825.8 109c4.1-5.3.4-13-6.3-13H436c-2.8 0-5.5 1.5-6.9 4L170 547.5c-3.1 5.3.7 12 6.9 12h174.4l-89.4 357.6c-1.9 7.8 7.5 13.3 13.3 7.7L853.5 373c5.2-4.9 1.7-13.7-5.5-13.7M378.2 732.5l60.3-241H281.1l189.6-327.4h224.6L487 427.4h211z"
fill="currentColor"
/>
</svg>
);
};

const JavascriptIcon: React.FC<IconSvgProps> = ({width = "1em", height = "1em", ...props}) => {
return (
<svg
Expand Down Expand Up @@ -470,6 +487,7 @@ export {
NewNextJSIcon,
StorybookIcon,
CodeSandboxIcon,
StackblitzIcon,
JavascriptIcon,
TypescriptIcon,
BunIcon,
Expand Down
29 changes: 29 additions & 0 deletions apps/docs/components/preview-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {forwardRef} from "react";
import {Button, ButtonProps} from "@nextui-org/react";
import {clsx} from "@nextui-org/shared-utils";

export interface PreviewButtonProps extends ButtonProps {
icon: React.ReactNode;
}

export const PreviewButton = forwardRef<HTMLButtonElement, PreviewButtonProps>((props, ref) => {
const {icon, className, ...buttonProps} = props;

return (
<Button
ref={ref}
isIconOnly
className={clsx(
"relative z-50 text-zinc-300 top-8 border-1 border-transparent bg-transparent before:bg-white/10 before:content-[''] before:block before:z-[-1] before:absolute before:inset-0 before:backdrop-blur-md before:backdrop-saturate-100 before:rounded-lg",
className,
)}
size="sm"
variant="light"
{...buttonProps}
>
{icon}
</Button>
);
});

PreviewButton.displayName = "PreviewButton";
45 changes: 45 additions & 0 deletions apps/docs/components/stackblitz-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, {forwardRef} from "react";
import stackblitzSdk from "@stackblitz/sdk";
import {SandpackFiles} from "@codesandbox/sandpack-react/types";

import {PreviewButton} from "./preview-button";

import {StackblitzIcon} from "@/components/icons";
import {useStackblitz} from "@/hooks/use-stackblitz";

export interface StackblitzButtonProps {
files: SandpackFiles;
typescriptStrict?: boolean;
className?: string;
}

export const StackblitzButton = forwardRef<HTMLButtonElement, StackblitzButtonProps>(
(props, ref) => {
const {files, typescriptStrict = false, className, ...rest} = props;
const {stackblitzPrefillConfig, entryFile} = useStackblitz({
files,
typescriptStrict,
});

return (
<PreviewButton
ref={ref}
className={className}
icon={
<StackblitzIcon
data-visible
className="opacity-0 data-[visible=true]:opacity-100 transition-transform-opacity"
/>
}
onPress={() => {
stackblitzSdk.openProject(stackblitzPrefillConfig, {
openFile: [entryFile],
});
}}
{...rest}
/>
);
},
);

StackblitzButton.displayName = "StackblitzButton";
80 changes: 80 additions & 0 deletions apps/docs/hooks/use-stackblitz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {Project} from "@stackblitz/sdk";
import {SandpackFiles} from "@codesandbox/sandpack-react/types";

import {mapKeys} from "@/../../packages/utilities/shared-utils/src";
import {useSandpack} from "@/components/sandpack/use-sandpack";

export interface UseSandpackProps {
files: SandpackFiles;
typescriptStrict?: boolean;
}

const viteConfig = `import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});
`;

export function useStackblitz(props: UseSandpackProps) {
const {files, typescriptStrict = false} = props;

const {
customSetup,
files: filesData,
entryFile,
} = useSandpack({
files,
typescriptStrict,
});
const transformFiles = mapKeys(filesData, (_, key) => key.replace(/^\//, ""));

const dependencies = {...customSetup.dependencies, ...customSetup.devDependencies};

const packageJson = `{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "18.3.1",
"react-dom": "18.3.1",
${Object.entries(dependencies)
.map(([key, value]) => `"${key}": "${value}"`)
.join(",\n ")}
},
"devDependencies": {
"@vitejs/plugin-react": "4.3.4",
"vite": "6.0.6",
"autoprefixer": "10.4.20",
"postcss": "8.4.49",
"tailwindcss": "3.4.17"
},
"main": "/index.jsx"
}`;

const stackblitzPrefillConfig: Project = {
files: {
...Object.fromEntries(
Object.entries(transformFiles).map(([key, value]) => [key, value.code ?? value]),
),
"vite.config.js": viteConfig,
"package.json": packageJson,
},
dependencies,
title: "NextUI",
template: "node",
};

const findEntryFile = Object.keys(stackblitzPrefillConfig.files).find((key) =>
key.includes("App"),
);

return {
entryFile: findEntryFile ?? entryFile,
stackblitzPrefillConfig,
};
}
3 changes: 2 additions & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"@nextui-org/use-clipboard": "workspace:*",
"@nextui-org/use-infinite-scroll": "workspace:*",
"@nextui-org/use-is-mobile": "workspace:*",
"clsx": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.0.5",
"@react-aria/focus": "3.19.0",
"@react-aria/i18n": "3.12.4",
Expand All @@ -53,8 +52,10 @@
"@react-stately/layout": "4.1.0",
"@react-stately/tree": "3.8.6",
"@rehooks/local-storage": "^2.4.5",
"@stackblitz/sdk": "^1.11.0",
"@tanstack/react-virtual": "3.10.9",
"canvas-confetti": "^1.9.2",
"clsx": "^1.2.1",
"cmdk": "^0.2.0",
"color2k": "2.0.3",
"contentlayer2": "0.5.3",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 146f2c8

Please sign in to comment.