style(ui): unify components with shared Input/Button
This commit is contained in:
parent
6f16fdf2f4
commit
1f96da9381
8 changed files with 98 additions and 23 deletions
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
:root {
|
||||
/* Typography */
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
38
src/components/ui/Button.tsx
Normal file
38
src/components/ui/Button.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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)}
|
||||
|
|
|
|||
25
src/components/ui/Input.tsx
Normal file
25
src/components/ui/Input.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface SettingContainerProps {
|
||||
title: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue