Polish pass — consistency, a11y, theme correctness

- Fix bg-white toggle thumbs in Settings (broke on dark themes)
- Eliminate 2px layout shift when switching Media feed tabs
- Unify "Follow"/"Mute" capitalization in NoteCard context menu
- Replace ASCII "..." with unicode ellipsis across compose/search/article
- Add rounded-sm to dropdowns, emoji picker, post/reply buttons
- Add aria-labels to sidebar toggle and onboarding copy buttons
- Add role=tablist/tab/aria-selected to login mode tabs
- Replace inline width/height styles with Tailwind w-16 h-16 in ProfileView
- Replace inline transform style with rotate-90 class in SettingsView
- Unify sidebar active state opacity (bg-accent/8 → bg-accent/10)
- Pad sidebar badges with py-0.5 for consistent pill height
- Match thread reply button sizing to compose post button
- Use var(--font-reading) on non-zen article title for consistency
- Format 'saved Xs ago' as minutes/hours after 60s
- Unify expand chevron to ▶ + rotate-90 pattern
- PollWidget: transition-all → transition-colors (no layout animation)
- Remove cryptic 'Ctrl+Enter' hint from compose and thread reply
This commit is contained in:
Jure
2026-04-09 13:32:59 +02:00
parent acd5a5979b
commit d134702da7
15 changed files with 49 additions and 35 deletions
+12 -1
View File
@@ -20,6 +20,16 @@ function extractImages(md: string): { alt: string; url: string }[] {
return images;
}
function formatSavedAgo(elapsedMs: number): string {
const s = Math.floor(elapsedMs / 1000);
if (s < 5) return "just now";
if (s < 60) return `${s}s ago`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m ago`;
const h = Math.floor(m / 60);
return `${h}h ago`;
}
export function ArticleEditor() {
const { goBack } = useUIStore();
const { activeDraftId, drafts, updateDraft, deleteDraft, setActiveDraft, createDraft } = useDraftStore();
@@ -279,7 +289,7 @@ export function ArticleEditor() {
<span className="text-text-dim text-[10px]">{wordCount > 0 ? `${wordCount} words` : "New article"}</span>
{activeDraft && !published && lastSaved && (
<span className="text-text-dim text-[10px]">
· saved {Math.floor((Date.now() - lastSaved) / 1000) < 5 ? "just now" : `${Math.floor((Date.now() - lastSaved) / 1000)}s ago`}
· saved {formatSavedAgo(Date.now() - lastSaved)}
</span>
)}
{published && publishedRelays > 0 && (
@@ -396,6 +406,7 @@ export function ArticleEditor() {
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
className="w-full bg-transparent text-text text-2xl font-bold placeholder:text-text-dim focus:outline-none"
style={{ fontFamily: "var(--font-reading)" }}
/>
</div>
+1 -1
View File
@@ -306,7 +306,7 @@ export function ArticleView() {
value={commentText}
onChange={(e) => { setCommentText(e.target.value); autoResize(e); }}
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleComment(); } }}
placeholder="Write a comment about this article..."
placeholder="Write a comment"
rows={3}
className="w-full bg-bg-raised border border-border rounded-sm px-3 py-2 text-[12px] text-text placeholder:text-text-dim resize-none focus:outline-none focus:border-accent leading-relaxed"
autoFocus
+3 -4
View File
@@ -270,7 +270,7 @@ export function ComposeBox({ onPublished, onNoteInjected }: { onPublished?: () =
)}
<button
onClick={() => removeAttachment(i)}
className="absolute -top-1.5 -right-1.5 w-4 h-4 bg-danger text-accent-text text-[10px] rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
className="absolute -top-1.5 -right-1.5 w-4 h-4 bg-danger text-white text-[10px] rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
title="Remove"
>
x
@@ -328,15 +328,14 @@ export function ComposeBox({ onPublished, onNoteInjected }: { onPublished?: () =
<button
onClick={() => setIsPoll((v) => !v)}
title={isPoll ? "Cancel poll" : "Create poll"}
className={`text-[14px] transition-colors ${isPoll ? "text-accent" : "text-text-dim hover:text-text"}`}
className={`text-[16px] transition-colors ${isPoll ? "text-accent" : "text-text-dim hover:text-text"}`}
>
&#9634;&#9634;
</button>
<span className="text-text-dim text-[10px]">Ctrl+Enter to post</span>
<button
onClick={handlePublish}
disabled={!canPost}
className="px-3 py-1 text-[11px] bg-accent hover:bg-accent-hover text-accent-text transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
className="px-3 py-1 text-[11px] bg-accent hover:bg-accent-hover text-accent-text rounded-sm transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
{publishing ? "posting…" : isPoll ? "post poll" : "post"}
</button>
+3 -2
View File
@@ -116,6 +116,7 @@ export function NoteActions({ event, onReplyToggle, showReply }: NoteActionsProp
disabled={reacting}
className="inline-flex items-center px-1.5 py-0.5 text-[12px] text-text-dim hover:text-accent border border-border hover:border-accent/40 rounded-sm transition-colors disabled:opacity-30"
title="React with emoji"
aria-label="React with emoji"
>
+
</button>
@@ -125,7 +126,7 @@ export function NoteActions({ event, onReplyToggle, showReply }: NoteActionsProp
{showEmojiPicker && (
<>
<div className="fixed inset-0 z-[9]" role="presentation" onClick={() => setShowEmojiPicker(false)} />
<div className="absolute bottom-6 left-0 bg-bg-raised border border-border shadow-lg z-10 flex gap-0.5 px-1.5 py-1">
<div className="absolute bottom-6 left-0 bg-bg-raised border border-border rounded-sm shadow-lg z-10 flex gap-0.5 px-1.5 py-1">
{REACTION_EMOJIS.map((emoji) => (
<button
key={emoji}
@@ -244,7 +245,7 @@ export function LoggedOutStats({ event }: { event: NDKEvent }) {
: [];
return (
<div className="flex flex-wrap items-center gap-2 mt-1.5">
<div className="flex flex-wrap items-center gap-2 mt-2">
{replyCount !== null && replyCount > 0 && (
<span className="text-text-dim text-[11px]"> {replyCount}</span>
)}
+3 -3
View File
@@ -121,18 +121,18 @@ export const NoteCard = memo(function NoteCard({ event, focused, onReplyInThread
{menuOpen && (
<>
<div className="fixed inset-0 z-[9]" role="presentation" onClick={() => setMenuOpen(false)} />
<div role="menu" className="absolute right-0 top-5 bg-bg-raised border border-border shadow-lg z-10 w-32">
<div role="menu" className="absolute right-0 top-5 bg-bg-raised border border-border rounded-sm shadow-lg z-10 w-32">
<button
onClick={() => { setMenuOpen(false); follows.includes(event.pubkey) ? unfollow(event.pubkey) : follow(event.pubkey); }}
className="w-full text-left px-3 py-2 text-[11px] text-text-muted hover:text-accent hover:bg-bg-hover transition-colors"
>
{follows.includes(event.pubkey) ? `unfollow` : `follow`}
{follows.includes(event.pubkey) ? "Unfollow" : "Follow"}
</button>
<button
onClick={() => { setMenuOpen(false); isMuted ? unmute(event.pubkey) : mute(event.pubkey); }}
className="w-full text-left px-3 py-2 text-[11px] text-text-muted hover:text-danger hover:bg-bg-hover transition-colors"
>
{isMuted ? `unmute` : `mute`}
{isMuted ? "Unmute" : "Mute"}
</button>
</div>
</>
+3 -3
View File
@@ -51,10 +51,10 @@ export function MediaFeed() {
<button
key={t}
onClick={() => setTab(t)}
className={`px-3 py-1 text-[12px] transition-colors ${
className={`px-3 py-1 text-[12px] transition-colors border-b-2 ${
tab === t
? "text-text border-b-2 border-accent"
: "text-text-muted hover:text-text"
? "text-text border-accent"
: "text-text-muted hover:text-text border-transparent"
}`}
>
{t.charAt(0).toUpperCase() + t.slice(1)}
+5 -1
View File
@@ -91,6 +91,7 @@ function CreateStep({ onNext }: { onNext: (signer: NDKPrivateKeySigner) => void
<span className="text-text font-mono text-[11px] truncate flex-1 select-all">{signer.npub}</span>
<button
onClick={handleCopy}
aria-label="Copy public key"
className="text-[10px] text-text-dim hover:text-accent transition-colors shrink-0"
>
{copied ? "copied ✓" : "copy"}
@@ -158,6 +159,7 @@ function BackupStep({ signer, onComplete }: { signer: NDKPrivateKeySigner; onCom
</button>
<button
onClick={handleCopy}
aria-label="Copy secret key"
className="text-[10px] text-text-dim hover:text-danger transition-colors shrink-0"
>
{copied ? "copied ✓" : "copy"}
@@ -229,10 +231,12 @@ function LoginStep({ onBack, onComplete }: { onBack: () => void; onComplete: ()
<Shell>
<Heading>Log in with your key.</Heading>
<div className="flex border border-border mb-4">
<div role="tablist" className="flex border border-border mb-4">
{(["nsec", "npub", "bunker"] as const).map((m) => (
<button
key={m}
role="tab"
aria-selected={mode === m}
onClick={() => { setMode(m); setValue(""); }}
className={`flex-1 py-2 text-[11px] transition-colors ${
mode === m ? "bg-accent/10 text-accent" : "text-text-dim hover:text-text"
+1 -1
View File
@@ -52,7 +52,7 @@ export function PollCompose({ options, onChange }: PollComposeProps) {
onClick={addOption}
className="text-accent hover:text-accent-hover text-[11px] transition-colors"
>
Add option
+ Add option
</button>
)}
</div>
+1 -1
View File
@@ -63,7 +63,7 @@ export const PollWidget = memo(function PollWidget({ event }: { event: NDKEvent
disabled={showResults}
className={`
relative w-full text-left px-3 py-2 rounded-sm overflow-hidden
transition-all duration-200
transition-colors duration-200
${showResults
? isMyVote
? "border border-accent/60"
+3 -5
View File
@@ -229,19 +229,17 @@ export function ProfileView() {
{/* Avatar + info */}
<div className="px-4 py-4 flex gap-4 items-start">
<div className="shrink-0" style={{ width: 64, height: 64 }}>
<div className="shrink-0 w-16 h-16">
{avatar ? (
<img
src={avatar}
alt={`${name}'s avatar`}
style={{ width: 64, height: 64 }}
className="rounded-sm object-cover bg-bg-raised"
className="w-16 h-16 rounded-sm object-cover bg-bg-raised"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
/>
) : (
<div
style={{ width: 64, height: 64 }}
className="rounded-sm bg-bg-raised border border-border flex items-center justify-center text-text-dim text-lg"
className="w-16 h-16 rounded-sm bg-bg-raised border border-border flex items-center justify-center text-text-dim text-lg"
>
{name.charAt(0).toUpperCase()}
</div>
+2 -2
View File
@@ -117,7 +117,7 @@ function SuggestionFollowButton({ pubkey }: { pubkey: string }) {
: "border-accent/60 text-accent hover:bg-accent hover:text-accent-text"
}`}
>
{pending ? "..." : isFollowing ? "unfollow" : "follow"}
{pending ? "" : isFollowing ? "Unfollow" : "Follow"}
</button>
);
}
@@ -410,7 +410,7 @@ export function SearchView() {
<p className="text-text-dim text-[10px] mt-0.5">Based on who your follows follow</p>
</div>
{suggestionsLoading && (
<div className="px-4 py-6 text-text-dim text-[11px] text-center">Finding suggestions...</div>
<div className="px-4 py-6 text-text-dim text-[11px] text-center">Finding suggestions</div>
)}
{visibleSuggestions.map((s) => s.profile && (
<div key={s.pubkey} className="flex items-center gap-3 px-4 py-2.5 border-b border-border hover:bg-bg-hover transition-colors group/suggestion">
+1 -1
View File
@@ -80,7 +80,7 @@ function RelayHealthCard({ result, poolConnected, onRemove }: { result: RelayHea
>
remove
</button>
<span className="text-text-dim text-[10px]">{expanded ? "▾" : "▸"}</span>
<span className={`text-text-dim text-[10px] transition-transform ${expanded ? "rotate-90" : "rotate-0"}`}></span>
</div>
</div>
+3 -3
View File
@@ -50,7 +50,7 @@ function MuteSection() {
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-2 w-full text-left group"
>
<span className="text-text-dim text-[10px] transition-transform" style={{ transform: expanded ? "rotate(90deg)" : "rotate(0deg)" }}>
<span className={`text-text-dim text-[10px] transition-transform ${expanded ? "rotate-90" : "rotate-0"}`}>
</span>
<h2 className="text-text text-[11px] font-medium uppercase tracking-widest text-text-dim group-hover:text-text transition-colors">
@@ -280,7 +280,7 @@ function NotificationSection() {
}`}
>
<span
className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-bg transition-transform ${
settings[key] ? "translate-x-4" : "translate-x-0"
}`}
/>
@@ -412,7 +412,7 @@ function ExperimentalSection() {
}`}
>
<span
className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-bg transition-transform ${
enabled ? "translate-x-4" : "translate-x-0"
}`}
/>
+5 -3
View File
@@ -46,6 +46,7 @@ export function Sidebar() {
<button
onClick={toggleSidebar}
title="Expand sidebar"
aria-label="Expand sidebar"
className="w-full flex items-center justify-center text-text-dim hover:text-accent transition-colors"
>
<span className="text-[13px]"></span>
@@ -60,6 +61,7 @@ export function Sidebar() {
<button
onClick={toggleSidebar}
title="Collapse sidebar"
aria-label="Collapse sidebar"
className="text-text-dim hover:text-accent transition-colors px-1"
>
<span className="text-[13px]"></span>
@@ -89,7 +91,7 @@ export function Sidebar() {
</span>
{!c && <span>write article</span>}
{!c && draftCount > 0 && (
<span className="ml-auto text-[10px] bg-accent/20 text-accent px-1 rounded-sm">{draftCount}</span>
<span className="ml-auto text-[10px] bg-accent/20 text-accent px-1.5 py-0.5 rounded-sm">{draftCount}</span>
)}
</button>
)}
@@ -103,7 +105,7 @@ export function Sidebar() {
title={c ? item.label : undefined}
className={`w-full text-left px-3 py-1.5 flex items-center gap-2 text-[12px] transition-colors ${
currentView === item.id
? "text-accent bg-accent/8"
? "text-accent bg-accent/10"
: "text-text-muted hover:text-text hover:bg-bg-hover"
}`}
>
@@ -115,7 +117,7 @@ export function Sidebar() {
</span>
{!c && <span>{item.label}</span>}
{!c && badge > 0 && (
<span className="ml-auto text-[10px] bg-accent/20 text-accent px-1 rounded-sm">{badge}</span>
<span className="ml-auto text-[10px] bg-accent/20 text-accent px-1.5 py-0.5 rounded-sm">{badge}</span>
)}
</button>
);
+3 -4
View File
@@ -57,7 +57,7 @@ function InlineThreadReply({ replyTo, rootEvent, onPublished }: {
};
return (
<div className="border-l-2 border-accent/40 ml-3 pl-3 py-2">
<div className="border-l-2 border-accent/40 ml-4 pl-3 py-2">
<div className="text-text-dim text-[10px] mb-1">replying to <span className="text-accent">@{name}</span></div>
<textarea
ref={ref}
@@ -95,13 +95,12 @@ function InlineThreadReply({ replyTo, rootEvent, onPublished }: {
/>
)}
</div>
<span className="text-text-dim text-[10px]">Ctrl+Enter</span>
<button
onClick={handleSubmit}
disabled={!text.trim() || replying}
className="px-2 py-0.5 text-[10px] bg-accent hover:bg-accent-hover text-accent-text transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
className="px-3 py-1 text-[11px] bg-accent hover:bg-accent-hover text-accent-text rounded-sm transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
{sent ? "replied ✓" : replying ? "posting..." : "reply"}
{sent ? "replied ✓" : replying ? "posting" : "reply"}
</button>
</div>
</div>