mirror of
https://github.com/hoornet/vega.git
synced 2026-06-08 14:11:55 -07:00
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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}`}
|
||||
>
|
||||
▢▢
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user