=== NEW === - studio/ — MCPEngine Studio scaffold (Next.js monorepo, build plan) - docs/FACTORY-V2.md — Factory v2 architecture doc - docs/CALENDLY_MCP_BUILD_SUMMARY.md — Calendly MCP build report === UPDATED SERVERS === - fieldedge: Added jobs-tools, UI build script, main entry update - lightspeed: Updated main + server entry points - squarespace: Added collection-browser + page-manager apps - toast: Added main + server entry points === INFRA === - infra/command-center/state.json — Updated pipeline state - infra/command-center/FACTORY-V2.md — Factory v2 operator playbook
99 lines
2.8 KiB
TypeScript
99 lines
2.8 KiB
TypeScript
'use client';
|
|
import React from 'react';
|
|
import { clsx } from 'clsx';
|
|
import { twMerge } from 'tailwind-merge';
|
|
|
|
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'success';
|
|
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
|
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
variant?: ButtonVariant;
|
|
size?: ButtonSize;
|
|
loading?: boolean;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
const variantStyles: Record<ButtonVariant, string> = {
|
|
primary:
|
|
'bg-indigo-500 text-white hover:bg-indigo-400 active:bg-indigo-600 focus-visible:ring-indigo-500/50',
|
|
secondary:
|
|
'bg-gray-700 text-gray-200 hover:bg-gray-600 active:bg-gray-800 focus-visible:ring-gray-500/50',
|
|
ghost:
|
|
'bg-transparent text-gray-300 hover:bg-gray-800 hover:text-gray-100 active:bg-gray-700 focus-visible:ring-gray-500/50',
|
|
danger:
|
|
'bg-red-500/10 text-red-400 hover:bg-red-500/20 active:bg-red-500/30 focus-visible:ring-red-500/50',
|
|
success:
|
|
'bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20 active:bg-emerald-500/30 focus-visible:ring-emerald-500/50',
|
|
};
|
|
|
|
const sizeStyles: Record<ButtonSize, string> = {
|
|
sm: 'px-3 py-1.5 text-xs gap-1.5',
|
|
md: 'px-4 py-2 text-sm gap-2',
|
|
lg: 'px-6 py-3 text-base gap-2.5',
|
|
};
|
|
|
|
const Spinner: React.FC<{ className?: string }> = ({ className }) => (
|
|
<svg
|
|
className={twMerge('animate-spin h-4 w-4', className)}
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
/>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
/>
|
|
</svg>
|
|
);
|
|
|
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
(
|
|
{
|
|
variant = 'primary',
|
|
size = 'md',
|
|
loading = false,
|
|
disabled,
|
|
children,
|
|
className,
|
|
...props
|
|
},
|
|
ref,
|
|
) => {
|
|
const isDisabled = disabled || loading;
|
|
|
|
return (
|
|
<button
|
|
ref={ref}
|
|
disabled={isDisabled}
|
|
className={twMerge(
|
|
clsx(
|
|
'inline-flex items-center justify-center font-medium rounded-lg',
|
|
'transition-all duration-150 ease-out',
|
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-900',
|
|
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
|
|
variantStyles[variant],
|
|
sizeStyles[size],
|
|
),
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{loading && <Spinner className={size === 'sm' ? 'h-3 w-3' : 'h-4 w-4'} />}
|
|
{children}
|
|
</button>
|
|
);
|
|
},
|
|
);
|
|
|
|
Button.displayName = 'Button';
|