style(ui): unify components with shared Input/Button

This commit is contained in:
Vlad Gerasimov 2025-08-09 08:08:49 +04:00
parent 6f16fdf2f4
commit 1f96da9381
8 changed files with 98 additions and 23 deletions

View file

@ -12,7 +12,7 @@
:root {
/* Typography */
font-size: 16px;
font-size: 15px;
line-height: 24px;
font-weight: 400;

View file

@ -1,5 +1,7 @@
import React, { useState } from "react";
import { useSettings } from "../../hooks/useSettings";
import { Input } from "../ui/Input";
import { Button } from "../ui/Button";
import { SettingContainer } from "../ui/SettingContainer";
interface CorrectWordsProps {
@ -44,39 +46,43 @@ export const CorrectWords: React.FC<CorrectWordsProps> = React.memo(({
grouped={grouped}
>
<div className="flex items-center gap-2">
<input
<Input
type="text"
className="max-w-[128px]"
value={newWord}
onChange={(e) => setNewWord(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Add a word"
className="px-2 py-1 text-sm border border-mid-gray/30 rounded focus:outline-none focus:ring-1 focus:ring-logo-primary focus:border-transparent bg-background"
variant="compact"
disabled={isUpdating("correct_words")}
/>
<button
<Button
onClick={handleAddWord}
disabled={!newWord.trim() || newWord.includes(' ') || newWord.trim().length > 50 || isUpdating("correct_words")}
className="px-3 py-1 text-xs font-medium text-white bg-logo-primary rounded hover:bg-logo-primary/90 focus:outline-none focus:ring-1 focus:ring-logo-primary disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
variant="primary"
size="md"
>
Add
</button>
</Button>
</div>
</SettingContainer>
{correctWords.length > 0 && (
<div className={`px-4 p-2 ${grouped ? "" : "rounded-lg border border-mid-gray/20"} flex flex-wrap gap-1`}>
{correctWords.map((word) => (
<button
<Button
key={word}
onClick={() => handleRemoveWord(word)}
disabled={isUpdating("correct_words")}
className="inline-flex items-center gap-1 px-2 py-1 bg-mid-gray/10 rounded text-xs hover:bg-red-100 hover:text-red-700 focus:outline-none disabled:opacity-50 cursor-pointer transition-colors"
variant="secondary"
size="sm"
className="inline-flex items-center gap-1 cursor-pointer"
aria-label={`Remove ${word}`}
>
<span>{word}</span>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M6 18L18 6M6 6l12 12" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</Button>
))}
</div>
)}

View file

@ -33,10 +33,13 @@ export const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = React.memo(
await resetSetting("selected_microphone");
};
const microphoneOptions = audioDevices.map(device => ({
value: device.name,
label: device.name
}));
const microphoneOptions = [
{ value: "Default", label: "Default" },
...audioDevices.map(device => ({
value: device.name,
label: device.name
}))
];
return (
<SettingContainer
@ -50,7 +53,7 @@ export const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = React.memo(
options={microphoneOptions}
selectedValue={selectedMicrophone}
onSelect={handleMicrophoneSelect}
placeholder={isLoading ? "Loading..." : "Select microphone..."}
placeholder={isLoading ? "Loading..." : ""}
disabled={isUpdating("selected_microphone") || isLoading}
onRefresh={refreshAudioDevices}
/>

View file

@ -34,10 +34,13 @@ export const OutputDeviceSelector: React.FC<OutputDeviceSelectorProps> = React.m
await resetSetting("selected_output_device");
};
const outputDeviceOptions = outputDevices.map(device => ({
value: device.name,
label: device.name
}));
const outputDeviceOptions = [
{ value: "Default", label: "Default" },
...outputDevices.map(device => ({
value: device.name,
label: device.name
}))
];
return (
<SettingContainer
@ -51,7 +54,7 @@ export const OutputDeviceSelector: React.FC<OutputDeviceSelectorProps> = React.m
options={outputDeviceOptions}
selectedValue={selectedOutputDevice}
onSelect={handleOutputDeviceSelect}
placeholder={isLoading ? "Loading..." : "Select output device..."}
placeholder={isLoading ? "Loading..." : ""}
disabled={isUpdating("selected_output_device") || isLoading}
onRefresh={refreshOutputDevices}
/>

View file

@ -0,0 +1,38 @@
import React from "react";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "danger" | "ghost";
size?: "sm" | "md" | "lg";
}
export const Button: React.FC<ButtonProps> = ({
children,
className = "",
variant = "primary",
size = "md",
...props
}) => {
const baseClasses = "font-medium rounded focus:outline-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed";
const variantClasses = {
primary: "text-white bg-background-ui hover:bg-background-ui/90 focus:ring-1 focus:ring-background-ui",
secondary: "bg-mid-gray/10 hover:bg-background-ui/30 focus:outline-none",
danger: "text-white bg-red-600 hover:bg-red-700 focus:ring-1 focus:ring-red-500",
ghost: "text-current hover:bg-mid-gray/10 focus:bg-mid-gray/20"
};
const sizeClasses = {
sm: "px-2 py-1 text-xs",
md: "px-3 py-1 text-sm",
lg: "px-4 py-2 text-base"
};
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
{...props}
>
{children}
</button>
);
};

View file

@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from "react";
import React, { useEffect, useRef, useState } from "react";
export interface DropdownOption {
value: string;
@ -95,7 +95,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
type="button"
className={`w-full px-2 py-1 text-sm text-left hover:bg-logo-primary/10 transition-colors duration-150 ${
selectedValue === option.value
? "bg-logo-primary/20 text-logo-primary font-semibold"
? "bg-logo-primary/20 font-semibold"
: ""
}`}
onClick={() => handleSelect(option.value)}

View file

@ -0,0 +1,25 @@
import React from "react";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
variant?: "default" | "compact";
}
export const Input: React.FC<InputProps> = ({
className = "",
variant = "default",
...props
}) => {
const baseClasses = "px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded text-left flex items-center justify-between transition-all duration-150 hover:bg-logo-primary/10 cursor-pointer hover:border-logo-primary focus:outline-none focus:bg-logo-primary/20 focus:border-logo-primary";
const variantClasses = {
default: "px-3 py-2",
compact: "px-2 py-1"
};
return (
<input
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
{...props}
/>
);
};

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from "react";
import React, { useEffect, useRef, useState } from "react";
interface SettingContainerProps {
title: string;