fixed search below content + light editor and search

This commit is contained in:
2026-05-14 08:56:40 +02:00
parent 0050de7ea6
commit 20089fb507
4 changed files with 81 additions and 68 deletions
+28 -23
View File
@@ -128,9 +128,10 @@ export default function Search() {
<button <button
type="button" type="button"
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
aria-label={`Search posts (${isMac ? '⌘' : 'Ctrl'}+K)`} aria-label={`Search the catalogue (${isMac ? '⌘' : 'Ctrl'}+K)`}
title={`Search (${isMac ? '⌘' : 'Ctrl'}+K)`} title={`Search (${isMac ? '⌘' : 'Ctrl'}+K)`}
className="text-subtext0 hover:text-text transition-colors flex items-center gap-2 px-2 py-1 rounded-md hover:bg-surface0/60" className="text-[var(--subtext0)] hover:text-[var(--mauve)] transition-colors flex items-center gap-2 px-2 py-1 hover:bg-[var(--surface0)]/60 font-display italic"
style={{ borderRadius: 1 }}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -147,7 +148,7 @@ export default function Search() {
<circle cx="11" cy="11" r="8" /> <circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" /> <path d="m21 21-4.3-4.3" />
</svg> </svg>
<kbd className="hidden md:inline-flex items-center gap-1 text-[10px] font-mono text-subtext0 px-1.5 py-0.5 rounded border border-surface1 bg-surface0/60"> <kbd className="hidden md:inline-flex items-center gap-1 text-[10px] font-mono text-[var(--subtext0)] px-1.5 py-0.5 border border-[var(--surface2)] bg-[var(--surface0)]/60" style={{ borderRadius: 1 }}>
<span>{isMac ? '⌘' : 'Ctrl'}</span><span className="opacity-50">+</span><span>K</span> <span>{isMac ? '⌘' : 'Ctrl'}</span><span className="opacity-50">+</span><span>K</span>
</kbd> </kbd>
</button> </button>
@@ -157,18 +158,22 @@ export default function Search() {
className="fixed inset-0 z-[200] flex items-start justify-center pt-[10vh] md:pt-[15vh] px-4" className="fixed inset-0 z-[200] flex items-start justify-center pt-[10vh] md:pt-[15vh] px-4"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Search posts" aria-label="Search the catalogue"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
> >
<div <div
className="absolute inset-0 bg-crust/70 backdrop-blur-md" className="absolute inset-0 bg-[var(--crust)]/55 backdrop-blur-md"
aria-hidden="true" aria-hidden="true"
/> />
<div <div
className="relative w-full max-w-xl bg-mantle border border-surface1 rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[70vh] animate-in fade-in slide-in-from-top-4 duration-200" className="relative w-full max-w-xl bg-[var(--rosewater)] text-[var(--text)] border border-[var(--surface2)] overflow-hidden flex flex-col max-h-[70vh] animate-in fade-in slide-in-from-top-4 duration-200"
style={{
borderRadius: 2,
boxShadow: '0 30px 60px -20px rgba(20,16,12,0.55), 0 8px 20px -8px rgba(20,16,12,0.3), inset 0 0 0 1px color-mix(in srgb, var(--surface1) 50%, transparent)',
}}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
<div className="flex items-center gap-3 px-4 py-3 border-b border-surface1"> <div className="flex items-center gap-3 px-4 py-3 border-b border-[var(--surface2)]/60 bg-[var(--surface0)]/40">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="18" width="18"
@@ -179,7 +184,7 @@ export default function Search() {
strokeWidth="2" strokeWidth="2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
className="text-subtext0 shrink-0" className="text-[var(--mauve)] shrink-0"
aria-hidden="true" aria-hidden="true"
> >
<circle cx="11" cy="11" r="8" /> <circle cx="11" cy="11" r="8" />
@@ -191,25 +196,25 @@ export default function Search() {
value={query} value={query}
onChange={e => setQuery(e.target.value)} onChange={e => setQuery(e.target.value)}
onKeyDown={onInputKey} onKeyDown={onInputKey}
placeholder="Search posts…" placeholder="Search the catalogue…"
aria-label="Search query" aria-label="Search query"
className="flex-1 bg-transparent outline-none text-base text-text placeholder:text-subtext0" className="flex-1 bg-transparent outline-none text-base text-[var(--text)] placeholder:text-[var(--subtext0)] font-display italic"
/> />
<kbd className="text-[10px] font-mono text-subtext0 px-1.5 py-0.5 rounded border border-surface1 bg-surface0/60"> <kbd className="text-[10px] font-mono text-[var(--subtext0)] px-1.5 py-0.5 border border-[var(--surface2)] bg-[var(--surface0)]/60" style={{ borderRadius: 1 }}>
Esc Esc
</kbd> </kbd>
</div> </div>
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
{loading && ( {loading && (
<div className="px-4 py-8 text-center text-sm text-subtext0">Loading</div> <div className="px-4 py-10 text-center font-display italic text-[var(--subtext0)]">Fetching the catalogue</div>
)} )}
{error && ( {error && (
<div className="px-4 py-8 text-center text-sm text-red">{error}</div> <div className="px-4 py-8 text-center text-sm text-[var(--red)] font-display italic">{error}</div>
)} )}
{!loading && !error && posts && results.length === 0 && ( {!loading && !error && posts && results.length === 0 && (
<div className="px-4 py-8 text-center text-sm text-subtext0"> <div className="px-4 py-10 text-center font-display italic text-[var(--subtext0)]">
{query ? 'No posts match.' : 'No posts.'} {query ? 'No works match.' : 'The catalogue is empty.'}
</div> </div>
)} )}
{!loading && !error && results.length > 0 && ( {!loading && !error && results.length > 0 && (
@@ -224,20 +229,20 @@ export default function Search() {
onClick={() => navigate(p.slug)} onClick={() => navigate(p.slug)}
className={`px-4 py-3 cursor-pointer border-l-2 transition-colors ${ className={`px-4 py-3 cursor-pointer border-l-2 transition-colors ${
active active
? 'bg-surface0 border-mauve' ? 'bg-[var(--surface0)] border-[var(--mauve)]'
: 'border-transparent hover:bg-surface0/60' : 'border-transparent hover:bg-[var(--surface0)]/60'
}`} }`}
> >
<div className="flex items-baseline justify-between gap-3"> <div className="flex items-baseline justify-between gap-3">
<div className={`font-medium truncate ${active ? 'text-mauve' : 'text-lavender'}`}> <div className={`font-display italic truncate text-lg ${active ? 'text-[var(--mauve)]' : 'text-[var(--text)]'}`}>
{title} {title}
</div> </div>
<time className="text-[10px] text-subtext0 shrink-0" dateTime={p.date}> <time className="text-[10px] font-sans uppercase tracking-[0.18em] text-[var(--subtext0)] shrink-0" dateTime={p.date}>
{formatDate(p.date)} {formatDate(p.date)}
</time> </time>
</div> </div>
{p.excerpt && ( {p.excerpt && (
<div className="text-xs text-subtext1 line-clamp-1 mt-0.5">{p.excerpt}</div> <div className="text-xs text-[var(--subtext1)] line-clamp-1 mt-0.5 font-sans italic">{p.excerpt}</div>
)} )}
</li> </li>
); );
@@ -246,14 +251,14 @@ export default function Search() {
)} )}
</div> </div>
<div className="flex items-center justify-between px-4 py-2 border-t border-surface1 text-[10px] text-subtext0 bg-crust/40"> <div className="flex items-center justify-between px-4 py-2 border-t border-[var(--surface2)]/60 text-[10px] text-[var(--subtext0)] bg-[var(--surface0)]/40 font-display italic">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<kbd className="font-mono px-1 rounded border border-surface1 bg-surface0/60"></kbd> <kbd className="font-mono px-1 border border-[var(--surface2)] bg-[var(--surface0)]/60 not-italic" style={{ borderRadius: 1 }}></kbd>
navigate navigate
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<kbd className="font-mono px-1 rounded border border-surface1 bg-surface0/60"></kbd> <kbd className="font-mono px-1 border border-[var(--surface2)] bg-[var(--surface0)]/60 not-italic" style={{ borderRadius: 1 }}></kbd>
open open
</span> </span>
</div> </div>
+43 -37
View File
@@ -18,43 +18,48 @@ interface Props {
const salonTheme = EditorView.theme({ const salonTheme = EditorView.theme({
'&': { '&': {
backgroundColor: 'var(--crust)', backgroundColor: 'var(--rosewater)',
color: 'var(--text)', color: 'var(--text)',
border: '1px solid var(--surface1)', border: '1px solid var(--surface2)',
borderRadius: '0.75rem', borderRadius: '2px',
fontSize: '14px', fontSize: '14px',
boxShadow: 'inset 0 0 0 1px color-mix(in srgb, var(--surface1) 40%, transparent)',
}, },
'.cm-content': { '.cm-content': {
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace', fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace',
padding: '1rem', padding: '1rem',
caretColor: 'var(--text)', caretColor: 'var(--mauve)',
},
'.cm-cursor': { borderLeftColor: 'var(--text)' },
'.cm-selectionBackground': { backgroundColor: 'var(--surface2) !important' },
'&.cm-focused .cm-selectionBackground': { backgroundColor: 'var(--surface2) !important' },
'.cm-activeLine': { backgroundColor: 'var(--surface0)' },
'.cm-gutters': {
backgroundColor: 'var(--mantle)',
color: 'var(--overlay0)',
border: 'none',
},
'.cm-activeLineGutter': { backgroundColor: 'var(--surface0)' },
'.cm-panels': {
backgroundColor: 'var(--mantle)',
color: 'var(--text)', color: 'var(--text)',
borderTop: '1px solid var(--surface1)',
}, },
'.cm-searchMatch': { backgroundColor: 'var(--yellow)', opacity: '0.3' }, '.cm-cursor': { borderLeftColor: 'var(--mauve)', borderLeftWidth: '2px' },
'.cm-searchMatch-selected': { backgroundColor: 'var(--peach)', opacity: '0.4' }, '.cm-selectionBackground': { backgroundColor: 'color-mix(in srgb, var(--mauve) 25%, transparent) !important' },
'&.cm-focused .cm-selectionBackground': { backgroundColor: 'color-mix(in srgb, var(--mauve) 30%, transparent) !important' },
'.cm-activeLine': { backgroundColor: 'color-mix(in srgb, var(--surface0) 55%, transparent)' },
'.cm-gutters': {
backgroundColor: 'var(--surface0)',
color: 'var(--subtext0)',
border: 'none',
borderRight: '1px solid var(--surface2)',
fontFamily: 'var(--font-display)',
fontStyle: 'italic',
},
'.cm-activeLineGutter': { backgroundColor: 'color-mix(in srgb, var(--mauve) 12%, transparent)', color: 'var(--mauve)' },
'.cm-panels': {
backgroundColor: 'var(--surface0)',
color: 'var(--text)',
borderTop: '1px solid var(--surface2)',
},
'.cm-searchMatch': { backgroundColor: 'color-mix(in srgb, var(--yellow) 45%, transparent)' },
'.cm-searchMatch-selected': { backgroundColor: 'color-mix(in srgb, var(--peach) 55%, transparent)' },
'.cm-fat-cursor': { '.cm-fat-cursor': {
backgroundColor: 'var(--mauve) !important', backgroundColor: 'var(--mauve) !important',
color: 'var(--crust) !important', color: 'var(--rosewater) !important',
}, },
'&:not(.cm-focused) .cm-fat-cursor': { '&:not(.cm-focused) .cm-fat-cursor': {
outline: '1px solid var(--mauve)', outline: '1px solid var(--mauve)',
backgroundColor: 'transparent !important', backgroundColor: 'transparent !important',
}, },
}, { dark: true }); }, { dark: false });
// Compartment for hot-swapping vim mode without recreating the editor // Compartment for hot-swapping vim mode without recreating the editor
const vimCompartment = new Compartment(); const vimCompartment = new Compartment();
@@ -472,11 +477,11 @@ export default function Editor({ editSlug }: Props) {
{/* Autocomplete dropdown */} {/* Autocomplete dropdown */}
{showAutocomplete && autocompleteAssets.length > 0 && ( {showAutocomplete && autocompleteAssets.length > 0 && (
<div <div
className="absolute z-50 bg-mantle border border-surface1 rounded-lg shadow-2xl max-h-64 overflow-y-auto w-80" className="absolute z-50 bg-[var(--rosewater)] border border-[var(--surface2)] shadow-2xl max-h-64 overflow-y-auto w-80"
style={{ top: autocompletePos.top, left: autocompletePos.left }} style={{ top: autocompletePos.top, left: autocompletePos.left, borderRadius: 2 }}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
<div className="p-2 text-[10px] text-subtext0 uppercase border-b border-white/5 bg-crust/50">Assets Library</div> <div className="p-2 text-[10px] text-[var(--subtext0)] uppercase tracking-[0.2em] border-b border-[var(--surface2)]/60 bg-[var(--surface0)]/60 font-display italic">Assets</div>
<ul className="py-1"> <ul className="py-1">
{autocompleteAssets.map(asset => { {autocompleteAssets.map(asset => {
const img = /\.(jpg|jpeg|png|webp|gif|svg)$/i.test(asset.name); const img = /\.(jpg|jpeg|png|webp|gif|svg)$/i.test(asset.name);
@@ -484,16 +489,16 @@ export default function Editor({ editSlug }: Props) {
<li <li
key={asset.name} key={asset.name}
onClick={() => insertAssetMarkdown(asset)} onClick={() => insertAssetMarkdown(asset)}
className="px-4 py-2 hover:bg-mauve/20 cursor-pointer text-sm truncate text-subtext1 hover:text-mauve flex items-center gap-3 transition-colors" className="px-4 py-2 hover:bg-[var(--mauve)]/15 cursor-pointer text-sm truncate text-[var(--subtext1)] hover:text-[var(--mauve)] flex items-center gap-3 transition-colors"
> >
<div className="w-6 h-6 flex-shrink-0 bg-surface0 rounded flex items-center justify-center overflow-hidden"> <div className="w-6 h-6 flex-shrink-0 bg-[var(--surface0)] border border-[var(--surface2)] flex items-center justify-center overflow-hidden" style={{ borderRadius: 1 }}>
{img ? ( {img ? (
<img src={asset.url} className="w-full h-full object-cover" alt="" /> <img src={asset.url} className="w-full h-full object-cover" alt="" />
) : ( ) : (
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/></svg>
)} )}
</div> </div>
<span className="truncate">{asset.name}</span> <span className="truncate font-mono text-xs">{asset.name}</span>
</li> </li>
); );
})} })}
@@ -505,11 +510,12 @@ export default function Editor({ editSlug }: Props) {
{/* Live Preview — stretches to match editor height on desktop, full-pane via tabs on mobile */} {/* Live Preview — stretches to match editor height on desktop, full-pane via tabs on mobile */}
{showPreview && ( {showPreview && (
<div <div
className={`border border-surface1 rounded-xl bg-crust/50 overflow-y-auto flex-col md:flex md:min-h-0 ${ className={`border border-[var(--surface2)] bg-[var(--rosewater)] overflow-y-auto flex-col md:flex md:min-h-0 ${
mobileView === 'preview' ? 'flex min-h-[60vh]' : 'hidden' mobileView === 'preview' ? 'flex min-h-[60vh]' : 'hidden'
}`} }`}
style={{ borderRadius: 2 }}
> >
<div className="sticky top-0 bg-mantle px-4 py-2 text-xs text-subtext0 uppercase border-b border-surface1 z-10"> <div className="sticky top-0 bg-[var(--surface0)] px-4 py-2 text-xs text-[var(--subtext0)] uppercase tracking-[0.2em] border-b border-[var(--surface2)] z-10 font-display italic">
Preview Preview
</div> </div>
<div ref={previewRef} className="prose max-w-none p-4 md:p-6 flex-1" /> <div ref={previewRef} className="prose max-w-none p-4 md:p-6 flex-1" />
@@ -520,18 +526,18 @@ export default function Editor({ editSlug }: Props) {
{/* Asset Modal */} {/* Asset Modal */}
{showModal && ( {showModal && (
<div className="fixed inset-0 z-[100] bg-crust/80 backdrop-blur-sm flex items-center justify-center p-4 md:p-6"> <div className="fixed inset-0 z-[100] bg-[var(--crust)]/55 backdrop-blur-sm flex items-center justify-center p-4 md:p-6">
<div className="glass w-full max-w-5xl max-h-[90vh] flex flex-col overflow-hidden"> <div className="glass w-full max-w-5xl max-h-[90vh] flex flex-col overflow-hidden bg-[var(--base)]">
<header className="p-4 md:p-6 border-b border-white/5 flex justify-between items-center bg-surface0/20"> <header className="p-4 md:p-6 border-b border-[var(--surface2)]/60 flex justify-between items-center bg-[var(--surface0)]/50">
<div> <div>
<h2 className="text-xl md:text-2xl font-bold text-mauve">Asset Library</h2> <h2 className="font-display italic text-2xl md:text-3xl text-[var(--text)] leading-tight">Asset library</h2>
<p className="text-xs text-subtext0">Click 'Insert' to add an asset to your post.</p> <p className="text-xs text-[var(--subtext0)] font-display italic mt-1">Click an asset to drop it into the work.</p>
</div> </div>
<button onClick={() => setShowModal(false)} className="p-2 text-subtext0 hover:text-red transition-colors"> <button onClick={() => setShowModal(false)} className="p-2 text-[var(--subtext0)] hover:text-[var(--red)] transition-colors">
<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> <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> </button>
</header> </header>
<div className="p-4 md:p-6 overflow-y-auto flex-1 bg-bg/50"> <div className="p-4 md:p-6 overflow-y-auto flex-1 bg-[var(--base)]/50">
<AssetManager mode="select" onSelect={handleAssetSelect} /> <AssetManager mode="select" onSelect={handleAssetSelect} />
</div> </div>
</div> </div>
+3 -3
View File
@@ -76,7 +76,7 @@ const year = new Date().getFullYear();
<body class="text-text"> <body class="text-text">
<div class="salon-atmosphere" aria-hidden="true"></div> <div class="salon-atmosphere" aria-hidden="true"></div>
<header class="border-b border-[var(--surface2)]/60 relative z-10"> <header class="border-b border-[var(--surface2)]/60">
<div class="max-w-6xl mx-auto px-6 md:px-10 py-5 md:py-7 flex flex-col md:flex-row md:items-end gap-4 md:gap-6"> <div class="max-w-6xl mx-auto px-6 md:px-10 py-5 md:py-7 flex flex-col md:flex-row md:items-end gap-4 md:gap-6">
<a href="/" class="nameplate group" aria-label="Home"> <a href="/" class="nameplate group" aria-label="Home">
{isAdmin ? ( {isAdmin ? (
@@ -129,11 +129,11 @@ const year = new Date().getFullYear();
</div> </div>
</header> </header>
<main class={`mx-auto px-6 md:px-10 py-10 md:py-16 relative z-10 ${wide ? 'max-w-[95vw]' : 'max-w-6xl'}`}> <main class={`mx-auto px-6 md:px-10 py-10 md:py-16 ${wide ? 'max-w-[95vw]' : 'max-w-6xl'}`}>
<slot /> <slot />
</main> </main>
<footer class="max-w-6xl mx-auto px-6 md:px-10 py-12 md:py-16 text-center border-t border-[var(--surface2)]/40 mt-12 relative z-10"> <footer class="max-w-6xl mx-auto px-6 md:px-10 py-12 md:py-16 text-center border-t border-[var(--surface2)]/40 mt-12">
<div class="section-rule mb-6"> <div class="section-rule mb-6">
<span class="ornament">✦</span> <span class="ornament">✦</span>
<span class="font-display italic text-[var(--subtext0)] text-sm">end of catalogue</span> <span class="font-display italic text-[var(--subtext0)] text-sm">end of catalogue</span>
+7 -5
View File
@@ -142,13 +142,15 @@ body {
position: relative; position: relative;
} }
/* Paper grain — applied as a fixed overlay so every page gets the texture. */ /* Paper grain — applied as a fixed overlay so every page gets the texture.
* All three layers sit behind content (negative z-index) so fixed-positioned
* modals (e.g. the search palette) can escape ancestor stacking traps. */
body::before { body::before {
content: ""; content: "";
position: fixed; position: fixed;
inset: 0; inset: 0;
pointer-events: none; pointer-events: none;
z-index: -2; z-index: -3;
background-color: var(--base); background-color: var(--base);
} }
body::after { body::after {
@@ -156,8 +158,8 @@ body::after {
position: fixed; position: fixed;
inset: 0; inset: 0;
pointer-events: none; pointer-events: none;
z-index: 0; z-index: -1;
opacity: 0.35; opacity: 0.32;
mix-blend-mode: multiply; mix-blend-mode: multiply;
background-image: background-image:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.10 0 0 0 0 0.07 0 0 0 0 0.04 0 0 0 0.22 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>"); url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.10 0 0 0 0 0.07 0 0 0 0 0.04 0 0 0 0.22 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
@@ -171,7 +173,7 @@ html.salon-noir body::after {
.salon-atmosphere { .salon-atmosphere {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: -1; z-index: -2;
pointer-events: none; pointer-events: none;
overflow: hidden; overflow: hidden;
} }