mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat: add button open in stackblitz (#4461)
* feat: add button open in stackblitz * feat: add button open in stackblitz * fix: multiple react * feat: add stackblitz button code preview * feat: add stackblitz button code preview
This commit is contained in:
parent
b2e924fe19
commit
da7003e649
@ -1,7 +1,8 @@
|
||||
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 {memo} from "react";
|
||||
|
||||
import {PreviewButton} from "./preview-button";
|
||||
|
||||
import {CheckLinearIcon, CopyLinearIcon} from "@/components/icons";
|
||||
|
||||
@ -9,35 +10,28 @@ export interface CopyButtonProps extends ButtonProps {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export const CopyButton: FC<CopyButtonProps> = ({value, className, ...buttonProps}) => {
|
||||
export const CopyButton = memo<CopyButtonProps>(({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} />;
|
||||
});
|
||||
|
||||
CopyButton.displayName = "CopyButton";
|
||||
|
||||
@ -10,6 +10,8 @@ import {SandpackFiles} from "@codesandbox/sandpack-react/types";
|
||||
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";
|
||||
import {PreviewButton} from "@/components/preview-button";
|
||||
|
||||
export interface ReactLiveDemoProps {
|
||||
code: string;
|
||||
@ -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
|
||||
@ -49,11 +52,18 @@ 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">
|
||||
<StackblitzButton
|
||||
button={<PreviewButton icon={undefined} />}
|
||||
className="before:hidden opacity-0 group-hover/code-demo:opacity-100 transition-opacity text-zinc-400"
|
||||
files={files}
|
||||
typescriptStrict={typescriptStrict}
|
||||
/>
|
||||
<CopyButton
|
||||
className="before:hidden opacity-0 group-hover/code-demo:opacity-100 transition-opacity text-zinc-400"
|
||||
value={files?.[DEFAULT_FILE] as string}
|
||||
|
||||
@ -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
|
||||
@ -470,6 +487,7 @@ export {
|
||||
NewNextJSIcon,
|
||||
StorybookIcon,
|
||||
CodeSandboxIcon,
|
||||
StackblitzIcon,
|
||||
JavascriptIcon,
|
||||
TypescriptIcon,
|
||||
BunIcon,
|
||||
|
||||
31
apps/docs/components/preview-button.tsx
Normal file
31
apps/docs/components/preview-button.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
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 | null, 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";
|
||||
@ -3,6 +3,8 @@
|
||||
import {FC, useRef} from "react";
|
||||
import {SandpackProvider, SandpackLayout, SandpackPreview} from "@codesandbox/sandpack-react";
|
||||
|
||||
import {StackblitzButton} from "../stackblitz-button";
|
||||
|
||||
import {SandpackCodeViewer} from "./code-viewer";
|
||||
import {nextuiTheme} from "./theme";
|
||||
import {UseSandpackProps, useSandpack} from "./use-sandpack";
|
||||
@ -72,6 +74,18 @@ export const Sandpack: FC<SandpackProps> = ({
|
||||
{showReportBug && <BugReportButton />}
|
||||
{showCopyCode && <CopyButton />}
|
||||
{!showPreview && showOpenInCodeSandbox && <CodeSandboxButton />}
|
||||
{!showPreview && showOpenInCodeSandbox && (
|
||||
<StackblitzButton
|
||||
isIconOnly
|
||||
as="span"
|
||||
className="dark:text-zinc-500 text-white"
|
||||
files={files}
|
||||
size="sm"
|
||||
title="Open in Stackblitz"
|
||||
typescriptStrict={typescriptStrict}
|
||||
variant="light"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{hasTypescript && sandpackTemplate && (
|
||||
<LanguageSelector template={sandpackTemplate} onChange={setCurrentTemplate} />
|
||||
|
||||
58
apps/docs/components/stackblitz-button.tsx
Normal file
58
apps/docs/components/stackblitz-button.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React, {forwardRef} from "react";
|
||||
import stackblitzSdk from "@stackblitz/sdk";
|
||||
import {SandpackFiles} from "@codesandbox/sandpack-react/types";
|
||||
|
||||
import {StackblitzIcon} from "./icons";
|
||||
|
||||
import {useStackblitz} from "@/hooks/use-stackblitz";
|
||||
import {Tooltip} from "@/../../packages/components/tooltip/src";
|
||||
import {Button, ButtonProps} from "@/../../packages/components/button/src";
|
||||
|
||||
export interface StackblitzButtonProps extends ButtonProps {
|
||||
files: SandpackFiles;
|
||||
typescriptStrict?: boolean;
|
||||
className?: string;
|
||||
button?: React.ReactElement;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const StackblitzButton = forwardRef<HTMLButtonElement, StackblitzButtonProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
files,
|
||||
typescriptStrict = false,
|
||||
className,
|
||||
button = <Button />,
|
||||
icon = (
|
||||
<StackblitzIcon
|
||||
data-visible
|
||||
className="opacity-0 data-[visible=true]:opacity-100 transition-transform-opacity"
|
||||
/>
|
||||
),
|
||||
...rest
|
||||
} = props;
|
||||
const {stackblitzPrefillConfig, entryFile} = useStackblitz({
|
||||
files,
|
||||
typescriptStrict,
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip closeDelay={0} content="Open in Stackblitz">
|
||||
{React.cloneElement(button, {
|
||||
ref,
|
||||
className,
|
||||
icon,
|
||||
onPress: () => {
|
||||
stackblitzSdk.openProject(stackblitzPrefillConfig, {
|
||||
openFile: [entryFile],
|
||||
});
|
||||
},
|
||||
children: icon,
|
||||
...rest,
|
||||
})}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
StackblitzButton.displayName = "StackblitzButton";
|
||||
88
apps/docs/hooks/use-stackblitz.ts
Normal file
88
apps/docs/hooks/use-stackblitz.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import {Project} from "@stackblitz/sdk";
|
||||
import {SandpackFiles} from "@codesandbox/sandpack-react/types";
|
||||
|
||||
import {mapKeys, omit} 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()],
|
||||
});
|
||||
`;
|
||||
|
||||
function transformSandpackFiles(files: SandpackFiles) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(files).map(([key, value]) => [
|
||||
key,
|
||||
typeof value === "string" ? value : value.code,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export function useStackblitz(props: UseSandpackProps) {
|
||||
const {files, typescriptStrict = false} = props;
|
||||
|
||||
const {
|
||||
customSetup,
|
||||
files: filesData,
|
||||
entryFile,
|
||||
} = useSandpack({
|
||||
files: transformSandpackFiles(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(omit(dependencies as any, ["react", "react-dom"]))
|
||||
.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: {
|
||||
...transformSandpackFiles(transformFiles),
|
||||
"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,
|
||||
};
|
||||
}
|
||||
@ -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",
|
||||
@ -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.11.2",
|
||||
"canvas-confetti": "^1.9.2",
|
||||
"clsx": "^1.2.1",
|
||||
"cmdk": "^0.2.0",
|
||||
"color2k": "2.0.3",
|
||||
"contentlayer2": "0.5.3",
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -376,6 +376,9 @@ importers:
|
||||
'@rehooks/local-storage':
|
||||
specifier: ^2.4.5
|
||||
version: 2.4.5(react@18.2.0)
|
||||
'@stackblitz/sdk':
|
||||
specifier: ^1.11.0
|
||||
version: 1.11.0
|
||||
'@tanstack/react-virtual':
|
||||
specifier: 3.11.2
|
||||
version: 3.11.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@ -7130,6 +7133,9 @@ packages:
|
||||
'@sinonjs/fake-timers@10.3.0':
|
||||
resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==}
|
||||
|
||||
'@stackblitz/sdk@1.11.0':
|
||||
resolution: {integrity: sha512-DFQGANNkEZRzFk1/rDP6TcFdM82ycHE+zfl9C/M/jXlH68jiqHWHFMQURLELoD8koxvu/eW5uhg94NSAZlYrUQ==}
|
||||
|
||||
'@stitches/core@1.2.8':
|
||||
resolution: {integrity: sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==}
|
||||
|
||||
@ -19160,6 +19166,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@sinonjs/commons': 3.0.1
|
||||
|
||||
'@stackblitz/sdk@1.11.0': {}
|
||||
|
||||
'@stitches/core@1.2.8': {}
|
||||
|
||||
'@storybook/addon-a11y@8.4.5(storybook@8.4.5(prettier@3.3.3))':
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user