Merge pull request #17 from ualsweb/claude/bakery-settings-ux-redesign-017J8nkGyr5NagisnW1TRPSs
Claude/bakery settings ux redesign 017 j8nk gyr5 nagisn w1 trp ss
This commit is contained in:
180
frontend/src/components/ui/SettingRow/SettingRow.tsx
Normal file
180
frontend/src/components/ui/SettingRow/SettingRow.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import React from 'react';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
import { Toggle } from '../Toggle';
|
||||
import { Input } from '../Input';
|
||||
import { Select } from '../Select';
|
||||
import { Badge } from '../Badge';
|
||||
|
||||
export interface SettingRowProps {
|
||||
label: string;
|
||||
description?: string;
|
||||
helpText?: string;
|
||||
icon?: React.ReactNode;
|
||||
badge?: {
|
||||
text: string;
|
||||
variant?: 'default' | 'success' | 'warning' | 'danger' | 'info';
|
||||
};
|
||||
|
||||
// For toggle type
|
||||
type?: 'toggle' | 'input' | 'select' | 'custom';
|
||||
checked?: boolean;
|
||||
onToggle?: (checked: boolean) => void;
|
||||
|
||||
// For input type
|
||||
inputType?: 'text' | 'number' | 'email' | 'tel' | 'password';
|
||||
value?: string | number;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
placeholder?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
error?: string;
|
||||
|
||||
// For select type
|
||||
options?: Array<{ value: string; label: string }>;
|
||||
selectValue?: string;
|
||||
onSelectChange?: (value: string) => void;
|
||||
|
||||
// For custom content
|
||||
children?: React.ReactNode;
|
||||
|
||||
// Common props
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const SettingRow: React.FC<SettingRowProps> = ({
|
||||
label,
|
||||
description,
|
||||
helpText,
|
||||
icon,
|
||||
badge,
|
||||
type = 'toggle',
|
||||
checked,
|
||||
onToggle,
|
||||
inputType = 'text',
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
error,
|
||||
options = [],
|
||||
selectValue,
|
||||
onSelectChange,
|
||||
children,
|
||||
disabled = false,
|
||||
className = '',
|
||||
required = false,
|
||||
}) => {
|
||||
const renderControl = () => {
|
||||
switch (type) {
|
||||
case 'toggle':
|
||||
return (
|
||||
<Toggle
|
||||
checked={checked || false}
|
||||
onChange={onToggle || (() => {})}
|
||||
disabled={disabled}
|
||||
size="md"
|
||||
/>
|
||||
);
|
||||
|
||||
case 'input':
|
||||
return (
|
||||
<div className="w-full max-w-xs">
|
||||
<Input
|
||||
type={inputType}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
error={error}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<div className="w-full max-w-xs">
|
||||
<Select
|
||||
options={options}
|
||||
value={selectValue}
|
||||
onChange={onSelectChange}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'custom':
|
||||
return children;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
flex flex-col sm:flex-row sm:items-center sm:justify-between
|
||||
gap-3 sm:gap-6 py-4 px-4 sm:px-6
|
||||
border-b border-[var(--border-primary)] last:border-b-0
|
||||
hover:bg-[var(--bg-secondary)] transition-colors
|
||||
${className}
|
||||
`}>
|
||||
{/* Left side - Label and Description */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{icon && (
|
||||
<div className="flex-shrink-0 text-[var(--text-secondary)]">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label className="text-sm font-medium text-[var(--text-primary)] flex items-center gap-2">
|
||||
{label}
|
||||
{required && <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
|
||||
{badge && (
|
||||
<Badge variant={badge.variant || 'default'} size="sm">
|
||||
{badge.text}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{helpText && (
|
||||
<Tooltip content={helpText}>
|
||||
<HelpCircle className="w-4 h-4 text-[var(--text-tertiary)] cursor-help" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{description && (
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right side - Control */}
|
||||
<div className="flex-shrink-0">
|
||||
{renderControl()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingRow;
|
||||
2
frontend/src/components/ui/SettingRow/index.ts
Normal file
2
frontend/src/components/ui/SettingRow/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as SettingRow } from './SettingRow';
|
||||
export type { SettingRowProps } from './SettingRow';
|
||||
108
frontend/src/components/ui/SettingSection/SettingSection.tsx
Normal file
108
frontend/src/components/ui/SettingSection/SettingSection.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Card } from '../Card';
|
||||
import { Badge } from '../Badge';
|
||||
|
||||
export interface SettingSectionProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
badge?: {
|
||||
text: string;
|
||||
variant?: 'default' | 'success' | 'warning' | 'danger' | 'info';
|
||||
};
|
||||
collapsible?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
headerAction?: React.ReactNode;
|
||||
}
|
||||
|
||||
const SettingSection: React.FC<SettingSectionProps> = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
badge,
|
||||
collapsible = false,
|
||||
defaultOpen = true,
|
||||
children,
|
||||
className = '',
|
||||
headerAction,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (collapsible) {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={`overflow-hidden ${className}`}>
|
||||
{/* Header */}
|
||||
<div
|
||||
className={`
|
||||
px-4 sm:px-6 py-4
|
||||
${collapsible ? 'cursor-pointer hover:bg-[var(--bg-secondary)]' : ''}
|
||||
transition-colors
|
||||
`}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1 min-w-0">
|
||||
{icon && (
|
||||
<div className="flex-shrink-0 mt-0.5 text-[var(--color-primary)]">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-base sm:text-lg font-semibold text-[var(--text-primary)]">
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
{badge && (
|
||||
<Badge variant={badge.variant || 'default'} size="sm">
|
||||
{badge.text}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{collapsible && (
|
||||
<div className="ml-auto flex-shrink-0">
|
||||
{isOpen ? (
|
||||
<ChevronUp className="w-5 h-5 text-[var(--text-secondary)]" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-[var(--text-secondary)]" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{description && (
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-1">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{headerAction && !collapsible && (
|
||||
<div className="flex-shrink-0">
|
||||
{headerAction}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{(!collapsible || isOpen) && (
|
||||
<div className="border-t border-[var(--border-primary)]">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingSection;
|
||||
2
frontend/src/components/ui/SettingSection/index.ts
Normal file
2
frontend/src/components/ui/SettingSection/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as SettingSection } from './SettingSection';
|
||||
export type { SettingSectionProps } from './SettingSection';
|
||||
63
frontend/src/components/ui/SettingsSearch/SettingsSearch.tsx
Normal file
63
frontend/src/components/ui/SettingsSearch/SettingsSearch.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Search, X } from 'lucide-react';
|
||||
|
||||
export interface SettingsSearchProps {
|
||||
placeholder?: string;
|
||||
onSearch: (query: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SettingsSearch: React.FC<SettingsSearchProps> = ({
|
||||
placeholder = 'Search settings...',
|
||||
onSearch,
|
||||
className = '',
|
||||
}) => {
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const debounceTimer = setTimeout(() => {
|
||||
onSearch(query);
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(debounceTimer);
|
||||
}, [query, onSearch]);
|
||||
|
||||
const handleClear = () => {
|
||||
setQuery('');
|
||||
onSearch('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-[var(--text-tertiary)]" />
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full pl-10 pr-10 py-2.5 border border-[var(--border-primary)] rounded-lg bg-[var(--bg-primary)] text-[var(--text-primary)] placeholder-[var(--text-tertiary)] focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent transition-all"
|
||||
/>
|
||||
{query && (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] transition-colors"
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{query && (
|
||||
<div className="absolute top-full left-0 right-0 mt-2 p-2 bg-[var(--bg-secondary)] border border-[var(--border-primary)] rounded-lg shadow-lg z-10">
|
||||
<p className="text-xs text-[var(--text-secondary)]">
|
||||
Searching for: <span className="font-semibold text-[var(--text-primary)]">"{query}"</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsSearch;
|
||||
2
frontend/src/components/ui/SettingsSearch/index.ts
Normal file
2
frontend/src/components/ui/SettingsSearch/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as SettingsSearch } from './SettingsSearch';
|
||||
export type { SettingsSearchProps } from './SettingsSearch';
|
||||
@@ -40,6 +40,9 @@ export { TableOfContents } from './TableOfContents';
|
||||
export { SavingsCalculator } from './SavingsCalculator';
|
||||
export { StepTimeline } from './StepTimeline';
|
||||
export { FAQAccordion } from './FAQAccordion';
|
||||
export { SettingRow } from './SettingRow';
|
||||
export { SettingSection } from './SettingSection';
|
||||
export { SettingsSearch } from './SettingsSearch';
|
||||
|
||||
// Export types
|
||||
export type { ButtonProps } from './Button';
|
||||
@@ -77,4 +80,7 @@ export type { FloatingCTAProps } from './FloatingCTA';
|
||||
export type { TableOfContentsProps, TOCSection } from './TableOfContents';
|
||||
export type { SavingsCalculatorProps } from './SavingsCalculator';
|
||||
export type { StepTimelineProps, TimelineStep } from './StepTimeline';
|
||||
export type { FAQAccordionProps, FAQItem } from './FAQAccordion';
|
||||
export type { FAQAccordionProps, FAQItem } from './FAQAccordion';
|
||||
export type { SettingRowProps } from './SettingRow';
|
||||
export type { SettingSectionProps } from './SettingSection';
|
||||
export type { SettingsSearchProps } from './SettingsSearch';
|
||||
Reference in New Issue
Block a user