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 {
|
:root {
|
||||||
/* Typography */
|
/* Typography */
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useSettings } from "../../hooks/useSettings";
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
|
import { Input } from "../ui/Input";
|
||||||
|
import { Button } from "../ui/Button";
|
||||||
import { SettingContainer } from "../ui/SettingContainer";
|
import { SettingContainer } from "../ui/SettingContainer";
|
||||||
|
|
||||||
interface CorrectWordsProps {
|
interface CorrectWordsProps {
|
||||||
|
|
@ -44,39 +46,43 @@ export const CorrectWords: React.FC<CorrectWordsProps> = React.memo(({
|
||||||
grouped={grouped}
|
grouped={grouped}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
className="max-w-[128px]"
|
||||||
value={newWord}
|
value={newWord}
|
||||||
onChange={(e) => setNewWord(e.target.value)}
|
onChange={(e) => setNewWord(e.target.value)}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
placeholder="Add a word"
|
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")}
|
disabled={isUpdating("correct_words")}
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
onClick={handleAddWord}
|
onClick={handleAddWord}
|
||||||
disabled={!newWord.trim() || newWord.includes(' ') || newWord.trim().length > 50 || isUpdating("correct_words")}
|
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
|
Add
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
{correctWords.length > 0 && (
|
{correctWords.length > 0 && (
|
||||||
<div className={`px-4 p-2 ${grouped ? "" : "rounded-lg border border-mid-gray/20"} flex flex-wrap gap-1`}>
|
<div className={`px-4 p-2 ${grouped ? "" : "rounded-lg border border-mid-gray/20"} flex flex-wrap gap-1`}>
|
||||||
{correctWords.map((word) => (
|
{correctWords.map((word) => (
|
||||||
<button
|
<Button
|
||||||
key={word}
|
key={word}
|
||||||
onClick={() => handleRemoveWord(word)}
|
onClick={() => handleRemoveWord(word)}
|
||||||
disabled={isUpdating("correct_words")}
|
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}`}
|
aria-label={`Remove ${word}`}
|
||||||
>
|
>
|
||||||
<span>{word}</span>
|
<span>{word}</span>
|
||||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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>
|
</svg>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,13 @@ export const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = React.memo(
|
||||||
await resetSetting("selected_microphone");
|
await resetSetting("selected_microphone");
|
||||||
};
|
};
|
||||||
|
|
||||||
const microphoneOptions = audioDevices.map(device => ({
|
const microphoneOptions = [
|
||||||
value: device.name,
|
{ value: "Default", label: "Default" },
|
||||||
label: device.name
|
...audioDevices.map(device => ({
|
||||||
}));
|
value: device.name,
|
||||||
|
label: device.name
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingContainer
|
<SettingContainer
|
||||||
|
|
@ -50,7 +53,7 @@ export const MicrophoneSelector: React.FC<MicrophoneSelectorProps> = React.memo(
|
||||||
options={microphoneOptions}
|
options={microphoneOptions}
|
||||||
selectedValue={selectedMicrophone}
|
selectedValue={selectedMicrophone}
|
||||||
onSelect={handleMicrophoneSelect}
|
onSelect={handleMicrophoneSelect}
|
||||||
placeholder={isLoading ? "Loading..." : "Select microphone..."}
|
placeholder={isLoading ? "Loading..." : ""}
|
||||||
disabled={isUpdating("selected_microphone") || isLoading}
|
disabled={isUpdating("selected_microphone") || isLoading}
|
||||||
onRefresh={refreshAudioDevices}
|
onRefresh={refreshAudioDevices}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,13 @@ export const OutputDeviceSelector: React.FC<OutputDeviceSelectorProps> = React.m
|
||||||
await resetSetting("selected_output_device");
|
await resetSetting("selected_output_device");
|
||||||
};
|
};
|
||||||
|
|
||||||
const outputDeviceOptions = outputDevices.map(device => ({
|
const outputDeviceOptions = [
|
||||||
value: device.name,
|
{ value: "Default", label: "Default" },
|
||||||
label: device.name
|
...outputDevices.map(device => ({
|
||||||
}));
|
value: device.name,
|
||||||
|
label: device.name
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingContainer
|
<SettingContainer
|
||||||
|
|
@ -51,7 +54,7 @@ export const OutputDeviceSelector: React.FC<OutputDeviceSelectorProps> = React.m
|
||||||
options={outputDeviceOptions}
|
options={outputDeviceOptions}
|
||||||
selectedValue={selectedOutputDevice}
|
selectedValue={selectedOutputDevice}
|
||||||
onSelect={handleOutputDeviceSelect}
|
onSelect={handleOutputDeviceSelect}
|
||||||
placeholder={isLoading ? "Loading..." : "Select output device..."}
|
placeholder={isLoading ? "Loading..." : ""}
|
||||||
disabled={isUpdating("selected_output_device") || isLoading}
|
disabled={isUpdating("selected_output_device") || isLoading}
|
||||||
onRefresh={refreshOutputDevices}
|
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 {
|
export interface DropdownOption {
|
||||||
value: string;
|
value: string;
|
||||||
|
|
@ -95,7 +95,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
|
||||||
type="button"
|
type="button"
|
||||||
className={`w-full px-2 py-1 text-sm text-left hover:bg-logo-primary/10 transition-colors duration-150 ${
|
className={`w-full px-2 py-1 text-sm text-left hover:bg-logo-primary/10 transition-colors duration-150 ${
|
||||||
selectedValue === option.value
|
selectedValue === option.value
|
||||||
? "bg-logo-primary/20 text-logo-primary font-semibold"
|
? "bg-logo-primary/20 font-semibold"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleSelect(option.value)}
|
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 {
|
interface SettingContainerProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue