redesigned buttons

This commit is contained in:
2026-05-15 14:40:08 +02:00
parent 59108835dd
commit 288bf890dc
13 changed files with 99 additions and 82 deletions
@@ -8,7 +8,7 @@ interface Props {
}
export default function AssetsButton({
className = 'inline-flex items-center gap-2 bg-surface0 hover:bg-surface1 text-subtext1 hover:text-text px-3 py-2 rounded-lg border border-surface1 transition-colors text-sm',
className = 'btn btn--ghost',
label = 'Assets',
iconSize = 14,
}: Props) {
@@ -89,8 +89,7 @@ export default function AssetsButton({
type="button"
onClick={() => setOpen(false)}
aria-label="Close"
className="p-1.5 text-[var(--subtext0)] hover:text-[var(--text)] hover:bg-[var(--surface0)] transition-colors"
style={{ borderRadius: 2 }}
className="btn btn--ghost btn--icon btn--sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -169,7 +169,7 @@ export default function ContactForm() {
<button
type="submit"
disabled={status === 'sending'}
className="btn-stamp disabled:opacity-60"
className="btn btn--primary"
>
{status === 'sending' ? 'Sending…' : 'Send message'}
</button>
@@ -31,7 +31,7 @@ export default function DeletePostButton({ slug, title, variant = 'full' }: Prop
disabled={busy}
title="Delete post"
aria-label="Delete post"
className="p-2 rounded-md bg-surface0/80 hover:bg-red/20 text-subtext0 hover:text-red border border-surface1 transition-colors disabled:opacity-50"
className="btn btn--danger btn--icon btn--sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -60,7 +60,7 @@ export default function DeletePostButton({ slug, title, variant = 'full' }: Prop
type="button"
onClick={handleClick}
disabled={busy}
className="inline-flex items-center gap-2 bg-surface0 hover:bg-red/15 text-subtext1 hover:text-red px-3 py-1.5 md:px-4 md:py-2 rounded-md border border-surface1 hover:border-red/30 transition-colors text-sm disabled:opacity-50"
className="btn btn--danger btn--sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
+2 -4
View File
@@ -197,8 +197,7 @@ export default function PostList({ posts: initialPosts, isAdmin = false }: Props
onClick={e => e.stopPropagation()}
title="Edit"
aria-label={`Edit ${displayTitle}`}
className="p-1.5 bg-[var(--mantle)] text-[var(--rosewater)] hover:bg-[var(--blue)] border border-[var(--surface2)] transition-colors"
style={{ borderRadius: 1 }}
className="btn btn--ghost btn--icon btn--sm"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" /><path d="m15 5 4 4" /></svg>
</a>
@@ -208,8 +207,7 @@ export default function PostList({ posts: initialPosts, isAdmin = false }: Props
disabled={isDeleting}
title="Remove"
aria-label={`Remove ${displayTitle}`}
className="p-1.5 bg-[var(--mantle)] text-[var(--rosewater)] hover:bg-[var(--red)] border border-[var(--surface2)] transition-colors disabled:opacity-50"
style={{ borderRadius: 1 }}
className="btn btn--danger btn--icon btn--sm"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /><line x1="10" x2="10" y1="11" y2="17" /><line x1="14" x2="14" y1="11" y2="17" /></svg>
</button>
@@ -109,8 +109,7 @@ export default function AssetManager({ mode = 'manage', onSelect }: Props) {
{mode === 'select' && onSelect && (
<button
onClick={() => onSelect(asset)}
className="bg-[var(--mauve)] hover:bg-[var(--red)] text-[var(--rosewater)] px-4 py-2 text-sm font-display italic font-medium transition-colors shadow-lg inline-flex items-center gap-1.5"
style={{ borderRadius: 1 }}
className="btn btn--primary btn--sm"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
Insert
@@ -118,8 +117,7 @@ export default function AssetManager({ mode = 'manage', onSelect }: Props) {
)}
<button
onClick={() => handleDelete(asset.name)}
className="bg-[var(--red)]/80 hover:bg-[var(--red)] text-[var(--rosewater)] p-2.5 transition-colors shadow-lg"
style={{ borderRadius: 1 }}
className="btn btn--danger btn--icon btn--sm"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
</button>
+9 -27
View File
@@ -415,11 +415,11 @@ export default function Editor({ editSlug }: Props) {
{/* Actions bar */}
<div className="flex flex-wrap gap-4 mb-6">
{originalSlug && (
<button onClick={handleDelete} className="btn-ghost py-3 px-6 text-[var(--red)] border-[var(--red)]/40 hover:bg-[var(--red)]/10 hover:border-[var(--red)] hover:text-[var(--red)]">
<button onClick={handleDelete} className="btn btn--danger">
Remove
</button>
)}
<button onClick={handleSave} className="btn-stamp py-3 px-8 whitespace-nowrap">
<button onClick={handleSave} className="btn btn--primary">
Save work
</button>
{originalSlug && (
@@ -427,7 +427,7 @@ export default function Editor({ editSlug }: Props) {
href={`/posts/${encodeURIComponent(originalSlug)}`}
target="_blank"
rel="noreferrer"
className="btn-ghost py-3 px-6 inline-flex items-center justify-center gap-2 whitespace-nowrap"
className="btn btn--ghost"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
View work
@@ -521,12 +521,7 @@ export default function Editor({ editSlug }: Props) {
<button
type="button"
onClick={() => setVimEnabled(v => !v)}
className={`text-xs px-3 py-1.5 border transition-colors font-mono ${
vimEnabled
? 'bg-[var(--mauve)]/20 text-[var(--mauve)] border-[var(--mauve)]/30'
: 'bg-[var(--surface0)] text-[var(--subtext0)] border-[var(--surface2)] hover:text-[var(--text)]'
}`}
style={{ borderRadius: 2 }}
className={`btn btn--sm${vimEnabled ? ' is-active' : ''}`}
title={vimEnabled ? 'Vim mode ON' : 'Vim mode OFF'}
>
{vimEnabled ? 'VIM' : 'vim'}
@@ -534,19 +529,14 @@ export default function Editor({ editSlug }: Props) {
<button
type="button"
onClick={() => setShowPreview(p => !p)}
className={`text-xs px-3 py-1.5 border transition-colors ${
showPreview
? 'bg-[var(--blue)]/20 text-[var(--blue)] border-[var(--blue)]/30'
: 'bg-[var(--surface0)] text-[var(--subtext0)] border-[var(--surface2)] hover:text-[var(--text)]'
}`}
style={{ borderRadius: 2 }}
className={`btn btn--sm${showPreview ? ' is-active' : ''}`}
>
{showPreview ? 'Hide Preview' : 'Show Preview'}
</button>
<button
type="button"
onClick={() => setShowModal(true)}
className="btn-stamp text-sm py-1.5 px-4"
className="btn btn--primary btn--sm"
title="Insert an image — also: drag an image into the editor, or paste from clipboard"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
@@ -563,11 +553,7 @@ export default function Editor({ editSlug }: Props) {
role="tab"
aria-selected={mobileView === 'edit'}
onClick={() => setMobileView('edit')}
className={`flex-1 text-xs px-3 py-2 rounded border transition-colors ${
mobileView === 'edit'
? 'bg-blue/20 text-blue border-blue/30'
: 'bg-surface0 text-subtext0 border-surface1 hover:text-text'
}`}
className={`btn btn--sm flex-1${mobileView === 'edit' ? ' is-active' : ''}`}
>
Edit
</button>
@@ -576,11 +562,7 @@ export default function Editor({ editSlug }: Props) {
role="tab"
aria-selected={mobileView === 'preview'}
onClick={() => setMobileView('preview')}
className={`flex-1 text-xs px-3 py-2 rounded border transition-colors ${
mobileView === 'preview'
? 'bg-blue/20 text-blue border-blue/30'
: 'bg-surface0 text-subtext0 border-surface1 hover:text-text'
}`}
className={`btn btn--sm flex-1${mobileView === 'preview' ? ' is-active' : ''}`}
>
Preview
</button>
@@ -679,7 +661,7 @@ export default function Editor({ editSlug }: Props) {
<h2 className="font-display italic text-2xl md:text-3xl text-[var(--text)] leading-tight">Add image</h2>
<p className="text-xs text-[var(--subtext0)] font-display italic mt-1">Click an image to insert it. Drag new files in to upload.</p>
</div>
<button onClick={closeAssetModal} className="p-2 text-[var(--subtext0)] hover:text-[var(--red)] transition-colors">
<button onClick={closeAssetModal} className="btn btn--ghost btn--icon btn--sm">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</header>
@@ -120,7 +120,7 @@ export default function Inbox() {
{m.email && (
<a
href={`mailto:${m.email}${m.subject ? `?subject=${encodeURIComponent('Re: ' + m.subject)}` : ''}`}
className="chip chip-accent uppercase"
className="btn btn--ghost btn--sm"
>
Reply
</a>
@@ -128,7 +128,7 @@ export default function Inbox() {
<button
type="button"
onClick={() => remove(m.id)}
className="chip text-[var(--red)]"
className="btn btn--danger btn--sm"
>
Delete
</button>
@@ -65,7 +65,7 @@ export default function Login() {
<button
type="submit"
disabled={busy}
className="btn-stamp w-full justify-center disabled:opacity-60 disabled:cursor-not-allowed"
className="btn btn--primary btn--block"
>
{busy ? 'Unlocking' : 'Enter'}
</button>
@@ -215,7 +215,7 @@ export default function Settings() {
<Field label="Footer text" value={config.footer || ''} onChange={v => update('footer', v)} />
</section>
<button type="submit" className="btn-stamp">
<button type="submit" className="btn btn--primary">
Save site settings
</button>
</form>
+1 -1
View File
@@ -19,6 +19,6 @@ import Layout from '../layouts/Layout.astro';
The room you reached for has either been re-hung, withdrawn,<br class="hidden md:block" />
or never made it to the wall in the first place.
</p>
<a href="/" class="btn-stamp">↶ Return to the catalogue</a>
<a href="/" class="btn btn--primary">↶ Return to the catalogue</a>
</div>
</Layout>
+5 -5
View File
@@ -96,16 +96,16 @@ const isAdmin = Astro.cookies.get('admin_session')?.value === '1';
{isAdmin && (
<div class="mt-8 flex flex-wrap items-center gap-3">
<a href="/admin/editor" class="btn-stamp">
<a href="/admin/editor" class="btn btn--primary">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
New work
</a>
<AssetsButton client:idle className="btn-ghost" iconSize={12} />
<a href="/admin/messages" class="btn-ghost">
<AssetsButton client:idle className="btn btn--ghost btn--sm" iconSize={12} />
<a href="/admin/messages" class="btn btn--ghost">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
Messages
</a>
<a href="/admin/settings" class="btn-ghost">
<a href="/admin/settings" class="btn btn--ghost">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/></svg>
Settings
</a>
@@ -128,7 +128,7 @@ const isAdmin = Astro.cookies.get('admin_session')?.value === '1';
</p>
<p class="font-sans text-[var(--subtext1)] mt-4">Check back soon.</p>
{isAdmin && (
<a href="/admin/editor" class="btn-stamp mt-8">Create the first post</a>
<a href="/admin/editor" class="btn btn--primary mt-8">Create the first post</a>
)}
</div>
)}
+2 -2
View File
@@ -84,7 +84,7 @@ const displayTitle = post ? (post.title || formatSlug(post.slug)) : 'Work';
<div class="max-w-2xl mx-auto py-20 md:py-32 text-center">
<div class="font-display italic text-[var(--subtext0)] text-sm tracking-[0.3em] uppercase mb-4">Pardon —</div>
<h2 class="font-display italic text-3xl md:text-5xl text-[var(--mauve)] mb-6 leading-tight">{error}</h2>
<a href="/" class="btn-ghost">← Return to the catalogue</a>
<a href="/" class="btn btn--ghost">← Return to the catalogue</a>
</div>
)}
@@ -99,7 +99,7 @@ const displayTitle = post ? (post.title || formatSlug(post.slug)) : 'Work';
{isAdmin && (
<div class="flex items-center gap-2">
<a href={`/admin/editor?edit=${encodeURIComponent(post.slug)}`} class="btn-ghost">
<a href={`/admin/editor?edit=${encodeURIComponent(post.slug)}`} class="btn btn--ghost">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
Edit
</a>
+69 -29
View File
@@ -914,58 +914,98 @@ code, pre, kbd, samp {
0 14px 40px -24px rgba(0, 0, 0, 0.9);
}
/* ───── Buttons ───── */
.btn-stamp {
/* ───── Buttons — one system ─────
* Base .btn = layout + size + focus/disabled. One variant for color
* (--primary / --ghost / --danger), one size modifier (--sm / --lg),
* shape modifiers (--icon / --block). Never restyle buttons ad-hoc. */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
height: 2.5rem;
padding: 0 1.2rem;
font-family: var(--font-display);
font-style: italic;
font-weight: 500;
font-size: 0.95rem;
padding: 0.55rem 1.2rem;
line-height: 1;
letter-spacing: 0.02em;
background: transparent;
color: var(--text);
border: 1px solid transparent;
border-radius: 1px;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition: transform 0.15s ease, background 0.15s ease, color 0.15s ease,
border-color 0.15s ease, box-shadow 0.15s ease;
}
.btn:focus-visible {
outline: none;
border-color: var(--mauve);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--mauve) 35%, transparent);
}
.btn:disabled,
.btn[aria-disabled="true"] {
opacity: 0.55;
cursor: default;
pointer-events: none;
}
.btn svg { width: 1.05em; height: 1.05em; flex-shrink: 0; }
/* Variants */
.btn--primary {
background: var(--mauve);
color: var(--rosewater);
border: 1px solid var(--mauve);
border-radius: 1px;
letter-spacing: 0.02em;
text-decoration: none;
cursor: pointer;
transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
border-color: var(--mauve);
box-shadow: 0 4px 0 -2px color-mix(in srgb, var(--mauve) 60%, black);
}
.btn-stamp:hover {
.btn--primary:hover {
transform: translateY(-1px);
background: var(--red);
border-color: var(--red);
box-shadow: 0 6px 0 -2px color-mix(in srgb, var(--red) 60%, black);
}
.btn-stamp:active {
.btn--primary:active {
transform: translateY(1px);
box-shadow: 0 1px 0 -1px color-mix(in srgb, var(--mauve) 60%, black);
}
.btn-ghost {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-family: var(--font-sans);
font-size: 0.82rem;
padding: 0.4rem 0.85rem;
background: transparent;
.btn--ghost {
color: var(--subtext1);
border: 1px solid var(--surface2);
border-radius: 1px;
letter-spacing: 0.06em;
text-transform: uppercase;
text-decoration: none;
cursor: pointer;
transition: color 0.15s, border-color 0.15s, background 0.15s;
border-color: var(--surface2);
background: color-mix(in srgb, var(--surface0) 45%, transparent);
}
.btn-ghost:hover {
.btn--ghost:hover {
color: var(--mauve);
border-color: var(--mauve);
background: color-mix(in srgb, var(--mauve) 8%, transparent);
border-color: color-mix(in srgb, var(--mauve) 50%, var(--surface2));
background: color-mix(in srgb, var(--surface0) 80%, transparent);
}
.btn--danger {
color: var(--red);
border-color: color-mix(in srgb, var(--red) 40%, var(--surface2));
background: color-mix(in srgb, var(--surface0) 45%, transparent);
}
.btn--danger:hover {
color: var(--rosewater);
background: var(--red);
border-color: var(--red);
}
/* Pressed/selected state for toggle & tab buttons */
.btn.is-active {
color: var(--mauve);
border-color: color-mix(in srgb, var(--mauve) 55%, var(--surface2));
background: color-mix(in srgb, var(--mauve) 14%, transparent);
}
/* Sizes */
.btn--sm { height: 2rem; padding: 0 0.85rem; font-size: 0.85rem; gap: 0.35rem; }
.btn--lg { height: 3rem; padding: 0 1.6rem; font-size: 1.05rem; }
/* Shapes */
.btn--icon { padding: 0; width: 2.5rem; }
.btn--icon.btn--sm { width: 2rem; }
.btn--block { width: 100%; }
/* ───── Top-bar controls — one height, one language ───── */
.topbar-control {