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:
Jure
2026-03-24 12:16:08 +01:00
parent 97fd6c55bf
commit ad028c7406
13 changed files with 326 additions and 46 deletions

View File

@@ -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) => {

View File

@@ -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 />

View File

@@ -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
View 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);
}
}

View File

@@ -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));