mirror of
https://github.com/hoornet/vega.git
synced 2026-05-07 04:39:12 -07:00
Bump to v0.9.3 — color themes, font size, settings polish
7 color themes (Midnight, Light, Catppuccin Mocha, Tokyo Night, Gruvbox, Ethereal, Hackerman), font size presets (Small/Normal/Large/Extra Large), collapsible muted accounts list, removed old sidebar connection indicator.
This commit is contained in:
14
src/App.tsx
14
src/App.tsx
@@ -24,6 +24,7 @@ import { ToastContainer } from "./components/shared/ToastContainer";
|
||||
import { DebugPanel } from "./components/shared/DebugPanel";
|
||||
import { HelpModal } from "./components/shared/HelpModal";
|
||||
import { useUIStore } from "./stores/ui";
|
||||
import { getTheme, applyTheme } from "./lib/themes";
|
||||
import { useUpdater } from "./hooks/useUpdater";
|
||||
import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
|
||||
|
||||
@@ -56,12 +57,25 @@ function App() {
|
||||
const toggleHelp = useUIStore((s) => s.toggleHelp);
|
||||
const showDebugPanel = useUIStore((s) => s.showDebugPanel);
|
||||
const toggleDebugPanel = useUIStore((s) => s.toggleDebugPanel);
|
||||
const fontSize = useUIStore((s) => s.fontSize);
|
||||
const themeId = useUIStore((s) => s.themeId);
|
||||
const [onboardingDone, setOnboardingDone] = useState(
|
||||
() => !!localStorage.getItem("wrystr_pubkey")
|
||||
);
|
||||
|
||||
useKeyboardShortcuts();
|
||||
|
||||
// Apply zoom level based on font size setting
|
||||
useEffect(() => {
|
||||
document.documentElement.style.zoom = `${fontSize / 14}`;
|
||||
}, [fontSize]);
|
||||
|
||||
// Apply color theme
|
||||
useEffect(() => {
|
||||
const theme = getTheme(themeId);
|
||||
if (theme) applyTheme(theme);
|
||||
}, [themeId]);
|
||||
|
||||
// Intercept external link clicks and open in system browser via Tauri opener
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState } from "react";
|
||||
import { save } from "@tauri-apps/plugin-dialog";
|
||||
import { writeTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useUIStore } from "../../stores/ui";
|
||||
import { themes } from "../../lib/themes";
|
||||
import { useMuteStore } from "../../stores/mute";
|
||||
import { useBookmarkStore } from "../../stores/bookmark";
|
||||
import { getStoredRelayUrls } from "../../lib/nostr";
|
||||
@@ -30,17 +32,28 @@ function MutedRow({ pubkey, onUnmute }: { pubkey: string; onUnmute: () => void }
|
||||
|
||||
function MuteSection() {
|
||||
const { mutedPubkeys, unmute } = useMuteStore();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
if (mutedPubkeys.length === 0) return null;
|
||||
return (
|
||||
<section>
|
||||
<h2 className="text-text text-[11px] font-medium uppercase tracking-widest mb-2 text-text-dim">
|
||||
Muted accounts ({mutedPubkeys.length})
|
||||
</h2>
|
||||
<div className="space-y-1">
|
||||
{mutedPubkeys.map((pk) => (
|
||||
<MutedRow key={pk} pubkey={pk} onUnmute={() => unmute(pk)} />
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
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>
|
||||
<h2 className="text-text text-[11px] font-medium uppercase tracking-widest text-text-dim group-hover:text-text transition-colors">
|
||||
Muted accounts ({mutedPubkeys.length})
|
||||
</h2>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="space-y-1 mt-2">
|
||||
{mutedPubkeys.map((pk) => (
|
||||
<MutedRow key={pk} pubkey={pk} onUnmute={() => unmute(pk)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -270,6 +283,78 @@ function NotificationSection() {
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeSection() {
|
||||
const { themeId, setTheme } = useUIStore();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className="text-text text-[11px] font-medium uppercase tracking-widest mb-3 text-text-dim">
|
||||
Theme
|
||||
</h2>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{themes.map((theme) => (
|
||||
<button
|
||||
key={theme.id}
|
||||
onClick={() => setTheme(theme.id)}
|
||||
className={`flex flex-col items-center gap-1.5 p-2 border transition-colors rounded-sm ${
|
||||
themeId === theme.id
|
||||
? "border-accent bg-bg-hover"
|
||||
: "border-border hover:border-accent/40"
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-0.5 w-full h-5 rounded-sm overflow-hidden">
|
||||
<div className="flex-1" style={{ background: theme.colors.bg }} />
|
||||
<div className="flex-1" style={{ background: theme.colors["bg-raised"] }} />
|
||||
<div className="flex-1" style={{ background: theme.colors.accent }} />
|
||||
<div className="flex-1" style={{ background: theme.colors.text }} />
|
||||
</div>
|
||||
<span className="text-[10px] text-text-muted truncate w-full text-center">
|
||||
{theme.name}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const FONT_PRESETS = [
|
||||
{ label: "Small", size: 12 },
|
||||
{ label: "Normal", size: 14 },
|
||||
{ label: "Large", size: 16 },
|
||||
{ label: "Extra Large", size: 18 },
|
||||
];
|
||||
|
||||
function FontSizeSection() {
|
||||
const { fontSize, setFontSize } = useUIStore();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className="text-text text-[11px] font-medium uppercase tracking-widest mb-2 text-text-dim">
|
||||
Font Size
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{FONT_PRESETS.map(({ label, size }) => (
|
||||
<button
|
||||
key={size}
|
||||
onClick={() => setFontSize(size)}
|
||||
className={`px-3 py-1.5 text-[11px] border transition-colors ${
|
||||
fontSize === size
|
||||
? "border-accent text-accent"
|
||||
: "border-border text-text-muted hover:text-text hover:border-accent/40"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-text-dim text-[10px] mt-2 px-1">
|
||||
Adjusts the base text size across the app. Articles use their own reading font.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsView() {
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
@@ -278,6 +363,8 @@ export function SettingsView() {
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-8">
|
||||
<ThemeSection />
|
||||
<FontSizeSection />
|
||||
<WalletSection />
|
||||
<NotificationSection />
|
||||
<ExportSection />
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useUIStore } from "../../stores/ui";
|
||||
import { useFeedStore } from "../../stores/feed";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useNotificationsStore } from "../../stores/notifications";
|
||||
import { useDraftStore } from "../../stores/drafts";
|
||||
@@ -25,7 +24,6 @@ const NAV_ITEMS = [
|
||||
|
||||
export function Sidebar() {
|
||||
const { currentView, setView, sidebarCollapsed, toggleSidebar } = useUIStore();
|
||||
const { connected } = useFeedStore();
|
||||
const { loggedIn } = useUserStore();
|
||||
const { unreadCount: notifUnread, dmUnreadCount } = useNotificationsStore();
|
||||
const draftCount = useDraftStore((s) => s.drafts.length);
|
||||
@@ -125,22 +123,6 @@ export function Sidebar() {
|
||||
{/* Account switcher (full) — expanded only */}
|
||||
{!c && <AccountSwitcher />}
|
||||
|
||||
{/* Footer — connection status */}
|
||||
<div className={`border-t border-border shrink-0 ${c ? "py-2 flex justify-center" : "px-3 py-2"}`}>
|
||||
{c ? (
|
||||
/* Collapsed: single dot */
|
||||
<span
|
||||
title={connected ? "Online" : "Offline"}
|
||||
className={`w-2 h-2 rounded-full inline-block ${connected ? "bg-success" : "bg-danger"}`}
|
||||
/>
|
||||
) : (
|
||||
/* Expanded: dot + label */
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-text-dim">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${connected ? "bg-success" : "bg-danger"}`} />
|
||||
<span>{connected ? "online" : "offline"}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
178
src/lib/themes.ts
Normal file
178
src/lib/themes.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
interface ThemeColors {
|
||||
bg: string;
|
||||
"bg-raised": string;
|
||||
"bg-hover": string;
|
||||
border: string;
|
||||
"border-subtle": string;
|
||||
text: string;
|
||||
"text-muted": string;
|
||||
"text-dim": string;
|
||||
accent: string;
|
||||
"accent-hover": string;
|
||||
zap: string;
|
||||
danger: string;
|
||||
warning: string;
|
||||
success: string;
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
id: string;
|
||||
name: string;
|
||||
colors: ThemeColors;
|
||||
}
|
||||
|
||||
export const themes: Theme[] = [
|
||||
{
|
||||
id: "midnight",
|
||||
name: "Midnight",
|
||||
colors: {
|
||||
bg: "#0a0a0a",
|
||||
"bg-raised": "#111111",
|
||||
"bg-hover": "#1a1a1a",
|
||||
border: "#222222",
|
||||
"border-subtle": "#1a1a1a",
|
||||
text: "#e0e0e0",
|
||||
"text-muted": "#777777",
|
||||
"text-dim": "#555555",
|
||||
accent: "#8b5cf6",
|
||||
"accent-hover": "#7c3aed",
|
||||
zap: "#f59e0b",
|
||||
danger: "#ef4444",
|
||||
warning: "#f59e0b",
|
||||
success: "#22c55e",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "light",
|
||||
name: "Light",
|
||||
colors: {
|
||||
bg: "#f5f5f5",
|
||||
"bg-raised": "#ffffff",
|
||||
"bg-hover": "#e8e8e8",
|
||||
border: "#d4d4d4",
|
||||
"border-subtle": "#e5e5e5",
|
||||
text: "#1a1a1a",
|
||||
"text-muted": "#6b7280",
|
||||
"text-dim": "#9ca3af",
|
||||
accent: "#7c3aed",
|
||||
"accent-hover": "#6d28d9",
|
||||
zap: "#d97706",
|
||||
danger: "#dc2626",
|
||||
warning: "#d97706",
|
||||
success: "#16a34a",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "catppuccin",
|
||||
name: "Catppuccin Mocha",
|
||||
colors: {
|
||||
bg: "#1e1e2e",
|
||||
"bg-raised": "#313244",
|
||||
"bg-hover": "#45475a",
|
||||
border: "#45475a",
|
||||
"border-subtle": "#313244",
|
||||
text: "#cdd6f4",
|
||||
"text-muted": "#a6adc8",
|
||||
"text-dim": "#6c7086",
|
||||
accent: "#cba6f7",
|
||||
"accent-hover": "#b4befe",
|
||||
zap: "#f9e2af",
|
||||
danger: "#f38ba8",
|
||||
warning: "#f9e2af",
|
||||
success: "#a6e3a1",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "tokyo-night",
|
||||
name: "Tokyo Night",
|
||||
colors: {
|
||||
bg: "#1a1b26",
|
||||
"bg-raised": "#24283b",
|
||||
"bg-hover": "#292e42",
|
||||
border: "#3b4261",
|
||||
"border-subtle": "#292e42",
|
||||
text: "#a9b1d6",
|
||||
"text-muted": "#565f89",
|
||||
"text-dim": "#3b4261",
|
||||
accent: "#7aa2f7",
|
||||
"accent-hover": "#89b4fa",
|
||||
zap: "#e0af68",
|
||||
danger: "#f7768e",
|
||||
warning: "#e0af68",
|
||||
success: "#9ece6a",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "gruvbox",
|
||||
name: "Gruvbox",
|
||||
colors: {
|
||||
bg: "#282828",
|
||||
"bg-raised": "#3c3836",
|
||||
"bg-hover": "#504945",
|
||||
border: "#504945",
|
||||
"border-subtle": "#3c3836",
|
||||
text: "#ebdbb2",
|
||||
"text-muted": "#a89984",
|
||||
"text-dim": "#665c54",
|
||||
accent: "#fe8019",
|
||||
"accent-hover": "#d65d0e",
|
||||
zap: "#fabd2f",
|
||||
danger: "#fb4934",
|
||||
warning: "#fabd2f",
|
||||
success: "#b8bb26",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ethereal",
|
||||
name: "Ethereal",
|
||||
colors: {
|
||||
bg: "#1a1a2e",
|
||||
"bg-raised": "#16213e",
|
||||
"bg-hover": "#1f2f50",
|
||||
border: "#2a3a5c",
|
||||
"border-subtle": "#1f2f50",
|
||||
text: "#dfe6e9",
|
||||
"text-muted": "#a0aec0",
|
||||
"text-dim": "#5a6a8a",
|
||||
accent: "#a29bfe",
|
||||
"accent-hover": "#6c5ce7",
|
||||
zap: "#ffeaa7",
|
||||
danger: "#ff7675",
|
||||
warning: "#ffeaa7",
|
||||
success: "#55efc4",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "hackerman",
|
||||
name: "Hackerman",
|
||||
colors: {
|
||||
bg: "#0a0a0a",
|
||||
"bg-raised": "#0d1117",
|
||||
"bg-hover": "#161b22",
|
||||
border: "#1a2332",
|
||||
"border-subtle": "#131a24",
|
||||
text: "#00ff41",
|
||||
"text-muted": "#00bb2d",
|
||||
"text-dim": "#006b1a",
|
||||
accent: "#00ff41",
|
||||
"accent-hover": "#33ff66",
|
||||
zap: "#ffff00",
|
||||
danger: "#ff0000",
|
||||
warning: "#ffff00",
|
||||
success: "#00ff41",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_THEME_ID = "midnight";
|
||||
|
||||
export function getTheme(id: string): Theme | undefined {
|
||||
return themes.find((t) => t.id === id);
|
||||
}
|
||||
|
||||
export function applyTheme(theme: Theme): void {
|
||||
const root = document.documentElement;
|
||||
for (const [key, value] of Object.entries(theme.colors)) {
|
||||
root.style.setProperty(`--color-${key}`, value);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ interface UIState {
|
||||
showHelp: boolean;
|
||||
showDebugPanel: boolean;
|
||||
feedLanguageFilter: string | null;
|
||||
fontSize: number;
|
||||
themeId: string;
|
||||
setView: (view: View) => void;
|
||||
setFeedTab: (tab: FeedTab) => void;
|
||||
openProfile: (pubkey: string) => void;
|
||||
@@ -39,12 +41,16 @@ interface UIState {
|
||||
openArticle: (naddr: string, event?: NDKEvent) => void;
|
||||
goBack: () => void;
|
||||
setFeedLanguageFilter: (filter: string | null) => void;
|
||||
setFontSize: (size: number) => void;
|
||||
setTheme: (id: string) => void;
|
||||
toggleSidebar: () => void;
|
||||
toggleHelp: () => void;
|
||||
toggleDebugPanel: () => void;
|
||||
}
|
||||
|
||||
const SIDEBAR_KEY = "wrystr_sidebar_collapsed";
|
||||
const FONT_SIZE_KEY = "wrystr_font_size";
|
||||
const THEME_KEY = "wrystr_theme";
|
||||
|
||||
export const useUIStore = create<UIState>((set, _get) => ({
|
||||
currentView: "feed",
|
||||
@@ -62,6 +68,8 @@ export const useUIStore = create<UIState>((set, _get) => ({
|
||||
showHelp: false,
|
||||
showDebugPanel: false,
|
||||
feedLanguageFilter: null,
|
||||
fontSize: parseInt(localStorage.getItem(FONT_SIZE_KEY) || "14", 10),
|
||||
themeId: localStorage.getItem(THEME_KEY) || "midnight",
|
||||
setView: (currentView) => set({ currentView }),
|
||||
setFeedTab: (feedTab) => set({ feedTab }),
|
||||
openProfile: (pubkey) => set((s) => {
|
||||
@@ -91,6 +99,14 @@ export const useUIStore = create<UIState>((set, _get) => ({
|
||||
return { showHelp: false, currentView: "feed", selectedNote: null, viewStack: [] };
|
||||
}),
|
||||
setFeedLanguageFilter: (feedLanguageFilter) => set({ feedLanguageFilter }),
|
||||
setFontSize: (fontSize) => {
|
||||
localStorage.setItem(FONT_SIZE_KEY, String(fontSize));
|
||||
set({ fontSize });
|
||||
},
|
||||
setTheme: (themeId) => {
|
||||
localStorage.setItem(THEME_KEY, themeId);
|
||||
set({ themeId });
|
||||
},
|
||||
toggleSidebar: () => set((s) => {
|
||||
const next = !s.sidebarCollapsed;
|
||||
localStorage.setItem(SIDEBAR_KEY, String(next));
|
||||
|
||||
Reference in New Issue
Block a user