diff options
| author | yyamashita <yyamashita@mosquit.one> | 2026-05-09 14:11:33 +0900 |
|---|---|---|
| committer | yyamashita <yyamashita@mosquit.one> | 2026-05-09 14:11:33 +0900 |
| commit | e2f492ccae9afcc98ae7eb76bb94dc973aed60d8 (patch) | |
| tree | a58b38d426dc51f06ccfa7e159c668f8dc9ce30c | |
| parent | cd8787b77dadf752826a967d404b718b3ec92601 (diff) | |
simplize css
| -rw-r--r-- | app/app.css | 256 | ||||
| -rw-r--r-- | app/root.tsx | 34 | ||||
| -rw-r--r-- | app/routes/artist-by-uuid.tsx | 75 | ||||
| -rw-r--r-- | app/routes/artist-edit.tsx | 71 | ||||
| -rw-r--r-- | app/routes/artist-history.tsx | 35 | ||||
| -rw-r--r-- | app/routes/artist-new.tsx | 66 | ||||
| -rw-r--r-- | app/routes/band-by-uuid.tsx | 77 | ||||
| -rw-r--r-- | app/routes/band-edit.tsx | 112 | ||||
| -rw-r--r-- | app/routes/band-history.tsx | 40 | ||||
| -rw-r--r-- | app/routes/band-new.tsx | 114 | ||||
| -rw-r--r-- | app/routes/home.tsx | 27 |
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> |
