feat: style editor for blog
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useEffect } from 'react';
|
||||
import { ActivityBar, Sidebar, Editor, StatusBar, Panel, TabBar, ToastContainer, showToast, ResizablePanel, WindowTitleBar } from './components';
|
||||
import { useAppStore, PostData, MediaData, TaskProgress } from './store';
|
||||
import { loadTabsForProject, saveTabsForProject } from './utils';
|
||||
import { ensureRendererPicoThemeStylesheet, getRendererPicoTheme } from './utils/picoTheme';
|
||||
import './App.css';
|
||||
|
||||
const App: React.FC = () => {
|
||||
@@ -22,6 +23,7 @@ const App: React.FC = () => {
|
||||
setActiveView,
|
||||
setSelectedPost,
|
||||
setActiveProject,
|
||||
setPicoTheme,
|
||||
openTab,
|
||||
restoreTabState,
|
||||
} = useAppStore();
|
||||
@@ -35,6 +37,11 @@ const App: React.FC = () => {
|
||||
const activeProject = await window.electronAPI?.projects.getActive();
|
||||
if (activeProject) {
|
||||
setActiveProject(activeProject as import('./store').ProjectData);
|
||||
|
||||
const metadata = await window.electronAPI?.meta.getProjectMetadata();
|
||||
setPicoTheme(metadata?.picoTheme);
|
||||
const resolvedTheme = getRendererPicoTheme(metadata?.picoTheme);
|
||||
await ensureRendererPicoThemeStylesheet(resolvedTheme);
|
||||
}
|
||||
|
||||
// Load posts (now with correct project context, limited to 500)
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import Markdown from 'marked-react';
|
||||
import documentationContent from '../../../../DOCUMENTATION.md?raw';
|
||||
import '@picocss/pico/css/pico.conditional.slate.min.css';
|
||||
import { useAppStore } from '../../store';
|
||||
import { ensureRendererPicoThemeStylesheet, getRendererPicoTheme } from '../../utils/picoTheme';
|
||||
import './DocumentationView.css';
|
||||
|
||||
export const DocumentationView: React.FC = () => {
|
||||
const { picoTheme } = useAppStore();
|
||||
const resolvedTheme = getRendererPicoTheme(picoTheme);
|
||||
|
||||
useEffect(() => {
|
||||
ensureRendererPicoThemeStylesheet(resolvedTheme).catch((error) => {
|
||||
console.error('Failed to load documentation theme stylesheet:', error);
|
||||
});
|
||||
}, [resolvedTheme]);
|
||||
|
||||
return (
|
||||
<div className="documentation-view">
|
||||
<div className="documentation-header">
|
||||
@@ -12,7 +22,7 @@ export const DocumentationView: React.FC = () => {
|
||||
<p>User guide for this installed bDS version.</p>
|
||||
</div>
|
||||
<main className="documentation-scroll">
|
||||
<div className="documentation-content markdown-body pico" data-theme="auto">
|
||||
<div className="documentation-content markdown-body pico" data-theme="auto" data-pico-theme={resolvedTheme}>
|
||||
<article className="documentation-article">
|
||||
<Markdown>{documentationContent}</Markdown>
|
||||
</article>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { LinkedMediaPanel } from '../LinkedMediaPanel';
|
||||
import { ErrorModal } from '../ErrorModal';
|
||||
import { ConfirmDeleteModal } from '../ConfirmDeleteModal';
|
||||
import { SettingsView } from '../SettingsView';
|
||||
import { StyleView } from '../StyleView/StyleView';
|
||||
import { TagsView } from '../TagsView';
|
||||
import { TagInput } from '../TagInput';
|
||||
import { ChatPanel } from '../ChatPanel';
|
||||
@@ -1697,6 +1698,7 @@ export const Editor: React.FC = () => {
|
||||
const showPost = activeTab?.type === 'post';
|
||||
const showMedia = activeTab?.type === 'media';
|
||||
const showSettings = activeTab?.type === 'settings';
|
||||
const showStyle = activeTab?.type === 'style';
|
||||
const showTags = activeTab?.type === 'tags';
|
||||
const showChat = activeTab?.type === 'chat';
|
||||
const showImport = activeTab?.type === 'import';
|
||||
@@ -1765,6 +1767,16 @@ export const Editor: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (showStyle) {
|
||||
return (
|
||||
<div className="editor">
|
||||
<StyleView />
|
||||
{renderErrorModal()}
|
||||
{renderConfirmDeleteModal()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show tags if tags tab is active
|
||||
if (showTags) {
|
||||
return (
|
||||
|
||||
@@ -1244,6 +1244,7 @@ const SettingsNav: React.FC = () => {
|
||||
|
||||
// Check if settings panel is currently active
|
||||
const isSettingsTabActive = tabs.some(t => t.type === 'settings' && t.id === activeTabId);
|
||||
const isStyleTabActive = tabs.some(t => t.type === 'style' && t.id === activeTabId);
|
||||
|
||||
const handleNavClick = (category: SettingsCategory) => {
|
||||
// If settings panel is not open or not active, open it first
|
||||
@@ -1257,6 +1258,10 @@ const SettingsNav: React.FC = () => {
|
||||
}, isSettingsTabActive ? 0 : 100);
|
||||
};
|
||||
|
||||
const handleStyleClick = () => {
|
||||
openTab({ type: 'style', id: 'style', isTransient: false });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sidebar-content settings-panel">
|
||||
<div className="sidebar-section">
|
||||
@@ -1308,6 +1313,13 @@ const SettingsNav: React.FC = () => {
|
||||
<span className="settings-nav-entry-icon">🗄️</span>
|
||||
<span>Data</span>
|
||||
</button>
|
||||
<button
|
||||
className={`settings-nav-entry ${isStyleTabActive ? 'active' : ''}`}
|
||||
onClick={handleStyleClick}
|
||||
>
|
||||
<span className="settings-nav-entry-icon">🎨</span>
|
||||
<span>Style</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -89,3 +89,8 @@
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-bar-item.theme-badge {
|
||||
border: 1px solid var(--vscode-statusBar-border, transparent);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAppStore } from '../../store';
|
||||
import { ProjectSelector } from '../ProjectSelector';
|
||||
import { getRendererPicoTheme } from '../../utils/picoTheme';
|
||||
import './StatusBar.css';
|
||||
|
||||
export const StatusBar: React.FC = () => {
|
||||
@@ -9,6 +10,7 @@ export const StatusBar: React.FC = () => {
|
||||
tasks,
|
||||
selectedPostId,
|
||||
totalPosts,
|
||||
picoTheme,
|
||||
} = useAppStore();
|
||||
|
||||
const [selectedPostStatus, setSelectedPostStatus] = useState<string | null>(null);
|
||||
@@ -25,6 +27,7 @@ export const StatusBar: React.FC = () => {
|
||||
}, [selectedPostId]);
|
||||
|
||||
const runningTasks = tasks.filter(t => t.status === 'running');
|
||||
const activeTheme = getRendererPicoTheme(picoTheme);
|
||||
|
||||
return (
|
||||
<div className="status-bar">
|
||||
@@ -61,6 +64,10 @@ export const StatusBar: React.FC = () => {
|
||||
<span>{media.length} media</span>
|
||||
</div>
|
||||
|
||||
<div className="status-bar-item theme-badge">
|
||||
<span>Theme: {activeTheme}</span>
|
||||
</div>
|
||||
|
||||
{/* App Name */}
|
||||
<div className="status-bar-item brand">
|
||||
<span>bDS</span>
|
||||
|
||||
106
src/renderer/components/StyleView/StyleView.css
Normal file
106
src/renderer/components/StyleView/StyleView.css
Normal file
@@ -0,0 +1,106 @@
|
||||
.style-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
gap: 12px;
|
||||
background: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
}
|
||||
|
||||
.style-view-header h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.style-view-header p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.style-theme-picker {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.style-theme-option {
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.style-theme-option.selected {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
box-shadow: 0 0 0 1px var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.style-theme-swatch {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
height: 56px;
|
||||
border-radius: 4px;
|
||||
color: var(--vscode-editor-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.style-theme-tones {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.style-theme-tone {
|
||||
height: 24px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid color-mix(in srgb, var(--vscode-input-border) 70%, transparent);
|
||||
}
|
||||
|
||||
.style-theme-tone-dark {
|
||||
border-color: color-mix(in srgb, #ffffff 18%, transparent);
|
||||
}
|
||||
|
||||
.style-theme-name {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.style-apply-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.style-preview-mode-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.style-preview-mode-control select {
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
.style-preview-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.style-preview-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
138
src/renderer/components/StyleView/StyleView.tsx
Normal file
138
src/renderer/components/StyleView/StyleView.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useAppStore } from '../../store';
|
||||
import { showToast } from '../Toast';
|
||||
import { PICO_THEME_NAMES, PICO_THEME_PREVIEW_TOKENS, getRendererPicoTheme, ensureRendererPicoThemeStylesheet, type PicoThemeName } from '../../utils/picoTheme';
|
||||
import './StyleView.css';
|
||||
|
||||
const PREVIEW_SERVER_BASE_URL = 'http://127.0.0.1:4123';
|
||||
type PreviewMode = 'auto' | 'light' | 'dark';
|
||||
|
||||
function toDisplayName(theme: PicoThemeName): string {
|
||||
return `${theme.charAt(0).toUpperCase()}${theme.slice(1)}`;
|
||||
}
|
||||
|
||||
export const StyleView: React.FC = () => {
|
||||
const { activeProject, picoTheme, setPicoTheme } = useAppStore();
|
||||
const [selectedTheme, setSelectedTheme] = useState<PicoThemeName>(getRendererPicoTheme(picoTheme));
|
||||
const [previewMode, setPreviewMode] = useState<PreviewMode>('auto');
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTheme(getRendererPicoTheme(picoTheme));
|
||||
}, [picoTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
ensureRendererPicoThemeStylesheet(selectedTheme).catch((error) => {
|
||||
console.error('Failed to load selected renderer theme stylesheet:', error);
|
||||
});
|
||||
}, [selectedTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadTheme = async () => {
|
||||
try {
|
||||
const metadata = await window.electronAPI?.meta.getProjectMetadata();
|
||||
const nextTheme = getRendererPicoTheme(metadata?.picoTheme);
|
||||
setPicoTheme(metadata?.picoTheme);
|
||||
setSelectedTheme(nextTheme);
|
||||
} catch (error) {
|
||||
console.error('Failed to load project style settings:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadTheme();
|
||||
}, [activeProject?.id, setPicoTheme]);
|
||||
|
||||
const previewUrl = useMemo(() => {
|
||||
return `${PREVIEW_SERVER_BASE_URL}/__style-preview?theme=${encodeURIComponent(selectedTheme)}&mode=${encodeURIComponent(previewMode)}`;
|
||||
}, [selectedTheme, previewMode]);
|
||||
|
||||
const handleApplyTheme = async () => {
|
||||
try {
|
||||
setIsApplying(true);
|
||||
await window.electronAPI?.meta.updateProjectMetadata({ picoTheme: selectedTheme });
|
||||
setPicoTheme(selectedTheme);
|
||||
showToast.success(`Applied theme: ${toDisplayName(selectedTheme)}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to apply style theme:', error);
|
||||
showToast.error('Failed to apply theme');
|
||||
} finally {
|
||||
setIsApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="style-view">
|
||||
<div className="style-view-header">
|
||||
<h2>Style</h2>
|
||||
<p>Select a Pico CSS theme and preview the top posts before applying.</p>
|
||||
</div>
|
||||
|
||||
<div className="style-theme-picker" role="group" aria-label="Pico Theme Picker">
|
||||
{PICO_THEME_NAMES.map((theme) => {
|
||||
const isSelected = selectedTheme === theme;
|
||||
const preview = PICO_THEME_PREVIEW_TOKENS[theme];
|
||||
return (
|
||||
<button
|
||||
key={theme}
|
||||
type="button"
|
||||
className={`style-theme-option ${isSelected ? 'selected' : ''}`}
|
||||
onClick={() => setSelectedTheme(theme)}
|
||||
aria-pressed={isSelected}
|
||||
aria-label={toDisplayName(theme)}
|
||||
>
|
||||
<span className={`style-theme-swatch style-theme-swatch-${theme}`}>
|
||||
<span className="style-theme-tones" aria-hidden="true">
|
||||
<span
|
||||
className="style-theme-tone style-theme-tone-accent"
|
||||
style={{ background: `linear-gradient(135deg, ${preview.lightPrimary}, ${preview.darkPrimary})` }}
|
||||
/>
|
||||
<span
|
||||
className="style-theme-tone style-theme-tone-light"
|
||||
style={{ backgroundColor: preview.lightBackground }}
|
||||
/>
|
||||
<span
|
||||
className="style-theme-tone style-theme-tone-dark"
|
||||
style={{ backgroundColor: preview.darkBackground }}
|
||||
/>
|
||||
</span>
|
||||
<span className="style-theme-name">{toDisplayName(theme)}</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="style-apply-row">
|
||||
<label className="style-preview-mode-control">
|
||||
<span>Preview mode</span>
|
||||
<select
|
||||
aria-label="Preview mode"
|
||||
value={previewMode}
|
||||
onChange={(event) => setPreviewMode(event.target.value as PreviewMode)}
|
||||
>
|
||||
<option value="auto">Auto</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</label>
|
||||
<button
|
||||
className="primary"
|
||||
onClick={handleApplyTheme}
|
||||
disabled={isApplying || picoTheme === selectedTheme}
|
||||
>
|
||||
Apply Theme
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="style-preview-container">
|
||||
<iframe
|
||||
title="Theme preview"
|
||||
className="style-preview-frame"
|
||||
src={previewUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StyleView;
|
||||
1
src/renderer/components/StyleView/index.ts
Normal file
1
src/renderer/components/StyleView/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { StyleView } from './StyleView';
|
||||
@@ -41,6 +41,10 @@ const getTabTitle = (
|
||||
if (tab.type === 'settings') {
|
||||
return 'Settings';
|
||||
}
|
||||
|
||||
if (tab.type === 'style') {
|
||||
return 'Style';
|
||||
}
|
||||
|
||||
if (tab.type === 'tags') {
|
||||
return 'Tags';
|
||||
@@ -107,6 +111,12 @@ const getTabIcon = (tab: Tab): React.ReactNode => {
|
||||
<path d="M9.1 4.4L8.6 2H7.4l-.5 2.4-.7.3-2-1.3-.9.8 1.3 2-.2.7-2.4.5v1.2l2.4.5.3.8-1.3 2 .8.8 2-1.3.8.3.4 2.3h1.2l.5-2.4.8-.3 2 1.3.8-.8-1.3-2 .3-.8 2.3-.4V7.4l-2.4-.5-.3-.8 1.3-2-.8-.8-2 1.3-.7-.2zM9.4 1l.5 2.4L12 2.1l2 2-1.4 2.1 2.4.4v3l-2.4.5L14 12l-2 2-2.1-1.4-.5 2.4h-3L5.9 12.5 4 14l-2-2 1.4-2.1L1 9.4v-3l2.4-.5L2 4l2-2 2.1 1.4.4-2.4h3zm.6 7c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zM8 9c.6 0 1-.4 1-1s-.4-1-1-1-1 .4-1 1 .4 1 1 1z"/>
|
||||
</svg>
|
||||
);
|
||||
case 'style':
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm0 1a6 6 0 0 1 4.91 2.55c-.61.3-1.39.52-2.28.52-1.08 0-1.9-.22-2.62-.42-.71-.2-1.33-.37-2.06-.37-.97 0-1.84.25-2.55.6A6 6 0 0 1 8 2zm-5 6a5.97 5.97 0 0 1 .17-1.42c.59-.37 1.5-.8 2.77-.8.59 0 1.1.14 1.76.32.79.22 1.69.47 2.92.47 1.05 0 1.99-.24 2.75-.59A6 6 0 0 1 13.99 8H3zm10.82 1h-10.6a6 6 0 0 0 10.6 0z"/>
|
||||
</svg>
|
||||
);
|
||||
case 'tags':
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
|
||||
@@ -12,6 +12,7 @@ export { TaskPopup } from './TaskPopup';
|
||||
export { ResizablePanel } from './ResizablePanel';
|
||||
export { CredentialsPanel } from './CredentialsPanel';
|
||||
export { SettingsView } from './SettingsView';
|
||||
export { StyleView } from './StyleView';
|
||||
export { TagsView, scrollToTagsSection, type TagsCategory } from './TagsView';
|
||||
export { TagInput } from './TagInput';
|
||||
export { PostLinks } from './PostLinks';
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
const STORAGE_KEY = 'bds-app-state';
|
||||
|
||||
// Tab types
|
||||
export type TabType = 'post' | 'media' | 'settings' | 'tags' | 'chat' | 'import' | 'metadata-diff' | 'git-diff' | 'documentation';
|
||||
export type TabType = 'post' | 'media' | 'settings' | 'style' | 'tags' | 'chat' | 'import' | 'metadata-diff' | 'git-diff' | 'documentation';
|
||||
|
||||
export interface Tab {
|
||||
type: TabType;
|
||||
@@ -65,6 +65,7 @@ interface AppState {
|
||||
selectedPostId: string | null;
|
||||
selectedMediaId: string | null;
|
||||
preferredEditorMode: EditorMode;
|
||||
picoTheme: import('../../main/shared/picoThemes').PicoThemeName | undefined;
|
||||
gitDiffPreferences: GitDiffPreferences;
|
||||
|
||||
// Data
|
||||
@@ -113,6 +114,7 @@ interface AppState {
|
||||
setSelectedPost: (id: string | null) => void;
|
||||
setSelectedMedia: (id: string | null) => void;
|
||||
setPreferredEditorMode: (mode: EditorMode) => void;
|
||||
setPicoTheme: (theme: import('../../main/shared/picoThemes').PicoThemeName | undefined) => void;
|
||||
setGitDiffPreferences: (preferences: GitDiffPreferences) => void;
|
||||
|
||||
setPosts: (posts: PostData[], hasMore?: boolean, total?: number) => void;
|
||||
@@ -166,6 +168,7 @@ export const useAppStore = create<AppState>()(
|
||||
selectedPostId: null,
|
||||
selectedMediaId: null,
|
||||
preferredEditorMode: 'wysiwyg',
|
||||
picoTheme: undefined,
|
||||
gitDiffPreferences: {
|
||||
wordWrap: true,
|
||||
viewStyle: 'inline',
|
||||
@@ -289,6 +292,7 @@ export const useAppStore = create<AppState>()(
|
||||
setSelectedPost: (id) => set({ selectedPostId: id }),
|
||||
setSelectedMedia: (id) => set({ selectedMediaId: id }),
|
||||
setPreferredEditorMode: (mode) => set({ preferredEditorMode: mode }),
|
||||
setPicoTheme: (theme) => set({ picoTheme: theme }),
|
||||
setGitDiffPreferences: (preferences) => set({ gitDiffPreferences: preferences }),
|
||||
|
||||
// Post Actions
|
||||
@@ -376,6 +380,7 @@ export const useAppStore = create<AppState>()(
|
||||
selectedPostId: state.selectedPostId,
|
||||
selectedMediaId: state.selectedMediaId,
|
||||
preferredEditorMode: state.preferredEditorMode,
|
||||
picoTheme: state.picoTheme,
|
||||
gitDiffPreferences: state.gitDiffPreferences,
|
||||
// Tabs are persisted here for now (project-specific persistence handled separately)
|
||||
tabs: state.tabs,
|
||||
@@ -393,6 +398,7 @@ export const useAppStore = create<AppState>()(
|
||||
activeTabId: persistedState.activeTabId || null,
|
||||
panelActiveTab: persistedState.panelActiveTab || current.panelActiveTab,
|
||||
dirtyPosts: new Set(persistedState.dirtyPosts || []),
|
||||
picoTheme: persistedState.picoTheme || current.picoTheme,
|
||||
gitDiffPreferences: persistedState.gitDiffPreferences || current.gitDiffPreferences,
|
||||
};
|
||||
},
|
||||
|
||||
70
src/renderer/utils/picoTheme.ts
Normal file
70
src/renderer/utils/picoTheme.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { PICO_THEME_NAMES, sanitizePicoTheme, type PicoThemeName } from '../../main/shared/picoThemes';
|
||||
|
||||
export { PICO_THEME_NAMES, type PicoThemeName };
|
||||
|
||||
export interface PicoThemePreviewToken {
|
||||
lightBackground: string;
|
||||
darkBackground: string;
|
||||
lightPrimary: string;
|
||||
darkPrimary: string;
|
||||
}
|
||||
|
||||
export const PICO_THEME_PREVIEW_TOKENS: Record<PicoThemeName, PicoThemePreviewToken> = {
|
||||
amber: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#876400', darkPrimary: '#c79400' },
|
||||
blue: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#2060df', darkPrimary: '#8999f9' },
|
||||
cyan: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#047878', darkPrimary: '#0ab1b1' },
|
||||
fuchsia: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#c1208b', darkPrimary: '#f869bf' },
|
||||
green: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#33790f', darkPrimary: '#4eb31b' },
|
||||
grey: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#6a6a6a', darkPrimary: '#9e9e9e' },
|
||||
indigo: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#655cd6', darkPrimary: '#a294e5' },
|
||||
jade: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#007a50', darkPrimary: '#00b478' },
|
||||
lime: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#577400', darkPrimary: '#82ab00' },
|
||||
orange: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#bd3c13', darkPrimary: '#f56b3d' },
|
||||
pink: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#c72259', darkPrimary: '#f7708e' },
|
||||
pumpkin: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#9c5900', darkPrimary: '#e48500' },
|
||||
purple: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#aa40bf', darkPrimary: '#d47de4' },
|
||||
red: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#c52f21', darkPrimary: '#f17961' },
|
||||
sand: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#6e6a60', darkPrimary: '#a39e8f' },
|
||||
slate: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#5d6b89', darkPrimary: '#909ebe' },
|
||||
violet: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#8352c5', darkPrimary: '#b290d9' },
|
||||
yellow: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#756b00', darkPrimary: '#ad9f00' },
|
||||
zinc: { lightBackground: '#fff', darkBackground: 'rgb(19, 22.5, 30.5)', lightPrimary: '#646b79', darkPrimary: '#969eaf' },
|
||||
};
|
||||
|
||||
const themeStylesheetLoaders: Record<PicoThemeName, () => Promise<unknown>> = {
|
||||
amber: () => import('@picocss/pico/css/pico.conditional.amber.min.css'),
|
||||
blue: () => import('@picocss/pico/css/pico.conditional.blue.min.css'),
|
||||
cyan: () => import('@picocss/pico/css/pico.conditional.cyan.min.css'),
|
||||
fuchsia: () => import('@picocss/pico/css/pico.conditional.fuchsia.min.css'),
|
||||
green: () => import('@picocss/pico/css/pico.conditional.green.min.css'),
|
||||
grey: () => import('@picocss/pico/css/pico.conditional.grey.min.css'),
|
||||
indigo: () => import('@picocss/pico/css/pico.conditional.indigo.min.css'),
|
||||
jade: () => import('@picocss/pico/css/pico.conditional.jade.min.css'),
|
||||
lime: () => import('@picocss/pico/css/pico.conditional.lime.min.css'),
|
||||
orange: () => import('@picocss/pico/css/pico.conditional.orange.min.css'),
|
||||
pink: () => import('@picocss/pico/css/pico.conditional.pink.min.css'),
|
||||
pumpkin: () => import('@picocss/pico/css/pico.conditional.pumpkin.min.css'),
|
||||
purple: () => import('@picocss/pico/css/pico.conditional.purple.min.css'),
|
||||
red: () => import('@picocss/pico/css/pico.conditional.red.min.css'),
|
||||
sand: () => import('@picocss/pico/css/pico.conditional.sand.min.css'),
|
||||
slate: () => import('@picocss/pico/css/pico.conditional.slate.min.css'),
|
||||
violet: () => import('@picocss/pico/css/pico.conditional.violet.min.css'),
|
||||
yellow: () => import('@picocss/pico/css/pico.conditional.yellow.min.css'),
|
||||
zinc: () => import('@picocss/pico/css/pico.conditional.zinc.min.css'),
|
||||
};
|
||||
|
||||
const loadedThemes = new Set<PicoThemeName>();
|
||||
|
||||
export function getRendererPicoTheme(theme: unknown): PicoThemeName {
|
||||
return sanitizePicoTheme(theme) ?? 'slate';
|
||||
}
|
||||
|
||||
export async function ensureRendererPicoThemeStylesheet(theme: PicoThemeName): Promise<void> {
|
||||
if (loadedThemes.has(theme)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loader = themeStylesheetLoaders[theme];
|
||||
await loader();
|
||||
loadedThemes.add(theme);
|
||||
}
|
||||
Reference in New Issue
Block a user