diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 7e694ec..68260a9 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -4,22 +4,22 @@ import { BadgeShape, ThemeVariant, getVariantColors } from "./utils"; interface BadgeProps extends React.HTMLAttributes { $variant?: ThemeVariant; $shape?: BadgeShape; - $isInteractive?: boolean; + $isClickable?: boolean; } /** * Badge component for compact status/label pills. - * Displays colored badges with different variants (success, warning, error, info) and shapes. + * Displays colored badges with different variants (primary, success, warning, error, accent) and shapes. * Supports interactive mode with hover effects for clickable badges. * @param $variant - Visual variant using ThemeVariant enum. * @param $shape - Shape of the badge: BadgeShape.Pill (fully rounded ends) or BadgeShape.Rounded (medium border radius). Defaults to Pill. - * @param $isInteractive - When true, adds hover/active states including translateY animation and shadow for clickable badges. Defaults to false. + * @param $isClickable - When true, adds hover/active states including translateY animation and shadow for clickable badges. Defaults to false. * @example * // Simple success badge * Active * @example * // Interactive warning badge with rounded shape - * Click me + * Click me */ export const Badge = styled.span` display: inline-flex; @@ -31,19 +31,19 @@ export const Badge = styled.span` border-radius: ${({ $shape = BadgeShape.Pill, theme }) => $shape === BadgeShape.Rounded ? theme.radius.md : theme.radius.full}; border: 1px solid - ${({ $variant = ThemeVariant.Info, theme }) => + ${({ $variant = ThemeVariant.Primary, theme }) => getVariantColors(theme, $variant).border}; font: ${({ theme }) => theme.typography.small.font}; line-height: ${({ theme }) => theme.fonts.lineHeight.mini}; - background: ${({ $variant = ThemeVariant.Info, theme }) => + background: ${({ $variant = ThemeVariant.Primary, theme }) => getVariantColors(theme, $variant).bg}; - color: ${({ $variant = ThemeVariant.Info, theme }) => + color: ${({ $variant = ThemeVariant.Primary, theme }) => getVariantColors(theme, $variant).text}; - ${({ $isInteractive, theme }) => - $isInteractive + ${({ $isClickable, theme }) => + $isClickable ? ` cursor: pointer; transition: transform ${theme.transitions.base}, diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 529e611..0df0780 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,9 +1,6 @@ -import { Children } from "react"; import styled from "styled-components"; -import { ControlLabel } from "./Typography"; import { - ButtonWidth, - getThemeSizeValuesByThemeSize, + getControlSize, getVariantColors, ThemeSize, ThemeVariant, @@ -13,85 +10,72 @@ export interface ButtonProps extends React.ButtonHTMLAttributesSubmit + * * @example - * // Ghost error button with auto width - * + * // Ghost error button + * * @example * // Large success button * */ -export const Button = styled.button.attrs(({ children }) => ({ - children: Children.map(children, (child) => - typeof child === "string" || typeof child === "number" - ? {child} - : child, - ), -}))` +export const Button = styled.button` display: inline-flex; align-items: center; justify-content: center; - gap: ${({ $size = ThemeSize.Medium }) => - getThemeSizeValuesByThemeSize($size)}; - ${({ $width }) => ($width === ButtonWidth.Full ? "width: 100%;" : "")} + ${({ $fullWidth }) => ($fullWidth ? "width: 100%;" : "")} ${({ $size = ThemeSize.Medium, theme }) => { - switch ($size) { - case ThemeSize.Small: - return ` - padding: ${theme.spacing.xs} ${theme.spacing.sm}; - `; - case ThemeSize.Medium: - return ` - padding: ${theme.spacing.sm} ${theme.spacing.md}; - `; - case ThemeSize.Large: - default: - return ` - padding: ${theme.spacing.md} ${theme.spacing.lg}; - `; - } + const { height, paddingX, gap } = getControlSize(theme, $size); + return ` + gap: ${gap}; + height: ${height}; + padding: 0 ${paddingX}; + `; }} border-radius: ${({ theme }) => theme.radius.md}; - ${({ $variant = ThemeVariant.Info, $ghost, theme }) => { + ${({ $variant = ThemeVariant.Primary, $ghost, theme }) => { const colors = getVariantColors(theme, $variant); - + const darken = (amount: string) => + `color-mix(in srgb, ${colors.solidBg}, ${theme.colors.shade} ${amount})`; + const hoverBg = $ghost + ? colors.solidBg + : darken(theme.interactions.hoverDarken); + const activeBg = darken(theme.interactions.activeDarken); + const disabledBorder = $ghost + ? theme.colors.gray[500] + : theme.colors.gray[300]; + + // A filled button always has a border in the same color as its fill. return ` - border: 1px solid ${colors.border}; + border: 1px solid ${$ghost ? colors.border : colors.solidBg}; background-color: ${$ghost ? "transparent" : colors.solidBg}; color: ${$ghost ? colors.border : colors.solidText}; &:hover:not(:disabled) { - background-color: color-mix( - in srgb, - ${colors.solidBg}, - ${theme.colors.shade} ${theme.interactions.hoverDarken} - ); + border-color: ${hoverBg}; + background-color: ${hoverBg}; } &:active:not(:disabled) { - background-color: color-mix( - in srgb, - ${colors.solidBg}, - ${theme.colors.shade} ${theme.interactions.activeDarken} - ); + border-color: ${activeBg}; + background-color: ${activeBg}; } ${ @@ -105,7 +89,7 @@ export const Button = styled.button.attrs(({ children }) => ({ } &:disabled { - border-color: ${theme.colors.gray[500]}; + border-color: ${disabledBorder}; background-color: ${$ghost ? "transparent" : theme.colors.gray[300]}; opacity: ${theme.interactions.disabledOpacity}; @@ -117,8 +101,9 @@ export const Button = styled.button.attrs(({ children }) => ({ }} font: ${({ theme }) => theme.typography.bodyMedium.font}; + font-size: ${({ $size = ThemeSize.Medium, theme }) => + getControlSize(theme, $size).fontSize}; - cursor: pointer; transition: background-color ${({ theme }) => theme.transitions.fast}, border-color ${({ theme }) => theme.transitions.fast}, diff --git a/src/components/CodeEditor.tsx b/src/components/CodeEditor.tsx index 4f39af8..c645b13 100644 --- a/src/components/CodeEditor.tsx +++ b/src/components/CodeEditor.tsx @@ -12,7 +12,6 @@ const EditorTextarea = styled.textarea<{ $minHeight?: string; }>` width: 100%; - box-sizing: border-box; min-height: ${({ $minHeight = "400px" }) => $minHeight}; padding: ${({ theme }) => theme.spacing.md}; @@ -24,7 +23,7 @@ const EditorTextarea = styled.textarea<{ background-color: ${({ theme }) => theme.colors.background.secondary}; color: ${({ theme }) => theme.colors.text.primary}; - font: ${({ theme }) => theme.typography.code_small.font}; + font: ${({ theme }) => theme.typography.code.font}; resize: vertical; @@ -44,8 +43,8 @@ const EditorTextarea = styled.textarea<{ export interface CodeEditorProps extends React.HTMLAttributes { $value: string; $onChange: (value: string) => void; - $placeholder?: string; - $disabled?: boolean; + placeholder?: string; + disabled?: boolean; $hasError?: boolean; $minHeight?: string; } @@ -55,22 +54,22 @@ export interface CodeEditorProps extends React.HTMLAttributes { * It is a controlled component and relies on `$value` / `$onChange` for state. * @param $value - Current text content of the editor. * @param $onChange - Callback invoked with new text when the content changes. - * @param $placeholder - Optional placeholder text when editor is empty. - * @param $disabled - When true the editor is non-editable and shows disabled visuals. + * @param placeholder - Optional placeholder text when editor is empty. + * @param disabled - When true the editor is non-editable and shows disabled visuals. * @param $hasError - When true the editor displays an error border state. * @param $minHeight - Optional CSS min-height for the editor area (defaults to `400px`). * @example: * */ export const CodeEditor: React.FC = ({ $value, $onChange, - $placeholder, - $disabled, + placeholder, + disabled, $hasError, $minHeight, ...rest @@ -80,8 +79,8 @@ export const CodeEditor: React.FC = ({ $onChange(e.target.value)} - placeholder={$placeholder} - disabled={$disabled} + placeholder={placeholder} + disabled={disabled} $hasError={$hasError} $minHeight={$minHeight} /> diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx index 8ed44f6..0ea3290 100644 --- a/src/components/ConfirmDialog.tsx +++ b/src/components/ConfirmDialog.tsx @@ -52,7 +52,7 @@ export const ConfirmDialog: React.FC = ({ {...rest} > - {$title} + {$title} {$description} diff --git a/src/components/Containers.tsx b/src/components/Containers.tsx index 31d90bc..97109ad 100644 --- a/src/components/Containers.tsx +++ b/src/components/Containers.tsx @@ -31,8 +31,6 @@ export const PageContainer = styled.div` background-position: center; background-repeat: no-repeat; `} - - padding: 0; `; export const CenteredCardContent = styled.div` diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx index b71340c..5aa0851 100644 --- a/src/components/CopyButton.tsx +++ b/src/components/CopyButton.tsx @@ -5,31 +5,30 @@ import toast from "react-hot-toast"; import { formatErrorForToast } from "@/api/utils"; import { Button } from "./Button"; import Icon from "./Icon"; -import { ThemeSize } from "./utils"; export interface CopyButtonProps extends React.ButtonHTMLAttributes { - $text: string; + $value: string; } /** * Small icon-only button to copy text to clipboard. - * @param $text - The text to copy to clipboard. + * @param $value - The text to copy to the clipboard. * @param title - The tooltip/title for the button (default: "Copy"). * @param disabled - Whether the button is disabled (default: false). * @example - * + * */ export const CopyButton: React.FC = ({ - $text, + $value, disabled = false, ...rest }) => { const handleClick = async (e: React.MouseEvent) => { e.stopPropagation(); - if (disabled || !$text) return; + if (disabled || !$value) return; try { - await navigator.clipboard.writeText($text); + await navigator.clipboard.writeText($value); toast.success("Copied to clipboard"); } catch (err) { toast.error(formatErrorForToast(err, "Failed to copy to clipboard")); @@ -40,7 +39,6 @@ export const CopyButton: React.FC = ({ ); diff --git a/src/components/FileDrop.tsx b/src/components/FileDrop.tsx index 72cb746..fa7f2d4 100644 --- a/src/components/FileDrop.tsx +++ b/src/components/FileDrop.tsx @@ -21,7 +21,7 @@ const DropZone = styled.div<{ text-align: center; cursor: pointer; - min-height: 400px; + min-height: 12rem; display: flex; flex-direction: column; align-items: center; @@ -36,10 +36,18 @@ const DropZone = styled.div<{ border-color: ${({ theme }) => theme.colors.info.border}; background-color: ${({ theme }) => theme.colors.info.bg}; } + + /* Disabled state comes from the hidden file input right before the label. */ + input:disabled + label & { + opacity: ${({ theme }) => theme.interactions.disabledOpacity}; + cursor: not-allowed; + + border-color: ${({ theme }) => theme.colors.border.medium}; + background-color: ${({ theme }) => theme.colors.background.secondary}; + } `; const DropZoneText = styled.p` - margin: 0; font: ${({ theme }) => theme.typography.body.font}; color: ${({ theme }) => theme.colors.text.secondary}; `; @@ -64,8 +72,8 @@ export interface FileDropProps extends React.HTMLAttributes { $onChange: (content: string, filename?: string) => void; $onRemove?: () => void; $accept?: string; - $disabled?: boolean; - $error?: string; + disabled?: boolean; + $hasError?: boolean; } /** @@ -77,8 +85,8 @@ export interface FileDropProps extends React.HTMLAttributes { * @param $onChange - Called with (content, filename) when a file is selected or dropped. * @param $onRemove - Optional callback when the remove button is clicked. * @param $accept - File extension filter (e.g. ".json"). Defaults to ".json". - * @param $disabled - When true, disables interactions. - * @param $error - Optional error message to display under the drop zone. + * @param disabled - When true, disables interactions. + * @param $hasError - When true the drop zone shows an error border. The page shows the error message itself. * @example * setFile(content)} $accept=".json" /> */ @@ -87,8 +95,8 @@ export const FileDrop: React.FC = ({ $onChange, $onRemove, $accept = ".json", - $disabled, - $error, + disabled, + $hasError = false, ...rest }) => { const [isDragActive, setIsDragActive] = useState(false); @@ -114,7 +122,7 @@ export const FileDrop: React.FC = ({ (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - if ($disabled) return; + if (disabled) return; setIsDragActive(false); const files = e.dataTransfer.files; if (files && files.length > 0) { @@ -126,17 +134,17 @@ export const FileDrop: React.FC = ({ readFile(file); } }, - [$accept, $disabled, $onChange, readFile], + [$accept, disabled, $onChange, readFile], ); const handleDragOver = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - if ($disabled) return; + if (disabled) return; setIsDragActive(true); }, - [$disabled], + [disabled], ); const handleDragLeave = useCallback( @@ -171,19 +179,19 @@ export const FileDrop: React.FC = ({ accept={$accept} onChange={handleFileSelect} style={{ display: "none" }} - disabled={$disabled} + disabled={disabled} />