mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
fix(dashboard): UI bug fixes (#1377)
## Summary Rolling PR for dashboard UI bug fixes. Each fix is appended to the **Fix log** below with before/after screenshots. This PR stays open until we batch-merge or split. --- ## Fix log ### 1. Hide Alpha/Beta stage badges in onboarding "Select apps" tooltip **Bug:** On the new-project onboarding, hovering an app card showed an "Alpha" or "Beta" stage badge next to the app name in the tooltip. These shouldn't be surfaced on the onboarding step. **Fix:** Removed the stage badge from the onboarding app-card tooltip only. The "Required" badge is preserved, and stage badges on other surfaces (app management, app store, command palette) are unchanged. #### Before / After — Beta (Payments) | Before | After | | --- | --- | |  |  | #### Before / After — Alpha (Onboarding) | Before | After | | --- | --- | |  |  | --- ### 2. Eliminate full-page flash when advancing onboarding steps **Bug:** Moving between onboarding steps (e.g. Configure authentication → Select email theme) briefly blanked out the entire page — only the navbar remained visible for roughly two seconds — before the next step rendered. It felt like a complete browser reload. **Fix:** Contained the suspension inside the wizard. A local Suspense boundary around the onboarding page means that when any data cache refresh fires during the step advance, the suspension no longer bubbles up to the site-wide loading indicator. The step-advance state update is also marked as a React transition, so the current step stays rendered until the next step is ready to commit. Net effect: the previous step is visible throughout the save, then the next step swaps in without a blank frame. #### Before — full blank flash mid-transition | Auth step (start) | Mid-transition (blank) | Email theme step (end) | | --- | --- | --- | |  |  |  | #### After — previous step stays visible, no blank frame | Auth step (start) | Mid-transition (auth stays visible) | Email theme step (end) | | --- | --- | --- | |  |  |  | --- ### 3. Add a subtle back arrow to the onboarding timeline **Bug:** The only way to return to a previous step in the new-project onboarding was to click one of the tiny completed-step dots at the bottom of the page — not discoverable, and easy to miss. **Fix:** Added a small muted left-arrow next to the timeline dots. Clicking it advances back one step. It's absolute-positioned so the dots stay perfectly centered, and it hides itself on the first step (where there's nothing to go back to). #### Before / After — Select apps step | Before — dots only | After — back arrow next to the dots | | --- | --- | |  |  | ### 4. Unify onboarding step styling — cards everywhere, no glassmorphism **Bug:** Step-to-step styling in the onboarding was inconsistent. The Config and Email-theme steps used a glassmorphic surround (`backdrop-blur`, translucent whites) while the other steps used solid cards. Advancing from auth to email made it look like the visual language had changed mid-flow. **Fix:** Dropped the glassmorphic variants from the onboarding wizard. The config-choice option cards, the email-theme container, and the `ModeNotImplementedCard` surround all now use the same solid card treatment (`bg-white/90` light, `bg-white/[0.06]` dark, with subtle ring). One consistent surface across every step. #### Before / After — Config choice step | Before — glassmorphic | After — solid card | | --- | --- | |  |  | #### Before / After — Email theme step | Before — glassmorphic | After — solid card | | --- | --- | |  |  | ### 5. Add "Copy prompt" button on the project setup page **Bug:** The post-project-creation setup page surfaces a terminal command for every framework (Next.js, React, JS, Python), but there was no one-click handoff for users who drive their setup through an AI agent. Users had to manually copy the command, figure out whether the Stack Auth MCP server got registered, and add it themselves if not. **Fix:** Added a compact **✦ Copy prompt** button at the top-right above the steps list. Clicking it copies a framework-aware prompt to the clipboard — the prompt tells the user's AI agent to run the install command for the currently-selected framework, then verify the Stack Auth MCP server (`stack-auth`, transport `http`, `https://mcp.stack-auth.com/`) is registered in its client config and add it manually if the install didn't. #### Before / After — Project setup page | Before — no AI handoff | After — "Copy prompt" at the top-right | | --- | --- | |  |  | ### 6. Disable email theme cards while the onboarding step is saving **Bug:** On the "Select an email theme" step, the theme cards stayed clickable after clicking Continue. Because we keep the previous step visible during the step-advance transition (fix #2), users could click through to a different theme mid-save — the server would then commit whatever selection was active at click time, not the one on screen when Continue was pressed. **Fix:** Added `disabled={saving}` to the email theme buttons, matching the same pattern the config-choice, apps-selection, and auth-setup steps already follow. Added `disabled:cursor-not-allowed disabled:opacity-60` so users get a clear visual signal that the cards are locked while the save is in flight. --- <!-- Append new fixes above this line. Template: ### N. <title> **Bug:** … **Fix:** … #### Before / After | Before | After | | --- | --- | |  |  | --> ## Test plan - [ ] Load the new-project onboarding "Select apps" step and hover every app card — no Alpha/Beta badge appears. - [ ] Hover a required app — "Required" badge still appears. - [ ] Confirm app management tooltips, app store detail page, and command palette still show stage badges (out of scope for this PR). - [ ] Drive the onboarding from Configure authentication to Select email theme — the auth panel stays rendered throughout the save phase and the email panel swaps in without the site-wide loading indicator or a blank content area. - [ ] Repeat for other step transitions (Config → Apps, Apps → Auth, Email → Domain, Domain → Payments) — same seamless behavior. - [ ] From any step after Config, the back arrow appears to the left of the dots. Clicking it goes back one step. On the first step, the arrow is not rendered. - [ ] Walk through every onboarding step. Container surface is visually consistent across steps — no glassmorphic/card mismatch between Config, Apps, Auth, Email Theme, Payments. - [ ] On the project setup page, the "Copy prompt" button appears above the steps (top-right). Clicking it copies the prompt for the currently-selected framework (Next.js / React / JS / Python) and shows a success toast. - [ ] On the "Select an email theme" step, click Continue — the three theme cards become visibly dimmed (`opacity-60`, `cursor-not-allowed`) for the duration of the save and don't respond to clicks. Once the next step renders they stop being visible anyway. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added back navigation to onboarding wizard steps. * Added "Copy prompt" button for framework-aware terminal commands with MCP verification. * Added loading indicator during asynchronous operations. * **UI/UX Improvements** * Updated card styling for unselected options. * Disabled email theme selection during save operations. * Removed stage badges (Alpha/Beta) from app cards. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
e2dc5f5ee0
commit
ed8961069c
@ -23,7 +23,7 @@ import {
|
||||
Typography,
|
||||
cn,
|
||||
} from "@/components/ui";
|
||||
import { CheckCircleIcon, WarningCircleIcon } from "@phosphor-icons/react";
|
||||
import { ArrowLeftIcon, CheckCircleIcon, WarningCircleIcon } from "@phosphor-icons/react";
|
||||
import { AdminOwnedProject } from "@stackframe/stack";
|
||||
import { ALL_APPS, type AppId } from "@stackframe/stack-shared/dist/apps/apps-config";
|
||||
import { previewTemplateSource } from "@stackframe/stack-shared/dist/helpers/emails";
|
||||
@ -40,6 +40,7 @@ export type OnboardingPageProps = {
|
||||
disabled?: boolean,
|
||||
primaryAction: ReactNode,
|
||||
secondaryAction?: ReactNode,
|
||||
onBack?: () => void,
|
||||
wide?: boolean,
|
||||
actionsLayout?: "stacked" | "inline",
|
||||
children: ReactNode,
|
||||
@ -92,7 +93,18 @@ export function OnboardingPage(props: OnboardingPageProps) {
|
||||
</div>
|
||||
|
||||
<div className="onboarding-cascade fixed bottom-6 left-0 right-0 z-50 flex justify-center" style={{ "--cascade-i": 3 } as CSSProperties}>
|
||||
<div className="flex items-center gap-[5px]">
|
||||
<div className="relative flex items-center gap-[5px]">
|
||||
{props.onBack != null && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onBack}
|
||||
disabled={props.disabled}
|
||||
aria-label="Go back to previous step"
|
||||
className="absolute right-full mr-3 inline-flex h-5 w-5 items-center justify-center rounded-full text-foreground/40 transition-colors hover:text-foreground/80 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
<ArrowLeftIcon className="h-3.5 w-3.5" weight="bold" />
|
||||
</button>
|
||||
)}
|
||||
{props.steps.map((step, index) => {
|
||||
const isComplete = index < currentIndex;
|
||||
const isCurrent = index === currentIndex;
|
||||
@ -124,16 +136,6 @@ export function OnboardingPage(props: OnboardingPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function appStageBadgeColor(stage: (typeof ALL_APPS)[AppId]["stage"]) {
|
||||
if (stage === "alpha") {
|
||||
return "orange";
|
||||
}
|
||||
if (stage === "beta") {
|
||||
return "blue";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export type OnboardingAppCardProps = {
|
||||
appId: AppId,
|
||||
selected: boolean,
|
||||
@ -145,7 +147,6 @@ export type OnboardingAppCardProps = {
|
||||
|
||||
export function OnboardingAppCard(props: OnboardingAppCardProps) {
|
||||
const app = ALL_APPS[props.appId];
|
||||
const stageBadgeColor = appStageBadgeColor(app.stage);
|
||||
|
||||
return (
|
||||
<Tooltip delayDuration={0}>
|
||||
@ -190,13 +191,6 @@ export function OnboardingAppCard(props: OnboardingAppCardProps) {
|
||||
{props.required && (
|
||||
<DesignBadge label="Required" color="orange" size="sm" />
|
||||
)}
|
||||
{!props.required && stageBadgeColor != null && (
|
||||
<DesignBadge
|
||||
label={app.stage === "alpha" ? "Alpha" : "Beta"}
|
||||
color={stageBadgeColor}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Typography className="text-xs leading-relaxed text-muted-foreground">
|
||||
{app.subtitle}
|
||||
@ -544,7 +538,6 @@ export function ModeNotImplementedCard(props: { onBack: () => void }) {
|
||||
variant="warning"
|
||||
title="Not available yet"
|
||||
description="Linking an existing config into onboarding is not available yet."
|
||||
glassmorphic
|
||||
/>
|
||||
<div className="flex justify-center">
|
||||
<DesignButton variant="outline" className="rounded-full px-8" onClick={props.onBack}>
|
||||
|
||||
@ -26,7 +26,7 @@ import { PlusCircleIcon } from "@phosphor-icons/react";
|
||||
import { AdminOwnedProject, useStackApp, useUser } from "@stackframe/stack";
|
||||
import { runAsynchronouslyWithAlert, wait } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Suspense, useCallback, useEffect, useMemo, useRef, useState, useTransition } from "react";
|
||||
|
||||
import type { ProjectOnboardingStatus } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { ProjectOnboardingWizard } from "./project-onboarding-wizard";
|
||||
@ -38,6 +38,20 @@ import {
|
||||
} from "./shared";
|
||||
|
||||
export default function PageClient() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex w-full flex-grow items-center justify-center">
|
||||
<Spinner size={24} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<PageClientInner />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function PageClientInner() {
|
||||
const app = useStackApp();
|
||||
const appInternals = useMemo(() => getStackAppInternals(app), [app]);
|
||||
const user = useUser({ or: "redirect", projectIdMustMatch: "internal" });
|
||||
@ -55,6 +69,7 @@ export default function PageClient() {
|
||||
|
||||
const [projectStatuses, setProjectStatuses] = useState<Map<string, ProjectOnboardingStatus>>(new Map());
|
||||
const [loadingStatuses, setLoadingStatuses] = useState(true);
|
||||
const [, startStatusTransition] = useTransition();
|
||||
const [projectName, setProjectName] = useState(displayNameFromSearch ?? "");
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
|
||||
const [creatingTeam, setCreatingTeam] = useState(false);
|
||||
@ -181,10 +196,12 @@ export default function PageClient() {
|
||||
throw new Error(`Failed to update onboarding status: ${response.status} ${await response.text()}`);
|
||||
}
|
||||
|
||||
setProjectStatuses((previous) => {
|
||||
const next = new Map(previous);
|
||||
next.set(project.id, status);
|
||||
return next;
|
||||
startStatusTransition(() => {
|
||||
setProjectStatuses((previous) => {
|
||||
const next = new Map(previous);
|
||||
next.set(project.id, status);
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
await appInternals.refreshOwnedProjects();
|
||||
|
||||
@ -166,6 +166,14 @@ export function ProjectOnboardingWizard(props: {
|
||||
});
|
||||
}, [currentTimelineIndex, props.mode, setMode, setStatus, timelineSteps]);
|
||||
|
||||
const handleBack = useMemo(() => {
|
||||
if (currentTimelineIndex <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
const previousStep = timelineSteps[currentTimelineIndex - 1].id;
|
||||
return () => handleTimelineStepClick(previousStep);
|
||||
}, [currentTimelineIndex, handleTimelineStepClick, timelineSteps]);
|
||||
|
||||
const advanceFromDomainSetup = useCallback(() => {
|
||||
return runAsynchronouslyWithAlert(async () => {
|
||||
setDomainSetupAutoAdvanceError(null);
|
||||
@ -304,6 +312,7 @@ export function ProjectOnboardingWizard(props: {
|
||||
steps={timelineSteps}
|
||||
currentStep="config_choice"
|
||||
onStepClick={handleTimelineStepClick}
|
||||
onBack={handleBack}
|
||||
disabled={saving}
|
||||
primaryAction={
|
||||
<DesignButton
|
||||
@ -330,7 +339,7 @@ export function ProjectOnboardingWizard(props: {
|
||||
"relative flex flex-col items-center gap-6 rounded-2xl p-10 text-center transition-[box-shadow,background-color] duration-150 hover:transition-none",
|
||||
createNewSelected
|
||||
? "bg-white ring-2 ring-blue-500/50 shadow-md dark:bg-blue-500/[0.08] dark:ring-blue-500/50 dark:shadow-none"
|
||||
: "bg-white/50 ring-1 ring-black/[0.06] hover:ring-black/[0.10] dark:bg-background/60 dark:backdrop-blur-xl dark:ring-white/[0.06] dark:hover:ring-white/[0.10]",
|
||||
: "bg-white/90 ring-1 ring-black/[0.06] hover:ring-black/[0.10] dark:bg-white/[0.06] dark:ring-white/[0.10] dark:hover:ring-white/[0.14]",
|
||||
)}
|
||||
>
|
||||
{createNewSelected && (
|
||||
@ -358,7 +367,7 @@ export function ProjectOnboardingWizard(props: {
|
||||
"relative flex flex-col items-center gap-6 rounded-2xl p-10 text-center transition-[box-shadow,background-color] duration-150 hover:transition-none",
|
||||
linkExistingSelected
|
||||
? "bg-white ring-2 ring-blue-500/50 shadow-md dark:bg-blue-500/[0.08] dark:ring-blue-500/50 dark:shadow-none"
|
||||
: "bg-white/50 ring-1 ring-black/[0.06] hover:ring-black/[0.10] dark:bg-background/60 dark:backdrop-blur-xl dark:ring-white/[0.06] dark:hover:ring-white/[0.10]",
|
||||
: "bg-white/90 ring-1 ring-black/[0.06] hover:ring-black/[0.10] dark:bg-white/[0.06] dark:ring-white/[0.10] dark:hover:ring-white/[0.14]",
|
||||
)}
|
||||
>
|
||||
{linkExistingSelected && (
|
||||
@ -398,6 +407,7 @@ export function ProjectOnboardingWizard(props: {
|
||||
steps={timelineSteps}
|
||||
currentStep="apps_selection"
|
||||
onStepClick={handleTimelineStepClick}
|
||||
onBack={handleBack}
|
||||
disabled={saving}
|
||||
wide
|
||||
primaryAction={
|
||||
@ -512,6 +522,7 @@ export function ProjectOnboardingWizard(props: {
|
||||
steps={timelineSteps}
|
||||
currentStep="auth_setup"
|
||||
onStepClick={handleTimelineStepClick}
|
||||
onBack={handleBack}
|
||||
disabled={saving}
|
||||
wide
|
||||
primaryAction={
|
||||
@ -668,6 +679,7 @@ export function ProjectOnboardingWizard(props: {
|
||||
steps={timelineSteps}
|
||||
currentStep="email_theme_setup"
|
||||
onStepClick={handleTimelineStepClick}
|
||||
onBack={handleBack}
|
||||
disabled={saving}
|
||||
wide
|
||||
primaryAction={
|
||||
@ -705,7 +717,6 @@ export function ProjectOnboardingWizard(props: {
|
||||
variant="warning"
|
||||
title="No themes found"
|
||||
description="Theme selection is temporarily unavailable. You can still continue."
|
||||
glassmorphic
|
||||
/>
|
||||
)}
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
@ -716,8 +727,10 @@ export function ProjectOnboardingWizard(props: {
|
||||
key={theme.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedEmailThemeId(theme.id)}
|
||||
disabled={saving}
|
||||
className={cn(
|
||||
"relative flex flex-col overflow-hidden rounded-2xl text-left transition-[box-shadow,background-color] duration-150 hover:transition-none",
|
||||
"disabled:cursor-not-allowed disabled:opacity-60",
|
||||
isSelected
|
||||
? cn(
|
||||
"bg-blue-500/[0.06] dark:bg-blue-500/[0.04] ring-1 ring-blue-500/40",
|
||||
@ -725,8 +738,8 @@ export function ProjectOnboardingWizard(props: {
|
||||
"dark:shadow-[0_14px_48px_-10px_rgba(96,165,250,0.38),0_0_1px_rgba(96,165,250,0.25)]",
|
||||
)
|
||||
: cn(
|
||||
"bg-white/60 dark:bg-background/40 dark:backdrop-blur-xl",
|
||||
"ring-1 ring-black/[0.05] hover:ring-black/[0.09] dark:ring-white/[0.05] dark:hover:ring-white/[0.09]",
|
||||
"bg-white/90 dark:bg-white/[0.06]",
|
||||
"ring-1 ring-black/[0.06] hover:ring-black/[0.10] dark:ring-white/[0.10] dark:hover:ring-white/[0.14]",
|
||||
),
|
||||
)}
|
||||
>
|
||||
@ -773,6 +786,7 @@ export function ProjectOnboardingWizard(props: {
|
||||
steps={timelineSteps}
|
||||
currentStep="payments_setup"
|
||||
onStepClick={handleTimelineStepClick}
|
||||
onBack={handleBack}
|
||||
disabled={saving}
|
||||
actionsLayout="inline"
|
||||
primaryAction={
|
||||
|
||||
@ -4,10 +4,10 @@ import { CodeBlock } from '@/components/code-block';
|
||||
import { APIEnvKeys, NextJsEnvKeys } from '@/components/env-keys';
|
||||
import { InlineCode } from '@/components/inline-code';
|
||||
import { StyledLink } from '@/components/link';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger, Typography, cn } from "@/components/ui";
|
||||
import { CopyPromptButton, Tabs, TabsContent, TabsList, TabsTrigger, Typography, cn } from "@/components/ui";
|
||||
import { DesignButton } from "@/components/design-components";
|
||||
import { useThemeWatcher } from '@/lib/theme';
|
||||
import { BookIcon, XIcon } from "@phosphor-icons/react";
|
||||
import { BookIcon, SparkleIcon, XIcon } from "@phosphor-icons/react";
|
||||
import { use } from "@stackframe/stack-shared/dist/utils/react";
|
||||
import { deindent } from '@stackframe/stack-shared/dist/utils/strings';
|
||||
import dynamic from "next/dynamic";
|
||||
@ -25,6 +25,21 @@ const Globe = dynamic(() => import('react-globe.gl').then((mod) => mod.default),
|
||||
const commandClasses = "text-red-600 dark:text-red-400";
|
||||
const nameClasses = "text-green-600 dark:text-green-500";
|
||||
|
||||
const INSTALL_COMMAND_BY_FRAMEWORK = {
|
||||
nextjs: 'npx @stackframe/stack-cli@latest init',
|
||||
react: 'npm install @stackframe/react',
|
||||
javascript: 'npm install @stackframe/js',
|
||||
python: 'pip install requests',
|
||||
} as const;
|
||||
|
||||
const buildInstallPrompt = (command: string) => deindent`
|
||||
Please run the following command in my project's terminal:
|
||||
|
||||
${command}
|
||||
|
||||
After it finishes, verify that the Stack Auth MCP server is registered in your AI client config — name: \`stack-auth\`, transport: \`http\`, URL: \`https://mcp.stack-auth.com/\`. The command above should handle this automatically; if for any reason it didn't, please add the MCP server manually so you have live access to Stack Auth docs and APIs.
|
||||
`;
|
||||
|
||||
export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
const adminApp = useAdminApp();
|
||||
const [selectedFramework, setSelectedFramework] = useState<'nextjs' | 'react' | 'javascript' | 'python'>('nextjs');
|
||||
@ -461,7 +476,18 @@ export default function SetupPage(props: { toMetrics: () => void }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-10 mx-4">
|
||||
<div className="flex justify-end mt-8 mx-4">
|
||||
<CopyPromptButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
content={buildInstallPrompt(INSTALL_COMMAND_BY_FRAMEWORK[selectedFramework])}
|
||||
>
|
||||
<SparkleIcon className="w-4 h-4 mr-2 text-purple-500 dark:text-purple-400" weight="fill" />
|
||||
Copy prompt
|
||||
</CopyPromptButton>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-4 mx-4">
|
||||
<ol className="relative text-gray-500 border-s border-gray-200 dark:border-gray-700 dark:text-gray-400 ">
|
||||
{[
|
||||
{
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CopyIcon } from "@phosphor-icons/react";
|
||||
import { CopyIcon, SparkleIcon } from "@phosphor-icons/react";
|
||||
import { forwardRefIfNeeded } from "@stackframe/stack-shared/dist/utils/react";
|
||||
import React from "react";
|
||||
import { Button } from "./button";
|
||||
@ -35,5 +35,32 @@ const CopyButton = forwardRefIfNeeded<
|
||||
});
|
||||
CopyButton.displayName = "CopyButton";
|
||||
|
||||
export { CopyButton };
|
||||
const CopyPromptButton = forwardRefIfNeeded<
|
||||
React.ElementRef<typeof Button>,
|
||||
React.ComponentProps<typeof Button> & { content: string }
|
||||
>(({ content, children, onClick, ...props }, ref) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
{...props}
|
||||
ref={ref}
|
||||
onClick={async (...args) => {
|
||||
await onClick?.(...args);
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
toast({ description: 'Prompt copied — paste it into your AI agent', variant: 'success' });
|
||||
} catch (e) {
|
||||
toast({ description: 'Failed to copy to clipboard', variant: 'destructive' });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children ?? <SparkleIcon className="text-purple-500 dark:text-purple-400" />}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
CopyPromptButton.displayName = "CopyPromptButton";
|
||||
|
||||
export { CopyButton, CopyPromptButton };
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user