mirror of
https://github.com/hoornet/vega.git
synced 2026-06-13 08:23:31 -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:
@@ -69,18 +69,15 @@ jobs:
|
||||
|
||||
> **Windows note:** The installer is not yet code-signed. Windows SmartScreen will show an "Unknown publisher" warning — click "More info → Run anyway" to install.
|
||||
|
||||
### New in v0.9.2 — Relay Status, Toasts & Debug Tools
|
||||
Better visibility into what's happening under the hood.
|
||||
- **Relay status badge** — compact "8/12 relays" indicator in feed header with color coding (green/yellow/red); hover for per-relay connection details
|
||||
- **Toast notifications** — transient messages for connection events: "Connection lost", "Back online", "Resetting relay connections", "Relays reconnected"
|
||||
- **Per-tab "last updated" timestamp** — shows how fresh each feed tab is; Global/Following/Trending tracked independently
|
||||
- **Subscription debug panel** — Ctrl+Shift+D toggles a hidden panel showing NDK uptime, live subscription status, per-relay state, feed timestamps, and recent diagnostics log
|
||||
- **Consolidated relay management** — all relay controls (add, remove, health check, publish list, recommendations) now in one Relays view; removed duplicate relay section from Settings
|
||||
- **Per-relay remove button** — hover any relay card to remove it individually
|
||||
- **Full NIP badge display** — relay cards now show all supported NIPs, not just a filtered subset
|
||||
- **Status dot tooltips** — hover relay status dots for explanations (Online, Slow, Offline, Awaiting check)
|
||||
- **Wider scrollbar** — 12px for easier mouse dragging on long threads
|
||||
- **Acknowledgements section** — added to README
|
||||
### New in v0.9.3 — Themes, Font Size & Settings Polish
|
||||
Make Wrystr yours.
|
||||
- **7 color themes** — Midnight (default), Light, Catppuccin Mocha, Tokyo Night, Gruvbox, Ethereal, Hackerman; instant switching from Settings
|
||||
- **Font size presets** — Small / Normal / Large / Extra Large; scales the entire UI uniformly
|
||||
- **Collapsible muted accounts** — muted user list collapsed by default in Settings; click to expand
|
||||
- **Removed old connection indicator** — sidebar offline/online dot removed in favor of the relay status badge in feed header
|
||||
|
||||
### Previous: v0.9.2 — Relay Status, Toasts & Debug Tools
|
||||
- Relay status badge, toast notifications, per-tab timestamps, debug panel (Ctrl+Shift+D), consolidated relay management, wider scrollbar
|
||||
|
||||
### Previous: v0.9.1 — Live Feed & Relay Reliability
|
||||
- Live streaming feed, timeouts on all relay fetches, fixed relay death spiral, NDK subscription hygiene, feed diagnostics, background relay recovery
|
||||
|
||||
@@ -47,6 +47,7 @@ CI triggers on the tag and builds all three platforms (Ubuntu, Windows, macOS AR
|
||||
- `src/App.tsx` — root component; shows `OnboardingFlow` for new users, then view routing via UI store
|
||||
- `src/stores/` — Zustand stores per domain: `feed.ts`, `user.ts`, `ui.ts`, `lightning.ts`, `drafts.ts`, `relayHealth.ts`, `bookmark.ts`, `toast.ts`
|
||||
- `src/lib/nostr/` — NDK wrapper split into domain modules (`core.ts`, `notes.ts`, `social.ts`, `articles.ts`, `engagement.ts`, `dms.ts`, `bookmarks.ts`, `muting.ts`, `search.ts`, `relays.ts`, `trending.ts`); barrel `index.ts` re-exports all; all Nostr calls go through here
|
||||
- `src/lib/themes.ts` — Color theme definitions (7 themes) and `applyTheme()` utility
|
||||
- `src/lib/lightning/` — NWC client (`nwc.ts`); Lightning payment logic
|
||||
- `src/hooks/` — `useProfile.ts`, `useReactionCount.ts`
|
||||
- `src/components/feed/` — Feed, NoteCard, NoteContent, NoteActions, InlineReplyBox, TextSegments, MediaCards, ComposeBox
|
||||
@@ -60,7 +61,7 @@ CI triggers on the tag and builds all three platforms (Ubuntu, Windows, macOS AR
|
||||
- `src/components/media/` — MediaFeed (media discovery with tab filtering)
|
||||
- `src/components/zap/` — ZapModal
|
||||
- `src/components/onboarding/` — OnboardingFlow (welcome, create key, backup, login)
|
||||
- `src/components/shared/` — RelaysView (relay health dashboard + recommendations), SettingsView (NWC + identity + data export), EmojiPicker (categorized emoji insertion)
|
||||
- `src/components/shared/` — RelaysView (relay health dashboard + recommendations), SettingsView (themes + font size + NWC + identity + data export), EmojiPicker (categorized emoji insertion)
|
||||
- `src/components/sidebar/` — Sidebar navigation
|
||||
|
||||
**Backend** (`src-tauri/`): Rust + Tauri 2.0
|
||||
@@ -105,7 +106,7 @@ CI triggers on the tag and builds all three platforms (Ubuntu, Windows, macOS AR
|
||||
- Zaps: NWC wallet connect (NIP-47) + NIP-57 via NDKZapper
|
||||
- **Advanced search** — query parser with modifiers: `by:author`, `mentions:npub`, `kind:N`, `is:article`, `has:image`, `since:date`, `until:date`, `#hashtag`, `"phrase"`, boolean `OR`; NIP-05 resolution; client-side content filters; search help panel
|
||||
- Search: NIP-50 full-text, hashtag (#t filter), people, articles
|
||||
- Settings: NWC wallet, notifications, data export, identity, mute lists
|
||||
- Settings: color themes (7 presets), font size presets, NWC wallet, notifications, data export, identity, mute lists
|
||||
- **Relay management** — consolidated Relays view with add/remove individual relays, health checker (NIP-11 info, WebSocket latency, online/slow/offline status), expandable cards with all supported NIPs, per-relay remove button, "Remove dead" workflow, publish relay list (NIP-65)
|
||||
- **Relay recommendations** — suggest relays based on follows' NIP-65 relay lists; "Discover relays" button with follow count, one-click "Add"
|
||||
- **Relay status badge** — compact "N/M relays" indicator in feed header with color coding; hover tooltip shows per-relay connection state
|
||||
@@ -141,6 +142,8 @@ CI triggers on the tag and builds all three platforms (Ubuntu, Windows, macOS AR
|
||||
- **Profile media gallery** — "Media" tab on profiles with grid layout; images open lightbox, videos/audio navigate to thread
|
||||
- **Emoji picker** — shared categorized emoji picker (Frequent/Faces/Gestures/Objects/Symbols) in compose box, inline reply, thread reply; emoji reaction picker on note cards via visible + button
|
||||
- **External link opener** — global click handler intercepts http(s) links and opens in system browser via `@tauri-apps/plugin-opener`
|
||||
- **Color themes** — 7 built-in themes (Midnight, Light, Catppuccin Mocha, Tokyo Night, Gruvbox, Ethereal, Hackerman); CSS custom properties swapped at runtime; persisted to localStorage
|
||||
- **Font size presets** — Small/Normal/Large/Extra Large; CSS zoom scaling on document root; persisted to localStorage
|
||||
|
||||
**Not yet implemented:**
|
||||
- Web of Trust scoring
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: hoornet <hoornet@users.noreply.github.com>
|
||||
pkgname=wrystr
|
||||
pkgver=0.9.2
|
||||
pkgver=0.9.3
|
||||
pkgrel=1
|
||||
pkgdesc="Cross-platform Nostr desktop client with Lightning integration"
|
||||
arch=('x86_64')
|
||||
|
||||
@@ -90,6 +90,10 @@ sudo dnf install gstreamer1-plugins-base gstreamer1-plugins-good gstreamer1-liba
|
||||
- Search: NIP-50 full-text, `#hashtag`, people search with inline follow, **article search** (kind 30023)
|
||||
- **NIP-05 verification badges** — cached verification with green checkmark on note cards
|
||||
|
||||
**Personalization**
|
||||
- **Color themes** — 7 built-in themes: Midnight (default dark), Light, Catppuccin Mocha, Tokyo Night, Gruvbox, Ethereal, Hackerman; instant switching from Settings
|
||||
- **Font size** — Small / Normal / Large / Extra Large presets; scales the entire UI uniformly
|
||||
|
||||
**Performance & UX**
|
||||
- **Resilient relay connectivity** — all relay queries have timeouts (no more infinite loading); automatic reconnection with NDK instance reset as last resort; toast notifications for connection events; feed diagnostics for debugging
|
||||
- **Per-tab "last updated" timestamp** — relative time indicator in feed header shows how fresh each tab's data is
|
||||
@@ -157,7 +161,6 @@ npm run tauri build # production binary
|
||||
See [ROADMAP.md](./ROADMAP.md) for the full prioritised next steps.
|
||||
|
||||
Up next:
|
||||
- Color themes / light mode
|
||||
- UI polish and visual makeover
|
||||
- Nostr NIP research sprint — expanding protocol support
|
||||
- Web of Trust scoring
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wrystr",
|
||||
"private": true,
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+1
-1
@@ -6157,7 +6157,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wrystr"
|
||||
version = "0.9.1"
|
||||
version = "0.9.3"
|
||||
dependencies = [
|
||||
"keyring",
|
||||
"rusqlite",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "wrystr"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
description = "Cross-platform Nostr desktop client with Lightning integration"
|
||||
authors = ["hoornet"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Wrystr",
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.3",
|
||||
"identifier": "com.hoornet.wrystr",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
+14
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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