summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/app.css256
-rw-r--r--app/root.tsx34
-rw-r--r--app/routes/artist-by-uuid.tsx75
-rw-r--r--app/routes/artist-edit.tsx71
-rw-r--r--app/routes/artist-history.tsx35
-rw-r--r--app/routes/artist-new.tsx66
-rw-r--r--app/routes/band-by-uuid.tsx77
-rw-r--r--app/routes/band-edit.tsx112
-rw-r--r--app/routes/band-history.tsx40
-rw-r--r--app/routes/band-new.tsx114
-rw-r--r--app/routes/home.tsx27
11 files changed, 460 insertions, 447 deletions
diff --git a/app/app.css b/app/app.css
index 99345d8..f7c2b1c 100644
--- a/app/app.css
+++ b/app/app.css
@@ -1,15 +1,251 @@
-@import "tailwindcss";
+*, *::before, *::after { box-sizing: border-box; }
-@theme {
- --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
- "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+body {
+ margin: 0;
+ background: #030712;
+ color: #e5e7eb;
+ font-family: ui-sans-serif, system-ui, sans-serif;
+ line-height: 1.5;
+ -webkit-font-smoothing: antialiased;
}
-html,
-body {
- @apply bg-white dark:bg-gray-950;
+a { color: inherit; text-decoration: none; }
+p { margin: 0; }
+h1, h2 { margin: 0; }
+ol, ul { list-style: none; padding: 0; margin: 0; }
+hr { border: none; border-top: 1px solid #1f2937; margin: 1.5rem 0; }
+section { margin-bottom: 1.5rem; }
+pre { background: #111827; border-radius: 6px; padding: 1rem; overflow-x: auto; font-size: .875rem; margin-top: 1rem; }
+
+/* ── Nav ── */
+
+nav { border-bottom: 1px solid #1f2937; }
+nav > div {
+ max-width: 48rem;
+ margin: 0 auto;
+ padding: .75rem 1rem;
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+}
+nav a { font-size: .875rem; color: #9ca3af; }
+nav a:hover { color: #f9fafb; }
+nav .logo { font-size: 1rem; font-weight: 700; color: #fff; letter-spacing: -.025em; }
+
+/* ── Main ── */
+
+main {
+ max-width: 48rem;
+ margin: 0 auto;
+ padding: 2rem 1rem;
+}
+
+h1 { font-size: 1.5rem; font-weight: 700; }
+h2 {
+ font-size: .6875rem;
+ font-weight: 600;
+ color: #6b7280;
+ text-transform: uppercase;
+ letter-spacing: .06em;
+ margin-bottom: .75rem;
+}
+
+/* ── Page header (← Title) ── */
+
+.page-header {
+ display: flex;
+ align-items: center;
+ gap: .75rem;
+ margin-bottom: 1.5rem;
+}
+.page-header h1 { font-size: 1.25rem; font-weight: 600; }
+.back { color: #6b7280; }
+.back:hover { color: #e5e7eb; }
+
+/* ── Detail header ── */
+
+.detail-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+}
+.detail-info { flex: 1; min-width: 0; }
+.detail-subtitle { font-size: .875rem; color: #9ca3af; display: flex; gap: .75rem; margin-top: .25rem; }
+.detail-desc { font-size: .875rem; color: #d1d5db; line-height: 1.6; margin-top: .75rem; }
+.detail-actions { display: flex; align-items: center; gap: .75rem; flex-shrink: 0; }
+.detail-actions .history { font-size: .875rem; color: #9ca3af; }
+.detail-actions .history:hover { color: #e5e7eb; }
+.detail-actions .edit {
+ font-size: .875rem;
+ background: #1f2937;
+ color: #e5e7eb;
+ padding: .375rem .75rem;
+ border-radius: 4px;
+}
+.detail-actions .edit:hover { background: #374151; }
+
+/* ── Home band list ── */
+
+.band-list li {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 1.5rem;
+ padding: 1rem 0;
+ border-bottom: 1px solid #0f172a;
+}
+.band-list li:last-child { border-bottom: none; }
+.band-list .name-col { min-width: 0; }
+.band-list .name-col a { font-weight: 500; }
+.band-list .name-col a:hover { color: #fff; }
+.band-list .desc {
+ font-size: .875rem;
+ color: #6b7280;
+ margin-top: .125rem;
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 1;
+}
+.band-list .info { font-size: .875rem; color: #6b7280; flex-shrink: 0; display: flex; gap: 1rem; padding-top: .125rem; }
+
+/* ── Detail lists ── */
+
+.member-list { display: flex; flex-direction: column; gap: .5rem; }
+.member-list li { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
+.member-list a { color: #60a5fa; font-weight: 500; }
+.member-list a:hover { color: #93c5fd; }
- @media (prefers-color-scheme: dark) {
- color-scheme: dark;
- }
+.link-list { display: flex; flex-direction: column; gap: .375rem; }
+.link-list a { font-size: .875rem; color: #60a5fa; }
+.link-list a:hover { color: #93c5fd; }
+
+/* ── Badge ── */
+
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: .25rem;
+ background: #111827;
+ color: #9ca3af;
+ font-size: .75rem;
+ padding: .125rem .5rem;
+ border-radius: 3px;
+}
+.badge button { all: unset; cursor: pointer; color: #6b7280; line-height: 1; }
+.badge button:hover { color: #f87171; }
+
+/* ── Meta footer ── */
+
+.meta {
+ font-family: ui-monospace, monospace;
+ font-size: .75rem;
+ color: #374151;
+ display: flex;
+ flex-direction: column;
+ gap: .25rem;
+}
+.meta a:hover { color: #9ca3af; }
+.meta .updated { font-family: ui-sans-serif, system-ui, sans-serif; color: #6b7280; margin-top: .5rem; }
+
+/* ── Forms ── */
+
+form { display: flex; flex-direction: column; gap: 1.25rem; }
+label { display: block; font-size: .875rem; color: #d1d5db; margin-bottom: .25rem; }
+.req { color: #f87171; }
+.muted { font-size: .875rem; color: #6b7280; }
+.muted a { color: #60a5fa; }
+.muted a:hover { color: #93c5fd; }
+
+input, textarea {
+ display: block;
+ width: 100%;
+ background: #1f2937;
+ border: 1px solid #374151;
+ border-radius: 4px;
+ padding: .5rem .75rem;
+ color: #f3f4f6;
+ font: inherit;
+ font-size: .875rem;
}
+input[type="hidden"] { display: none; }
+select {
+ background: #1f2937;
+ border: 1px solid #374151;
+ border-radius: 4px;
+ padding: .5rem .75rem;
+ color: #f3f4f6;
+ font: inherit;
+ font-size: .875rem;
+ width: auto;
+}
+input:focus, textarea:focus, select:focus { outline: none; border-color: #3b82f6; }
+textarea { resize: vertical; }
+input.mono { font-family: ui-monospace, monospace; }
+
+/* ── Buttons ── */
+
+button {
+ display: inline-flex;
+ align-items: center;
+ padding: .5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ font: inherit;
+ font-size: .875rem;
+ cursor: pointer;
+ background: #1f2937;
+ color: #d1d5db;
+}
+button:hover { background: #374151; }
+button[type="submit"] { background: #2563eb; color: #fff; font-weight: 500; }
+button[type="submit"]:hover { background: #3b82f6; }
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ padding: .5rem 1rem;
+ border-radius: 4px;
+ font-size: .875rem;
+ background: #1f2937;
+ color: #d1d5db;
+}
+.btn:hover { background: #374151; }
+
+.btn-text { background: none; padding: 0; color: #60a5fa; font-size: .875rem; }
+.btn-text:hover { background: none; color: #93c5fd; }
+
+.btn-icon { background: none; padding: .125rem .5rem; color: #6b7280; }
+.btn-icon:hover { background: none; color: #f87171; }
+
+/* ── Form components ── */
+
+.actions { display: flex; gap: .75rem; padding-top: .5rem; }
+.error { color: #f87171; font-size: .875rem; margin-top: .25rem; }
+
+.links-form { display: flex; flex-direction: column; gap: .5rem; }
+.link-row { display: flex; gap: .5rem; align-items: center; }
+.link-row input { width: auto; flex: 1; min-width: 0; }
+.link-row .label-input { flex: 0 0 7rem; }
+.link-row select { width: 9rem; flex-shrink: 0; }
+
+.members-form { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .75rem; }
+.member-card { background: #111827; border-radius: 6px; padding: .75rem; }
+.member-card .card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .5rem; }
+.member-card .card-name { font-size: .875rem; font-weight: 500; color: #e5e7eb; }
+.member-card .badges { display: flex; flex-wrap: wrap; gap: .375rem; margin-bottom: .5rem; }
+.member-card .role-row { display: flex; gap: .5rem; align-items: center; }
+.member-card .role-row .custom-input { width: 8rem; flex: none; }
+
+/* ── Revision list ── */
+
+.rev-list { display: flex; flex-direction: column; gap: 1rem; }
+.rev { background: #111827; border-radius: 6px; padding: 1rem; }
+.rev-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; }
+.rev-main { flex: 1; min-width: 0; }
+.rev-message { font-weight: 500; color: #f3f4f6; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.rev-time { font-size: .75rem; color: #6b7280; margin-top: .25rem; }
+.rev-latest { font-size: .75rem; color: #60a5fa; flex-shrink: 0; }
+.rev-snap { font-size: .75rem; color: #9ca3af; margin-top: .75rem; display: flex; flex-direction: column; gap: .125rem; }
diff --git a/app/root.tsx b/app/root.tsx
index 5d66876..26bfb48 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -26,7 +26,7 @@ export const links: Route.LinksFunction = () => [
export function Layout({ children }: { children: React.ReactNode }) {
return (
- <html lang="ja" className="dark">
+ <html lang="ja">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -34,7 +34,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
<Meta />
<Links />
</head>
- <body className="bg-gray-950 text-gray-100 antialiased">
+ <body>
{children}
<ScrollRestoration />
<Scripts />
@@ -46,23 +46,11 @@ export function Layout({ children }: { children: React.ReactNode }) {
export default function App() {
return (
<>
- <nav className="border-b border-gray-800">
- <div className="max-w-3xl mx-auto px-4 py-3 flex items-center gap-6">
- <Link to="/" className="font-bold text-white tracking-tight">
- whois.band
- </Link>
- <Link
- to="/bands/new"
- className="text-sm text-gray-400 hover:text-white transition-colors"
- >
- + Band
- </Link>
- <Link
- to="/artists/new"
- className="text-sm text-gray-400 hover:text-white transition-colors"
- >
- + Artist
- </Link>
+ <nav>
+ <div>
+ <Link to="/" className="logo">whois.band</Link>
+ <Link to="/bands/new">+ Band</Link>
+ <Link to="/artists/new">+ Artist</Link>
</div>
</nav>
<Outlet />
@@ -87,11 +75,11 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
}
return (
- <main className="max-w-3xl mx-auto px-4 py-16">
- <h1 className="text-2xl font-bold">{message}</h1>
- <p className="mt-2 text-gray-400">{details}</p>
+ <main>
+ <h1>{message}</h1>
+ <p className="muted" style={{ marginTop: ".5rem" }}>{details}</p>
{stack && (
- <pre className="mt-4 w-full p-4 overflow-x-auto bg-gray-900 rounded text-sm">
+ <pre>
<code>{stack}</code>
</pre>
)}
diff --git a/app/routes/artist-by-uuid.tsx b/app/routes/artist-by-uuid.tsx
index 9b8a4b1..6eb06a7 100644
--- a/app/routes/artist-by-uuid.tsx
+++ b/app/routes/artist-by-uuid.tsx
@@ -14,42 +14,23 @@ export async function loader({ params }: LoaderFunctionArgs) {
export default function ArtistDetail() {
const { artist, links, bands, latest } = useLoaderData<typeof loader>();
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-start justify-between mb-6">
- <h1 className="text-2xl font-bold">{artist.name}</h1>
- <div className="flex items-center gap-3 text-sm shrink-0 ml-4">
- <Link
- to={`/artists/of/${artist.id}/history`}
- className="text-gray-400 hover:text-gray-200 transition-colors"
- >
- 履歴
- </Link>
- <Link
- to={`/artists/of/${artist.id}/edit`}
- className="bg-gray-800 hover:bg-gray-700 text-gray-200 px-3 py-1.5 rounded transition-colors"
- >
- 編集
- </Link>
+ <main>
+ <div className="detail-header">
+ <h1>{artist.name}</h1>
+ <div className="detail-actions">
+ <Link to={`/artists/of/${artist.id}/history`} className="history">履歴</Link>
+ <Link to={`/artists/of/${artist.id}/edit`} className="edit">編集</Link>
</div>
</div>
{bands.length > 0 && (
- <section className="mb-6">
- <h2 className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">
- バンド
- </h2>
- <ul className="space-y-2">
+ <section>
+ <h2>バンド</h2>
+ <ul className="member-list">
{bands.map((b) => (
- <li key={b.band_id} className="flex items-center gap-3">
- <Link
- to={`/bands/of/${b.band_id}`}
- className="text-blue-400 hover:text-blue-300 transition-colors font-medium"
- >
- {b.band_name}
- </Link>
- {b.role && (
- <span className="text-gray-400 text-sm">{b.role}</span>
- )}
+ <li key={b.band_id}>
+ <Link to={`/bands/of/${b.band_id}`}>{b.band_name}</Link>
+ {b.role && <span className="muted">{b.role}</span>}
</li>
))}
</ul>
@@ -57,42 +38,26 @@ export default function ArtistDetail() {
)}
{links.length > 0 && (
- <section className="mb-6">
- <h2 className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">
- リンク
- </h2>
- <ul className="space-y-1.5">
+ <section>
+ <h2>リンク</h2>
+ <ul className="link-list">
{links.map((l) => (
<li key={l.id}>
- <a
- href={l.url}
- target="_blank"
- rel="noopener noreferrer"
- className="text-blue-400 hover:text-blue-300 transition-colors text-sm"
- >
- {l.label}
- </a>
+ <a href={l.url} target="_blank" rel="noopener noreferrer">{l.label}</a>
</li>
))}
</ul>
</section>
)}
- <hr className="border-gray-800 my-6" />
- <div className="text-xs text-gray-600 space-y-1 font-mono">
+ <hr />
+ <div className="meta">
<p>/artists/of/{artist.id}</p>
<p>
- <Link
- to={`/artists/named/${artist.slug}`}
- className="hover:text-gray-400 transition-colors"
- >
- /artists/named/{artist.slug}
- </Link>
+ <Link to={`/artists/named/${artist.slug}`}>/artists/named/{artist.slug}</Link>
</p>
{latest && (
- <p className="font-sans text-gray-500 mt-2">
- 最終更新: {latest.created_at} — {latest.message}
- </p>
+ <p className="updated">最終更新: {latest.created_at} — {latest.message}</p>
)}
</div>
</main>
diff --git a/app/routes/artist-edit.tsx b/app/routes/artist-edit.tsx
index f2e5c18..e3a49af 100644
--- a/app/routes/artist-edit.tsx
+++ b/app/routes/artist-edit.tsx
@@ -54,24 +54,17 @@ export default function ArtistEdit() {
const [links, setLinks] = useState(initLinks.map((l) => ({ label: l.label, url: l.url })));
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-center gap-3 mb-6">
- <Link
- to={`/artists/of/${artist.id}`}
- className="text-gray-400 hover:text-gray-200 transition-colors"
- >
- ←
- </Link>
- <h1 className="text-xl font-semibold">Edit Artist</h1>
+ <main>
+ <div className="page-header">
+ <Link to={`/artists/of/${artist.id}`} className="back">←</Link>
+ <h1>Edit Artist</h1>
</div>
- <Form method="post" className="space-y-5">
+ <Form method="post">
<input type="hidden" name="links" value={JSON.stringify(links)} />
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- 名前 <span className="text-red-400">*</span>
- </label>
+ <label>名前 <span className="req">*</span></label>
<input
name="name"
value={name}
@@ -79,45 +72,41 @@ export default function ArtistEdit() {
setName(e.target.value);
if (!slugManual) setSlug(toSlug(e.target.value));
}}
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
/>
- {errors.name && <p className="text-red-400 text-sm mt-1">{errors.name}</p>}
+ {errors.name && <p className="error">{errors.name}</p>}
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- Slug <span className="text-red-400">*</span>
- </label>
+ <label>Slug <span className="req">*</span></label>
<input
name="slug"
value={slug}
onChange={(e) => { setSlugManual(true); setSlug(e.target.value); }}
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 font-mono text-sm"
+ className="mono"
/>
- {errors.slug && <p className="text-red-400 text-sm mt-1">{errors.slug}</p>}
+ {errors.slug && <p className="error">{errors.slug}</p>}
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-2">リンク</label>
- <div className="space-y-2">
+ <label>リンク</label>
+ <div className="links-form">
{links.map((link, i) => (
- <div key={i} className="flex gap-2">
+ <div key={i} className="link-row">
<input
+ className="label-input"
value={link.label}
onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, label: e.target.value } : l))}
placeholder="ラベル (例: X)"
- className="w-28 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm"
/>
<input
value={link.url}
onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, url: e.target.value } : l))}
placeholder="https://..."
- className="flex-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm"
/>
<button
type="button"
+ className="btn-icon"
onClick={() => setLinks(links.filter((_, idx) => idx !== i))}
- className="text-gray-500 hover:text-red-400 px-2 transition-colors"
>
×
</button>
@@ -126,38 +115,22 @@ export default function ArtistEdit() {
</div>
<button
type="button"
+ className="btn-text"
onClick={() => setLinks([...links, { label: "", url: "" }])}
- className="mt-2 text-blue-400 hover:text-blue-300 text-sm transition-colors"
>
+ リンクを追加
</button>
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- 更新メッセージ <span className="text-red-400">*</span>
- </label>
- <input
- name="message"
- placeholder="例: SNSリンク追加"
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
- />
- {errors.message && <p className="text-red-400 text-sm mt-1">{errors.message}</p>}
+ <label>更新メッセージ <span className="req">*</span></label>
+ <input name="message" placeholder="例: SNSリンク追加" />
+ {errors.message && <p className="error">{errors.message}</p>}
</div>
- <div className="flex gap-3 pt-2">
- <button
- type="submit"
- className="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded font-medium transition-colors"
- >
- 保存
- </button>
- <Link
- to={`/artists/of/${artist.id}`}
- className="bg-gray-800 hover:bg-gray-700 text-gray-300 px-4 py-2 rounded transition-colors"
- >
- キャンセル
- </Link>
+ <div className="actions">
+ <button type="submit">保存</button>
+ <Link to={`/artists/of/${artist.id}`} className="btn">キャンセル</Link>
</div>
</Form>
</main>
diff --git a/app/routes/artist-history.tsx b/app/routes/artist-history.tsx
index c2fb4cb..2808927 100644
--- a/app/routes/artist-history.tsx
+++ b/app/routes/artist-history.tsx
@@ -12,38 +12,29 @@ export async function loader({ params }: LoaderFunctionArgs) {
export default function ArtistHistory() {
const { artist, revisions } = useLoaderData<typeof loader>();
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-center gap-3 mb-6">
- <Link
- to={`/artists/of/${artist.id}`}
- className="text-gray-400 hover:text-gray-200 transition-colors"
- >
- ←
- </Link>
- <h1 className="text-xl font-semibold">{artist.name} — 編集履歴</h1>
+ <main>
+ <div className="page-header">
+ <Link to={`/artists/of/${artist.id}`} className="back">←</Link>
+ <h1>{artist.name} — 編集履歴</h1>
</div>
{revisions.length === 0 ? (
- <p className="text-gray-400">履歴がありません。</p>
+ <p className="muted">履歴がありません。</p>
) : (
- <ol className="space-y-4">
+ <ol className="rev-list">
{revisions.map((rev, i) => {
let snap: { name?: string; links?: unknown[] } = {};
try { snap = JSON.parse(rev.snapshot); } catch { /* ignore */ }
return (
- <li key={rev.id} className="bg-gray-900 rounded-lg p-4">
- <div className="flex items-start justify-between gap-4">
- <div className="flex-1 min-w-0">
- <p className="font-medium text-gray-100 truncate">{rev.message}</p>
- <p className="text-xs text-gray-500 mt-1">
- {rev.created_at} · {rev.ip_address}
- </p>
+ <li key={rev.id} className="rev">
+ <div className="rev-header">
+ <div className="rev-main">
+ <p className="rev-message">{rev.message}</p>
+ <p className="rev-time">{rev.created_at} · {rev.ip_address}</p>
</div>
- {i === 0 && (
- <span className="text-xs text-blue-400 shrink-0">最新</span>
- )}
+ {i === 0 && <span className="rev-latest">最新</span>}
</div>
- <div className="mt-3 text-xs text-gray-400 space-y-0.5">
+ <div className="rev-snap">
<p>名前: {snap.name ?? "—"}</p>
<p>リンク: {snap.links?.length ?? 0}件</p>
</div>
diff --git a/app/routes/artist-new.tsx b/app/routes/artist-new.tsx
index 168a7cc..ce91dab 100644
--- a/app/routes/artist-new.tsx
+++ b/app/routes/artist-new.tsx
@@ -44,19 +44,17 @@ export default function ArtistNew() {
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-center gap-3 mb-6">
- <Link to="/" className="text-gray-400 hover:text-gray-200 transition-colors">←</Link>
- <h1 className="text-xl font-semibold">New Artist</h1>
+ <main>
+ <div className="page-header">
+ <Link to="/" className="back">←</Link>
+ <h1>New Artist</h1>
</div>
- <Form method="post" className="space-y-5">
+ <Form method="post">
<input type="hidden" name="links" value={JSON.stringify(links)} />
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- 名前 <span className="text-red-400">*</span>
- </label>
+ <label>名前 <span className="req">*</span></label>
<input
name="name"
value={name}
@@ -64,45 +62,41 @@ export default function ArtistNew() {
setName(e.target.value);
if (!slugManual) setSlug(toSlug(e.target.value));
}}
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
/>
- {errors.name && <p className="text-red-400 text-sm mt-1">{errors.name}</p>}
+ {errors.name && <p className="error">{errors.name}</p>}
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- Slug <span className="text-red-400">*</span>
- </label>
+ <label>Slug <span className="req">*</span></label>
<input
name="slug"
value={slug}
onChange={(e) => { setSlugManual(true); setSlug(e.target.value); }}
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 font-mono text-sm"
+ className="mono"
/>
- {errors.slug && <p className="text-red-400 text-sm mt-1">{errors.slug}</p>}
+ {errors.slug && <p className="error">{errors.slug}</p>}
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-2">リンク</label>
- <div className="space-y-2">
+ <label>リンク</label>
+ <div className="links-form">
{links.map((link, i) => (
- <div key={i} className="flex gap-2">
+ <div key={i} className="link-row">
<input
+ className="label-input"
value={link.label}
onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, label: e.target.value } : l))}
placeholder="ラベル (例: X)"
- className="w-28 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm"
/>
<input
value={link.url}
onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, url: e.target.value } : l))}
placeholder="https://..."
- className="flex-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm"
/>
<button
type="button"
+ className="btn-icon"
onClick={() => setLinks(links.filter((_, idx) => idx !== i))}
- className="text-gray-500 hover:text-red-400 px-2 transition-colors"
>
×
</button>
@@ -111,38 +105,22 @@ export default function ArtistNew() {
</div>
<button
type="button"
+ className="btn-text"
onClick={() => setLinks([...links, { label: "", url: "" }])}
- className="mt-2 text-blue-400 hover:text-blue-300 text-sm transition-colors"
>
+ リンクを追加
</button>
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- 更新メッセージ <span className="text-red-400">*</span>
- </label>
- <input
- name="message"
- placeholder="例: 初回登録"
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
- />
- {errors.message && <p className="text-red-400 text-sm mt-1">{errors.message}</p>}
+ <label>更新メッセージ <span className="req">*</span></label>
+ <input name="message" placeholder="例: 初回登録" />
+ {errors.message && <p className="error">{errors.message}</p>}
</div>
- <div className="flex gap-3 pt-2">
- <button
- type="submit"
- className="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded font-medium transition-colors"
- >
- 作成
- </button>
- <Link
- to="/"
- className="bg-gray-800 hover:bg-gray-700 text-gray-300 px-4 py-2 rounded transition-colors"
- >
- キャンセル
- </Link>
+ <div className="actions">
+ <button type="submit">作成</button>
+ <Link to="/" className="btn">キャンセル</Link>
</div>
</Form>
</main>
diff --git a/app/routes/band-by-uuid.tsx b/app/routes/band-by-uuid.tsx
index 9cb4d33..99a8b5c 100644
--- a/app/routes/band-by-uuid.tsx
+++ b/app/routes/band-by-uuid.tsx
@@ -26,50 +26,33 @@ const STATUS_LABEL: Record<string, string> = {
export default function BandDetail() {
const { band, links, artists, latest } = useLoaderData<typeof loader>();
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-start justify-between mb-6">
- <div>
- <h1 className="text-2xl font-bold">{band.name}</h1>
- <div className="flex items-center gap-3 mt-1 text-sm text-gray-400">
+ <main>
+ <div className="detail-header">
+ <div className="detail-info">
+ <h1>{band.name}</h1>
+ <div className="detail-subtitle">
{band.area && <span>{band.area}</span>}
<span>{STATUS_LABEL[band.status] ?? band.status}</span>
</div>
{band.description && (
- <p className="mt-3 text-gray-300 text-sm leading-relaxed">{band.description}</p>
+ <p className="detail-desc">{band.description}</p>
)}
</div>
- <div className="flex items-center gap-3 text-sm shrink-0 ml-4">
- <Link
- to={`/bands/of/${band.id}/history`}
- className="text-gray-400 hover:text-gray-200 transition-colors"
- >
- 履歴
- </Link>
- <Link
- to={`/bands/of/${band.id}/edit`}
- className="bg-gray-800 hover:bg-gray-700 text-gray-200 px-3 py-1.5 rounded transition-colors"
- >
- 編集
- </Link>
+ <div className="detail-actions">
+ <Link to={`/bands/of/${band.id}/history`} className="history">履歴</Link>
+ <Link to={`/bands/of/${band.id}/edit`} className="edit">編集</Link>
</div>
</div>
{artists.length > 0 && (
- <section className="mb-6">
- <h2 className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">
- メンバー
- </h2>
- <ul className="space-y-2">
+ <section>
+ <h2>メンバー</h2>
+ <ul className="member-list">
{artists.map((a) => (
- <li key={a.artist_id} className="flex items-center gap-3 flex-wrap">
- <Link
- to={`/artists/of/${a.artist_id}`}
- className="text-blue-400 hover:text-blue-300 transition-colors font-medium"
- >
- {a.artist_name}
- </Link>
+ <li key={a.artist_id}>
+ <Link to={`/artists/of/${a.artist_id}`}>{a.artist_name}</Link>
{a.role && a.role.split(", ").filter(Boolean).map((r, i) => (
- <span key={i} className="text-gray-500 text-xs bg-gray-900 px-1.5 py-0.5 rounded">{r}</span>
+ <span key={i} className="badge">{r}</span>
))}
</li>
))}
@@ -78,19 +61,12 @@ export default function BandDetail() {
)}
{links.length > 0 && (
- <section className="mb-6">
- <h2 className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">
- リンク
- </h2>
- <ul className="space-y-1.5">
+ <section>
+ <h2>リンク</h2>
+ <ul className="link-list">
{links.map((l) => (
<li key={l.id}>
- <a
- href={l.url}
- target="_blank"
- rel="noopener noreferrer"
- className="text-blue-400 hover:text-blue-300 transition-colors text-sm"
- >
+ <a href={l.url} target="_blank" rel="noopener noreferrer">
{LINK_TYPE_LABEL[l.label] ?? l.label}
</a>
</li>
@@ -99,21 +75,14 @@ export default function BandDetail() {
</section>
)}
- <hr className="border-gray-800 my-6" />
- <div className="text-xs text-gray-600 space-y-1 font-mono">
+ <hr />
+ <div className="meta">
<p>/bands/of/{band.id}</p>
<p>
- <Link
- to={`/bands/named/${band.slug}`}
- className="hover:text-gray-400 transition-colors"
- >
- /bands/named/{band.slug}
- </Link>
+ <Link to={`/bands/named/${band.slug}`}>/bands/named/{band.slug}</Link>
</p>
{latest && (
- <p className="font-sans text-gray-500 mt-2">
- 最終更新: {latest.created_at} — {latest.message}
- </p>
+ <p className="updated">最終更新: {latest.created_at} — {latest.message}</p>
)}
</div>
</main>
diff --git a/app/routes/band-edit.tsx b/app/routes/band-edit.tsx
index 0a98bd6..2e29277 100644
--- a/app/routes/band-edit.tsx
+++ b/app/routes/band-edit.tsx
@@ -97,13 +97,13 @@ export default function BandEdit() {
}
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-center gap-3 mb-6">
- <Link to={`/bands/of/${band.id}`} className="text-gray-400 hover:text-gray-200 transition-colors">←</Link>
- <h1 className="text-xl font-semibold">Edit Band</h1>
+ <main>
+ <div className="page-header">
+ <Link to={`/bands/of/${band.id}`} className="back">←</Link>
+ <h1>Edit Band</h1>
</div>
- <Form method="post" className="space-y-5">
+ <Form method="post">
<input type="hidden" name="links" value={JSON.stringify(links)} />
<input
type="hidden"
@@ -112,47 +112,34 @@ export default function BandEdit() {
/>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- バンド名 <span className="text-red-400">*</span>
- </label>
+ <label>バンド名 <span className="req">*</span></label>
<input
name="name"
value={name}
onChange={(e) => { setName(e.target.value); if (!slugManual) setSlug(toSlug(e.target.value)); }}
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
/>
- {errors.name && <p className="text-red-400 text-sm mt-1">{errors.name}</p>}
+ {errors.name && <p className="error">{errors.name}</p>}
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- Slug <span className="text-red-400">*</span>
- </label>
+ <label>Slug <span className="req">*</span></label>
<input
name="slug"
value={slug}
onChange={(e) => { setSlugManual(true); setSlug(e.target.value); }}
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 font-mono text-sm"
+ className="mono"
/>
- {errors.slug && <p className="text-red-400 text-sm mt-1">{errors.slug}</p>}
+ {errors.slug && <p className="error">{errors.slug}</p>}
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">活動拠点</label>
- <input
- name="area"
- defaultValue={band.area ?? ""}
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
- />
+ <label>活動拠点</label>
+ <input name="area" defaultValue={band.area ?? ""} />
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">ステータス</label>
- <select
- name="status"
- defaultValue={band.status}
- className="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
- >
+ <label>ステータス</label>
+ <select name="status" defaultValue={band.status}>
<option value="active">活動中</option>
<option value="hiatus">活動休止</option>
<option value="disbanded">解散</option>
@@ -160,24 +147,18 @@ export default function BandEdit() {
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">説明</label>
- <textarea
- name="description"
- rows={3}
- defaultValue={band.description ?? ""}
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 resize-none"
- />
+ <label>説明</label>
+ <textarea name="description" rows={3} defaultValue={band.description ?? ""} />
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-2">リンク</label>
- <div className="space-y-2">
+ <label>リンク</label>
+ <div className="links-form">
{links.map((link, i) => (
- <div key={i} className="flex gap-2">
+ <div key={i} className="link-row">
<select
value={link.label}
onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, label: e.target.value } : l))}
- className="w-36 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm"
>
{LINK_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
</select>
@@ -185,73 +166,65 @@ export default function BandEdit() {
value={link.url}
onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, url: e.target.value } : l))}
placeholder="https://..."
- className="flex-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm"
/>
- <button type="button" onClick={() => setLinks(links.filter((_, idx) => idx !== i))} className="text-gray-500 hover:text-red-400 px-2 transition-colors">×</button>
+ <button type="button" className="btn-icon" onClick={() => setLinks(links.filter((_, idx) => idx !== i))}>×</button>
</div>
))}
</div>
<button
type="button"
+ className="btn-text"
onClick={() => setLinks([...links, { label: LINK_TYPES[0].value, url: "" }])}
- className="mt-2 text-blue-400 hover:text-blue-300 text-sm transition-colors"
>
+ リンクを追加
</button>
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-2">メンバー</label>
+ <label>メンバー</label>
{picked.length > 0 && (
- <div className="space-y-2 mb-3">
+ <div className="members-form">
{picked.map((p) => {
const pend = pending[p.id] ?? DEFAULT_PENDING;
return (
- <div key={p.id} className="bg-gray-900 rounded-lg p-3">
- <div className="flex items-center justify-between mb-2">
- <span className="text-gray-200 text-sm font-medium">{p.name}</span>
+ <div key={p.id} className="member-card">
+ <div className="card-header">
+ <span className="card-name">{p.name}</span>
<button
type="button"
+ className="btn-icon"
onClick={() => setPicked(picked.filter((a) => a.id !== p.id))}
- className="text-gray-500 hover:text-red-400 text-xs transition-colors"
>
削除
</button>
</div>
{p.roles.length > 0 && (
- <div className="flex flex-wrap gap-1.5 mb-2">
+ <div className="badges">
{p.roles.map((r, ri) => (
- <span key={ri} className="inline-flex items-center gap-1 bg-gray-800 text-gray-300 text-xs px-2 py-0.5 rounded">
+ <span key={ri} className="badge">
{r}
- <button type="button" onClick={() => removeRole(p.id, ri)} className="text-gray-500 hover:text-red-400 leading-none">×</button>
+ <button type="button" onClick={() => removeRole(p.id, ri)}>×</button>
</span>
))}
</div>
)}
- <div className="flex gap-2 items-center">
+ <div className="role-row">
<select
value={pend.type}
onChange={(e) => setPending({ ...pending, [p.id]: { ...pend, type: e.target.value } })}
- className="bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-gray-300 text-sm focus:outline-none focus:border-blue-500"
>
{ARTIST_ROLES.map((r) => <option key={r} value={r}>{r}</option>)}
<option value="other">その他...</option>
</select>
{pend.type === "other" && (
<input
+ className="custom-input"
value={pend.custom}
onChange={(e) => setPending({ ...pending, [p.id]: { ...pend, custom: e.target.value } })}
placeholder="ロール名"
- className="bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-gray-300 text-sm focus:outline-none focus:border-blue-500 w-32"
/>
)}
- <button
- type="button"
- onClick={() => addRole(p.id)}
- className="text-blue-400 hover:text-blue-300 text-sm transition-colors"
- >
- + 追加
- </button>
+ <button type="button" className="btn-text" onClick={() => addRole(p.id)}>+ 追加</button>
</div>
</div>
);
@@ -269,7 +242,6 @@ export default function BandEdit() {
}
}}
defaultValue=""
- className="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-400 focus:outline-none focus:border-blue-500 text-sm"
>
<option value="">+ アーティストを追加...</option>
{available.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
@@ -278,20 +250,14 @@ export default function BandEdit() {
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- 更新メッセージ <span className="text-red-400">*</span>
- </label>
- <input
- name="message"
- placeholder="例: メンバー追加"
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
- />
- {errors.message && <p className="text-red-400 text-sm mt-1">{errors.message}</p>}
+ <label>更新メッセージ <span className="req">*</span></label>
+ <input name="message" placeholder="例: メンバー追加" />
+ {errors.message && <p className="error">{errors.message}</p>}
</div>
- <div className="flex gap-3 pt-2">
- <button type="submit" className="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded font-medium transition-colors">保存</button>
- <Link to={`/bands/of/${band.id}`} className="bg-gray-800 hover:bg-gray-700 text-gray-300 px-4 py-2 rounded transition-colors">キャンセル</Link>
+ <div className="actions">
+ <button type="submit">保存</button>
+ <Link to={`/bands/of/${band.id}`} className="btn">キャンセル</Link>
</div>
</Form>
</main>
diff --git a/app/routes/band-history.tsx b/app/routes/band-history.tsx
index 11e2f2f..954fd52 100644
--- a/app/routes/band-history.tsx
+++ b/app/routes/band-history.tsx
@@ -12,44 +12,32 @@ export async function loader({ params }: LoaderFunctionArgs) {
export default function BandHistory() {
const { band, revisions } = useLoaderData<typeof loader>();
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-center gap-3 mb-6">
- <Link
- to={`/bands/of/${band.id}`}
- className="text-gray-400 hover:text-gray-200 transition-colors"
- >
- ←
- </Link>
- <h1 className="text-xl font-semibold">{band.name} — 編集履歴</h1>
+ <main>
+ <div className="page-header">
+ <Link to={`/bands/of/${band.id}`} className="back">←</Link>
+ <h1>{band.name} — 編集履歴</h1>
</div>
{revisions.length === 0 ? (
- <p className="text-gray-400">履歴がありません。</p>
+ <p className="muted">履歴がありません。</p>
) : (
- <ol className="space-y-4">
+ <ol className="rev-list">
{revisions.map((rev, i) => {
let snap: { name?: string; area?: string; links?: unknown[]; artists?: unknown[] } = {};
try { snap = JSON.parse(rev.snapshot); } catch { /* ignore */ }
return (
- <li key={rev.id} className="bg-gray-900 rounded-lg p-4">
- <div className="flex items-start justify-between gap-4">
- <div className="flex-1 min-w-0">
- <p className="font-medium text-gray-100 truncate">{rev.message}</p>
- <p className="text-xs text-gray-500 mt-1">
- {rev.created_at} · {rev.ip_address}
- </p>
+ <li key={rev.id} className="rev">
+ <div className="rev-header">
+ <div className="rev-main">
+ <p className="rev-message">{rev.message}</p>
+ <p className="rev-time">{rev.created_at} · {rev.ip_address}</p>
</div>
- {i === 0 && (
- <span className="text-xs text-blue-400 shrink-0">最新</span>
- )}
+ {i === 0 && <span className="rev-latest">最新</span>}
</div>
- <div className="mt-3 text-xs text-gray-400 space-y-0.5">
+ <div className="rev-snap">
<p>名前: {snap.name ?? "—"}</p>
{snap.area && <p>拠点: {snap.area}</p>}
- <p>
- リンク: {snap.links?.length ?? 0}件 / メンバー:{" "}
- {snap.artists?.length ?? 0}人
- </p>
+ <p>リンク: {snap.links?.length ?? 0}件 / メンバー: {snap.artists?.length ?? 0}人</p>
</div>
</li>
);
diff --git a/app/routes/band-new.tsx b/app/routes/band-new.tsx
index a15ce24..62be1d4 100644
--- a/app/routes/band-new.tsx
+++ b/app/routes/band-new.tsx
@@ -75,13 +75,13 @@ export default function BandNew() {
}
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-center gap-3 mb-6">
- <Link to="/" className="text-gray-400 hover:text-gray-200 transition-colors">←</Link>
- <h1 className="text-xl font-semibold">New Band</h1>
+ <main>
+ <div className="page-header">
+ <Link to="/" className="back">←</Link>
+ <h1>New Band</h1>
</div>
- <Form method="post" className="space-y-5">
+ <Form method="post">
<input type="hidden" name="links" value={JSON.stringify(links)} />
<input
type="hidden"
@@ -90,46 +90,34 @@ export default function BandNew() {
/>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- バンド名 <span className="text-red-400">*</span>
- </label>
+ <label>バンド名 <span className="req">*</span></label>
<input
name="name"
value={name}
onChange={(e) => { setName(e.target.value); if (!slugManual) setSlug(toSlug(e.target.value)); }}
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
/>
- {errors.name && <p className="text-red-400 text-sm mt-1">{errors.name}</p>}
+ {errors.name && <p className="error">{errors.name}</p>}
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- Slug <span className="text-red-400">*</span>
- </label>
+ <label>Slug <span className="req">*</span></label>
<input
name="slug"
value={slug}
onChange={(e) => { setSlugManual(true); setSlug(e.target.value); }}
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 font-mono text-sm"
+ className="mono"
/>
- {errors.slug && <p className="text-red-400 text-sm mt-1">{errors.slug}</p>}
+ {errors.slug && <p className="error">{errors.slug}</p>}
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">活動拠点</label>
- <input
- name="area"
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
- />
+ <label>活動拠点</label>
+ <input name="area" />
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">ステータス</label>
- <select
- name="status"
- defaultValue="active"
- className="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
- >
+ <label>ステータス</label>
+ <select name="status" defaultValue="active">
<option value="active">活動中</option>
<option value="hiatus">活動休止</option>
<option value="disbanded">解散</option>
@@ -137,23 +125,18 @@ export default function BandNew() {
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">説明</label>
- <textarea
- name="description"
- rows={3}
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 resize-none"
- />
+ <label>説明</label>
+ <textarea name="description" rows={3} />
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-2">リンク</label>
- <div className="space-y-2">
+ <label>リンク</label>
+ <div className="links-form">
{links.map((link, i) => (
- <div key={i} className="flex gap-2">
+ <div key={i} className="link-row">
<select
value={link.label}
onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, label: e.target.value } : l))}
- className="w-36 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm"
>
{LINK_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
</select>
@@ -161,73 +144,65 @@ export default function BandNew() {
value={link.url}
onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, url: e.target.value } : l))}
placeholder="https://..."
- className="flex-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm"
/>
- <button type="button" onClick={() => setLinks(links.filter((_, idx) => idx !== i))} className="text-gray-500 hover:text-red-400 px-2 transition-colors">×</button>
+ <button type="button" className="btn-icon" onClick={() => setLinks(links.filter((_, idx) => idx !== i))}>×</button>
</div>
))}
</div>
<button
type="button"
+ className="btn-text"
onClick={() => setLinks([...links, { label: LINK_TYPES[0].value, url: "" }])}
- className="mt-2 text-blue-400 hover:text-blue-300 text-sm transition-colors"
>
+ リンクを追加
</button>
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-2">メンバー</label>
+ <label>メンバー</label>
{picked.length > 0 && (
- <div className="space-y-2 mb-3">
+ <div className="members-form">
{picked.map((p) => {
const pend = pending[p.id] ?? DEFAULT_PENDING;
return (
- <div key={p.id} className="bg-gray-900 rounded-lg p-3">
- <div className="flex items-center justify-between mb-2">
- <span className="text-gray-200 text-sm font-medium">{p.name}</span>
+ <div key={p.id} className="member-card">
+ <div className="card-header">
+ <span className="card-name">{p.name}</span>
<button
type="button"
+ className="btn-icon"
onClick={() => setPicked(picked.filter((a) => a.id !== p.id))}
- className="text-gray-500 hover:text-red-400 text-xs transition-colors"
>
削除
</button>
</div>
{p.roles.length > 0 && (
- <div className="flex flex-wrap gap-1.5 mb-2">
+ <div className="badges">
{p.roles.map((r, ri) => (
- <span key={ri} className="inline-flex items-center gap-1 bg-gray-800 text-gray-300 text-xs px-2 py-0.5 rounded">
+ <span key={ri} className="badge">
{r}
- <button type="button" onClick={() => removeRole(p.id, ri)} className="text-gray-500 hover:text-red-400 leading-none">×</button>
+ <button type="button" onClick={() => removeRole(p.id, ri)}>×</button>
</span>
))}
</div>
)}
- <div className="flex gap-2 items-center">
+ <div className="role-row">
<select
value={pend.type}
onChange={(e) => setPending({ ...pending, [p.id]: { ...pend, type: e.target.value } })}
- className="bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-gray-300 text-sm focus:outline-none focus:border-blue-500"
>
{ARTIST_ROLES.map((r) => <option key={r} value={r}>{r}</option>)}
<option value="other">その他...</option>
</select>
{pend.type === "other" && (
<input
+ className="custom-input"
value={pend.custom}
onChange={(e) => setPending({ ...pending, [p.id]: { ...pend, custom: e.target.value } })}
placeholder="ロール名"
- className="bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-gray-300 text-sm focus:outline-none focus:border-blue-500 w-32"
/>
)}
- <button
- type="button"
- onClick={() => addRole(p.id)}
- className="text-blue-400 hover:text-blue-300 text-sm transition-colors"
- >
- + 追加
- </button>
+ <button type="button" className="btn-text" onClick={() => addRole(p.id)}>+ 追加</button>
</div>
</div>
);
@@ -245,34 +220,27 @@ export default function BandNew() {
}
}}
defaultValue=""
- className="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-400 focus:outline-none focus:border-blue-500 text-sm"
>
<option value="">+ アーティストを追加...</option>
{available.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
</select>
) : artists.length === 0 ? (
- <p className="text-gray-500 text-sm">
+ <p className="muted">
アーティストがいません。{" "}
- <Link to="/artists/new" className="text-blue-400 hover:text-blue-300">先に作成</Link>
+ <Link to="/artists/new">先に作成</Link>
</p>
) : null}
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- 更新メッセージ <span className="text-red-400">*</span>
- </label>
- <input
- name="message"
- placeholder="例: 初回登録"
- className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
- />
- {errors.message && <p className="text-red-400 text-sm mt-1">{errors.message}</p>}
+ <label>更新メッセージ <span className="req">*</span></label>
+ <input name="message" placeholder="例: 初回登録" />
+ {errors.message && <p className="error">{errors.message}</p>}
</div>
- <div className="flex gap-3 pt-2">
- <button type="submit" className="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded font-medium transition-colors">作成</button>
- <Link to="/" className="bg-gray-800 hover:bg-gray-700 text-gray-300 px-4 py-2 rounded transition-colors">キャンセル</Link>
+ <div className="actions">
+ <button type="submit">作成</button>
+ <Link to="/" className="btn">キャンセル</Link>
</div>
</Form>
</main>
diff --git a/app/routes/home.tsx b/app/routes/home.tsx
index 5600645..5aa7de1 100644
--- a/app/routes/home.tsx
+++ b/app/routes/home.tsx
@@ -14,32 +14,23 @@ const STATUS_LABEL: Record<string, string> = {
export default function Home() {
const { bands } = useLoaderData<typeof loader>();
return (
- <main className="max-w-2xl mx-auto px-6 py-12">
+ <main>
{bands.length === 0 ? (
- <p className="text-gray-600 text-sm">
+ <p className="muted">
バンドがまだありません。{" "}
- <Link to="/bands/new" className="underline">
- 追加する
- </Link>
+ <Link to="/bands/new">追加する</Link>
</p>
) : (
- <ul className="divide-y divide-gray-900">
+ <ul className="band-list">
{bands.map((band) => (
- <li key={band.id} className="py-4 flex items-start justify-between gap-6">
- <div className="min-w-0">
- <Link
- to={`/bands/of/${band.id}`}
- className="text-gray-100 hover:text-white font-medium"
- >
- {band.name}
- </Link>
+ <li key={band.id}>
+ <div className="name-col">
+ <Link to={`/bands/of/${band.id}`}>{band.name}</Link>
{band.description && (
- <p className="text-gray-500 text-sm mt-0.5 line-clamp-1">
- {band.description}
- </p>
+ <p className="desc">{band.description}</p>
)}
</div>
- <div className="flex items-center gap-4 text-sm text-gray-500 shrink-0 pt-0.5">
+ <div className="info">
{band.area && <span>{band.area}</span>}
<span>{STATUS_LABEL[band.status] ?? band.status}</span>
</div>