1
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
type EditorNoticeTone = 'muted' | 'warning';
|
||||
|
||||
const TONE_CLASS_NAMES: Record<EditorNoticeTone, string> = {
|
||||
muted: 'text-xs text-zinc-400',
|
||||
warning: 'text-xs text-amber-200/90',
|
||||
};
|
||||
|
||||
export function EditorNotice({
|
||||
message,
|
||||
tone = 'muted',
|
||||
}: {
|
||||
message: string | null;
|
||||
tone?: EditorNoticeTone;
|
||||
}) {
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={TONE_CLASS_NAMES[tone]}>{message}</div>;
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import { Save } from 'lucide-react';
|
||||
|
||||
import { EditorNotice } from './EditorNotice';
|
||||
|
||||
function safeNumber(value: number) {
|
||||
return Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
|
||||
function toNumber(value: string, fallback = 0) {
|
||||
const next = Number(value);
|
||||
return Number.isFinite(next) ? next : fallback;
|
||||
}
|
||||
|
||||
export type SelectFieldOption = {
|
||||
label: string;
|
||||
value: string | number;
|
||||
};
|
||||
|
||||
export function TextField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<label className="block">
|
||||
<div className="mb-1 text-xs font-medium text-zinc-300">{label}</div>
|
||||
<input
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm text-white outline-none transition focus:border-emerald-400/40 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function NumberField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
min,
|
||||
step = 1,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
min?: number;
|
||||
step?: number;
|
||||
}) {
|
||||
return (
|
||||
<label className="block">
|
||||
<div className="mb-1 text-xs font-medium text-zinc-300">{label}</div>
|
||||
<input
|
||||
type="number"
|
||||
value={safeNumber(value)}
|
||||
min={min}
|
||||
step={step}
|
||||
onChange={(event) => onChange(toNumber(event.target.value, value))}
|
||||
className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm text-white outline-none transition focus:border-emerald-400/40"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function TextAreaField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
rows = 4,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
rows?: number;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<label className="block">
|
||||
<div className="mb-1 text-xs font-medium text-zinc-300">{label}</div>
|
||||
<textarea
|
||||
rows={rows}
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm leading-relaxed text-white outline-none transition focus:border-emerald-400/40 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function SelectField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
disabled = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
onChange: (value: string) => void;
|
||||
options: SelectFieldOption[];
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<label className="block">
|
||||
<div className="mb-1 text-xs font-medium text-zinc-300">{label}</div>
|
||||
<select
|
||||
value={String(value)}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
disabled={disabled}
|
||||
className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm text-white outline-none transition focus:border-emerald-400/40 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={`${label}-${option.value}`} value={String(option.value)}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function SaveBar({
|
||||
saveLabel,
|
||||
onSave,
|
||||
isSaving,
|
||||
saveMessage,
|
||||
}: {
|
||||
saveLabel: string;
|
||||
onSave: () => void;
|
||||
isSaving: boolean;
|
||||
saveMessage: string | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-5 flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSave}
|
||||
disabled={isSaving}
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-emerald-500 px-4 py-2 text-sm font-medium text-black transition hover:bg-emerald-400 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
<span>{isSaving ? '保存中...' : saveLabel}</span>
|
||||
</button>
|
||||
<EditorNotice message={saveMessage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export function SectionCard({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
className = '',
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
className={`rounded-2xl border border-white/10 bg-black/20 p-5 ${className}`}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<div className="text-sm font-semibold text-white">{title}</div>
|
||||
{description && (
|
||||
<div className="mt-1 text-xs leading-relaxed text-zinc-400">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user