This commit is contained in:
2026-04-21 19:18:26 +08:00
parent 4372ab5be1
commit 48957311bc
78 changed files with 643 additions and 3801 deletions

View File

@@ -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>;
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}