Skip to content

Commit 2e72c48

Browse files
Stivarosclaude
andcommitted
feat(thoughts): add terminal filter; extract .terminal-filter to global.css
- Promote cursor/filter styles from index.astro <style> block to .terminal-filter component class in global.css - thoughts/index: filter rows by title/tag with up/down nav and Enter-to-navigate, matching the index page pattern Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 074f8f0 commit 2e72c48

3 files changed

Lines changed: 96 additions & 29 deletions

File tree

src/pages/index.astro

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -76,36 +76,11 @@ function fmtDate(d: Date) {
7676
spellcheck="false"
7777
data-placeholder="filter..."
7878
aria-label="Filter sections"
79-
class="font-mono text-sm text-zinc-100 light:text-zinc-900 ml-1 outline-none inline-block"
79+
class="terminal-filter font-mono text-sm text-zinc-100 light:text-zinc-900 ml-1 outline-none inline-block"
8080
></span>
8181
</div>
8282
</BaseLayout>
8383

84-
<style>
85-
#terminal-filter {
86-
caret-color: transparent;
87-
min-width: 1ch;
88-
}
89-
#terminal-filter:focus-visible {
90-
box-shadow: none;
91-
}
92-
#terminal-filter::after {
93-
content: '';
94-
display: inline-block;
95-
width: 0.5em;
96-
height: 1.5em;
97-
background-color: #34d399; /* emerald-400 */
98-
vertical-align: bottom;
99-
animation: blink 1s steps(2) infinite;
100-
}
101-
:global(.light) #terminal-filter::after {
102-
background-color: #059669; /* emerald-600 */
103-
}
104-
#terminal-filter:empty::before {
105-
content: attr(data-placeholder);
106-
color: #52525b; /* zinc-600 */
107-
}
108-
</style>
10984

11085
<script>
11186
const input = document.getElementById('terminal-filter') as HTMLElement;

src/pages/thoughts/index.astro

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ entries.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
2929
const date = entry.data.date.toISOString().slice(0, 10);
3030
const tag = entry.data.tags?.[0] ?? '';
3131
return (
32-
<div class="grid grid-cols-[100px_1fr_80px] sm:grid-cols-[100px_60px_1fr_80px] gap-4 py-2.5 items-baseline border-b border-dashed border-zinc-800 light:border-zinc-200 min-w-[360px]">
32+
<div
33+
class="thought-row grid grid-cols-[100px_1fr_80px] sm:grid-cols-[100px_60px_1fr_80px] gap-4 py-2.5 items-baseline border-b border-dashed border-zinc-800 light:border-zinc-200 min-w-[360px] transition-colors duration-100"
34+
data-name={`${entry.data.title} ${tag}`}
35+
>
3336
<span class="text-zinc-500 text-xs">{date}</span>
3437
<span class="hidden sm:block text-zinc-600 text-xs text-right">—</span>
3538
<a href={`/thoughts/${entry.id}`} class="text-sm font-medium truncate">
@@ -45,8 +48,69 @@ entries.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
4548
)
4649
}
4750

48-
<div class="term-prompt mt-6">
51+
<div class="term-prompt border border-accent/40 px-3 py-1.5 inline-flex items-center mt-6">
4952
<span class="text-accent">$</span>
50-
<span class="ml-1">_</span>
53+
<span
54+
id="thought-filter"
55+
role="textbox"
56+
contenteditable="plaintext-only"
57+
spellcheck="false"
58+
data-placeholder="filter..."
59+
aria-label="Filter thoughts"
60+
class="terminal-filter font-mono text-sm text-zinc-100 light:text-zinc-900 ml-1 outline-none inline-block"
61+
></span>
5162
</div>
5263
</BaseLayout>
64+
65+
<script>
66+
const input = document.getElementById('thought-filter') as HTMLElement;
67+
const rows = Array.from(document.querySelectorAll<HTMLElement>('.thought-row'));
68+
69+
let focusedIndex = -1;
70+
71+
const visibleRows = () => rows.filter(r => r.style.display !== 'none');
72+
73+
function setFocus(index: number) {
74+
focusedIndex = index;
75+
rows.forEach(r => r.classList.remove('ls-row-focused'));
76+
const visible = visibleRows();
77+
if (focusedIndex >= 0 && focusedIndex < visible.length) {
78+
visible[focusedIndex].classList.add('ls-row-focused');
79+
}
80+
}
81+
82+
input.focus();
83+
84+
input.addEventListener('input', () => {
85+
if (input.innerHTML === '<br>') input.innerHTML = '';
86+
const q = (input.textContent ?? '').toLowerCase();
87+
rows.forEach(r => {
88+
r.style.display = !q || r.dataset.name!.toLowerCase().includes(q) ? '' : 'none';
89+
});
90+
setFocus(-1);
91+
});
92+
93+
input.addEventListener('keydown', (e: KeyboardEvent) => {
94+
const visible = visibleRows();
95+
if (e.key === 'ArrowDown') {
96+
e.preventDefault();
97+
setFocus(Math.min(focusedIndex + 1, visible.length - 1));
98+
} else if (e.key === 'ArrowUp') {
99+
e.preventDefault();
100+
setFocus(Math.max(focusedIndex - 1, 0));
101+
} else if (e.key === 'Enter') {
102+
e.preventDefault();
103+
const target = focusedIndex >= 0 ? visible[focusedIndex] : visible[0];
104+
if (target) {
105+
const link = target.querySelector('a');
106+
if (link) window.location.href = link.href;
107+
}
108+
}
109+
});
110+
111+
input.addEventListener('paste', (e: ClipboardEvent) => {
112+
e.preventDefault();
113+
const text = e.clipboardData?.getData('text/plain') ?? '';
114+
document.execCommand('insertText', false, text);
115+
});
116+
</script>

src/styles/global.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,34 @@
175175
.ls-row-focused {
176176
background-color: rgba(52, 211, 153, 0.08);
177177
}
178+
179+
.terminal-filter {
180+
caret-color: transparent;
181+
min-width: 1ch;
182+
183+
&:focus-visible {
184+
box-shadow: none;
185+
}
186+
187+
&::after {
188+
content: '';
189+
display: inline-block;
190+
width: 0.5em;
191+
height: 1.5em;
192+
background-color: #34d399; /* emerald-400 */
193+
vertical-align: bottom;
194+
animation: blink 1s steps(2) infinite;
195+
}
196+
197+
&:empty::before {
198+
content: attr(data-placeholder);
199+
color: #52525b; /* zinc-600 */
200+
}
201+
}
202+
203+
html.light .terminal-filter::after {
204+
background-color: #059669; /* emerald-600 */
205+
}
178206
}
179207

180208
@layer base {

0 commit comments

Comments
 (0)