Polish UI: SearchBar sizing, SessionList layout, Tooltip offset, and progress CSS

SearchBar:
- Switch from fixed w-80/sm:w-96 to fluid min-w-80 max-w-md w-full
- Adjust left padding for better visual alignment with search icon

SessionList:
- Memoize project grouping computation with useMemo
- Move horizontal padding from per-item margin (mx-2 + width calc hack)
  to container padding (px-2) for cleaner layout and full-width buttons
- Remove inline width override that was compensating for the old margins

Tooltip:
- Increase offset from 8px to 12px for better visual separation

CSS:
- Add prose-message-progress variant with compact 11px mono typography
  for progress event content (code blocks, tables, links, blockquotes)
- Reduce search minimap marker height from 4px to 3px
- Normalize prose-message line-height: paragraphs 1.625→1.6, list
  items 1.5→1.6 for consistent rhythm
- Switch custom checkbox checkmark sizing from fixed px to percentages
  for better scaling across different zoom levels

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 23:04:38 -05:00
parent 51a54e3fdd
commit d4de363227
4 changed files with 136 additions and 24 deletions

View File

@@ -107,7 +107,7 @@ export function SearchBar({
const showControls = !!localQuery || !!query; const showControls = !!localQuery || !!query;
return ( return (
<div className="w-80 sm:w-96"> <div className="min-w-80 max-w-md w-full">
{/* Unified search container */} {/* Unified search container */}
<div <div
className={` className={`
@@ -137,7 +137,7 @@ export function SearchBar({
onFocus={() => setIsFocused(true)} onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)} onBlur={() => setIsFocused(false)}
placeholder="Search messages..." placeholder="Search messages..."
className="flex-1 min-w-0 bg-transparent px-2.5 py-2 text-body text-foreground className="flex-1 min-w-0 bg-transparent pl-3 pr-2.5 py-2 text-body text-foreground
placeholder:text-foreground-muted placeholder:text-foreground-muted
focus:outline-none" focus:outline-none"
/> />

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useMemo } from "react";
import type { SessionEntry } from "../lib/types"; import type { SessionEntry } from "../lib/types";
interface Props { interface Props {
@@ -11,13 +11,16 @@ interface Props {
export function SessionList({ sessions, loading, selectedId, onSelect }: Props) { export function SessionList({ sessions, loading, selectedId, onSelect }: Props) {
const [selectedProject, setSelectedProject] = useState<string | null>(null); const [selectedProject, setSelectedProject] = useState<string | null>(null);
// Group by project // Group by project (memoized to avoid recomputing on unrelated rerenders)
const grouped = new Map<string, SessionEntry[]>(); const grouped = useMemo(() => {
for (const session of sessions) { const map = new Map<string, SessionEntry[]>();
const group = grouped.get(session.project) || []; for (const session of sessions) {
group.push(session); const group = map.get(session.project) || [];
grouped.set(session.project, group); group.push(session);
} map.set(session.project, group);
}
return map;
}, [sessions]);
// Auto-select project when selectedId changes // Auto-select project when selectedId changes
useEffect(() => { useEffect(() => {
@@ -73,7 +76,7 @@ export function SessionList({ sessions, loading, selectedId, onSelect }: Props)
<div className="px-4 py-2 text-caption font-semibold text-foreground-muted uppercase tracking-wider border-b border-border-muted" style={{ background: "var(--color-surface-inset)" }}> <div className="px-4 py-2 text-caption font-semibold text-foreground-muted uppercase tracking-wider border-b border-border-muted" style={{ background: "var(--color-surface-inset)" }}>
{formatProjectName(selectedProject)} {formatProjectName(selectedProject)}
</div> </div>
<div className="py-1"> <div className="py-1 px-2">
{projectSessions.map((session, idx) => { {projectSessions.map((session, idx) => {
const isSelected = selectedId === session.id; const isSelected = selectedId === session.id;
return ( return (
@@ -81,13 +84,13 @@ export function SessionList({ sessions, loading, selectedId, onSelect }: Props)
key={session.id} key={session.id}
onClick={() => onSelect(session.id)} onClick={() => onSelect(session.id)}
className={` className={`
w-full text-left mx-2 my-0.5 px-3 py-2.5 rounded-lg transition-all duration-200 w-full text-left my-0.5 px-3 py-2.5 rounded-lg transition-all duration-200
${isSelected ${isSelected
? "bg-accent-light shadow-glow-accent ring-1 ring-accent/25" ? "bg-accent-light shadow-glow-accent ring-1 ring-accent/25"
: "hover:bg-surface-overlay" : "hover:bg-surface-overlay"
} }
`} `}
style={{ width: "calc(100% - 1rem)", animationDelay: `${idx * 30}ms` }} style={{ animationDelay: `${idx * 30}ms` }}
> >
<div className={`text-body font-medium truncate ${isSelected ? "text-accent-dark" : "text-foreground"}`}> <div className={`text-body font-medium truncate ${isSelected ? "text-accent-dark" : "text-foreground"}`}>
{session.summary || session.firstPrompt || "Untitled Session"} {session.summary || session.firstPrompt || "Untitled Session"}
@@ -113,7 +116,7 @@ export function SessionList({ sessions, loading, selectedId, onSelect }: Props)
// Project list // Project list
return ( return (
<div className="py-1 animate-fade-in"> <div className="py-1 px-2 animate-fade-in">
{[...grouped.entries()].map(([project, projectSessions]) => { {[...grouped.entries()].map(([project, projectSessions]) => {
const latest = projectSessions.reduce((a, b) => const latest = projectSessions.reduce((a, b) =>
(a.modified || a.created) > (b.modified || b.created) ? a : b (a.modified || a.created) > (b.modified || b.created) ? a : b
@@ -123,8 +126,7 @@ export function SessionList({ sessions, loading, selectedId, onSelect }: Props)
<button <button
key={project} key={project}
onClick={() => setSelectedProject(project)} onClick={() => setSelectedProject(project)}
className="w-full text-left mx-2 my-0.5 px-3 py-2.5 rounded-lg hover:bg-surface-overlay transition-all duration-200 group" className="w-full text-left my-0.5 px-3 py-2.5 rounded-lg hover:bg-surface-overlay transition-all duration-200 group"
style={{ width: "calc(100% - 1rem)" }}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-body font-medium text-foreground truncate"> <div className="text-body font-medium text-foreground truncate">

View File

@@ -68,7 +68,7 @@ export function Tooltip({ content, children, delayMs = 150, side = "top" }: Prop
style={{ style={{
position: "fixed", position: "fixed",
left: position.x, left: position.x,
top: side === "top" ? position.y - 8 : position.y + 8, top: side === "top" ? position.y - 12 : position.y + 12,
zIndex: 9999, zIndex: 9999,
}} }}
data-side={side} data-side={side}

View File

@@ -247,7 +247,7 @@ mark.search-highlight {
position: absolute; position: absolute;
right: 0; right: 0;
width: 8px; width: 8px;
height: 4px; height: 3px;
background: rgba(254, 240, 138, 0.7); background: rgba(254, 240, 138, 0.7);
border: none; border: none;
padding: 0; padding: 0;
@@ -362,7 +362,7 @@ mark.search-highlight {
.prose-message p { .prose-message p {
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
line-height: 1.625; line-height: 1.6;
} }
.prose-message p:first-child { .prose-message p:first-child {
@@ -383,7 +383,7 @@ mark.search-highlight {
.prose-message li { .prose-message li {
margin-top: 0.25rem; margin-top: 0.25rem;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
line-height: 1.5; line-height: 1.6;
} }
.prose-message code:not(pre code) { .prose-message code:not(pre code) {
@@ -445,6 +445,116 @@ mark.search-highlight {
font-weight: 600; font-weight: 600;
} }
/* ═══════════════════════════════════════════════
Progress markdown (compact variant)
═══════════════════════════════════════════════ */
.prose-message-progress {
font-family: "JetBrains Mono", "Fira Code", "SF Mono", ui-monospace, monospace;
font-size: 11px;
line-height: 1.5;
color: var(--color-foreground-secondary);
word-break: break-word;
}
.prose-message-progress h1,
.prose-message-progress h2,
.prose-message-progress h3,
.prose-message-progress h4,
.prose-message-progress h5,
.prose-message-progress h6 {
font-size: 12px;
font-weight: 600;
color: var(--color-foreground);
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
.prose-message-progress p {
margin-top: 0.125rem;
margin-bottom: 0.125rem;
}
.prose-message-progress p:first-child {
margin-top: 0;
}
.prose-message-progress p:last-child {
margin-bottom: 0;
}
.prose-message-progress ul,
.prose-message-progress ol {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
padding-left: 1rem;
}
.prose-message-progress li {
margin-top: 0.125rem;
margin-bottom: 0.125rem;
line-height: 1.5;
}
.prose-message-progress code:not(pre code) {
font-family: inherit;
font-size: 0.9em;
padding: 0.0625rem 0.25rem;
border-radius: 0.1875rem;
background: var(--color-surface-inset);
border: 1px solid var(--color-border-muted);
color: #c4a1ff;
font-weight: 500;
}
.prose-message-progress pre {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
max-width: 100%;
overflow-x: auto;
}
.prose-message-progress blockquote {
border-left: 2px solid var(--color-border);
padding-left: 0.5rem;
color: var(--color-foreground-secondary);
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
.prose-message-progress a {
color: var(--color-accent);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-color: rgba(91, 156, 245, 0.3);
transition: text-decoration-color 150ms;
}
.prose-message-progress a:hover {
color: var(--color-accent-dark);
text-decoration-color: rgba(125, 180, 255, 0.6);
}
.prose-message-progress table {
width: 100%;
border-collapse: collapse;
margin-top: 0.375rem;
margin-bottom: 0.375rem;
font-size: 11px;
}
.prose-message-progress th,
.prose-message-progress td {
border: 1px solid var(--color-border-muted);
padding: 0.25rem 0.5rem;
text-align: left;
}
.prose-message-progress th {
background: var(--color-surface-inset);
font-weight: 600;
}
/* ═══════════════════════════════════════════════ /* ═══════════════════════════════════════════════
Focus ring system Focus ring system
═══════════════════════════════════════════════ */ ═══════════════════════════════════════════════ */
@@ -481,10 +591,10 @@ mark.search-highlight {
.custom-checkbox:checked::after { .custom-checkbox:checked::after {
content: ""; content: "";
position: absolute; position: absolute;
top: 1px; top: 6.25%;
left: 4px; left: 25%;
width: 5px; width: 31.25%;
height: 9px; height: 56.25%;
border: solid #0c1017; border: solid #0c1017;
border-width: 0 2px 2px 0; border-width: 0 2px 2px 0;
transform: rotate(45deg); transform: rotate(45deg);