diff --git a/public/font/aifont.woff2 b/public/font/aifont.woff2
index fc8e415..f226481 100644
Binary files a/public/font/aifont.woff2 and b/public/font/aifont.woff2 differ
diff --git a/public/icon/font.svg b/public/icon/font.svg
new file mode 100644
index 0000000..99468dd
--- /dev/null
+++ b/public/icon/font.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/web/components/mode-tabs.ts b/src/web/components/mode-tabs.ts
index 5732733..ed3df15 100644
--- a/src/web/components/mode-tabs.ts
+++ b/src/web/components/mode-tabs.ts
@@ -3,6 +3,12 @@ import { isLoggedIn } from '../lib/auth'
let currentNetwork = 'bsky.social'
let currentLang = localStorage.getItem('preferred-lang') || 'en'
+let currentFont = localStorage.getItem('preferred-font') || 'system'
+
+const FONT_OPTIONS: { id: string; label: string }[] = [
+ { id: 'system', label: 'System' },
+ { id: 'aifont', label: 'aifont' },
+]
export function getCurrentNetwork(): string {
return currentNetwork
@@ -52,17 +58,52 @@ export function renderModeTabs(handle: string, activeTab: 'blog' | 'browser' | '
return `
`
}
+export function getCurrentFont(): string {
+ return currentFont
+}
+
+export function setCurrentFont(font: string): void {
+ currentFont = font
+ localStorage.setItem('preferred-font', font)
+}
+
+function applyFont(font: string): void {
+ // Remove all font-* classes
+ for (const f of FONT_OPTIONS) {
+ document.body.classList.remove(`font-${f.id}`)
+ }
+ // Add selected (skip system = no class)
+ if (font !== 'system') {
+ document.body.classList.add(`font-${font}`)
+ }
+}
+
// Render language selector (above content)
export function renderLangSelector(langs: string[]): string {
- if (langs.length < 2) return ''
-
- return `
+ const langHtml = langs.length >= 2 ? `
+ ` : ''
+
+ const fontHtml = renderFontToggle()
+
+ if (!langHtml && !fontHtml) return ''
+ return ``
+}
+
+// Render font selector with dropdown
+export function renderFontToggle(): string {
+ return `
+
+
+
+
`
}
@@ -157,9 +198,54 @@ export async function setupModeTabs(onNetworkChange: (network: string) => void,
})
}
+ // Setup font selector
+ const fontTab = document.getElementById('font-tab')
+ const fontDropdown = document.getElementById('font-dropdown')
+
+ if (fontTab && fontDropdown) {
+ // Apply saved font on load
+ applyFont(currentFont)
+
+ // Build font options
+ const fontOptionsHtml = FONT_OPTIONS.map(f => {
+ const isSelected = f.id === currentFont
+ return `
+
+ ${f.label}
+ \u2713
+
+ `
+ }).join('')
+
+ fontDropdown.innerHTML = fontOptionsHtml
+
+ // Toggle dropdown
+ fontTab.addEventListener('click', (e) => {
+ e.stopPropagation()
+ fontDropdown.classList.toggle('show')
+ })
+
+ // Handle option selection
+ fontDropdown.querySelectorAll('.font-option').forEach(opt => {
+ opt.addEventListener('click', (e) => {
+ e.stopPropagation()
+ const font = (opt as HTMLElement).dataset.font || 'system'
+
+ applyFont(font)
+ setCurrentFont(font)
+ fontTab.classList.toggle('active', font !== 'system')
+
+ fontDropdown.querySelectorAll('.font-option').forEach(o => o.classList.remove('selected'))
+ opt.classList.add('selected')
+ fontDropdown.classList.remove('show')
+ })
+ })
+ }
+
// Close dropdowns on outside click
document.addEventListener('click', () => {
pdsDropdown?.classList.remove('show')
langDropdown?.classList.remove('show')
+ fontDropdown?.classList.remove('show')
})
}
diff --git a/src/web/main.ts b/src/web/main.ts
index 82a903e..dc8fbe6 100644
--- a/src/web/main.ts
+++ b/src/web/main.ts
@@ -885,6 +885,12 @@ async function render(route: Route): Promise {
}
}
+// Apply saved font preference immediately
+const savedFont = localStorage.getItem('preferred-font')
+if (savedFont && savedFont !== 'system') {
+ document.body.classList.add(`font-${savedFont}`)
+}
+
// Initial render
render(parseRoute())
diff --git a/src/web/styles/main.css b/src/web/styles/main.css
index cd3dbd0..bdadba1 100644
--- a/src/web/styles/main.css
+++ b/src/web/styles/main.css
@@ -15,7 +15,7 @@
}
body {
- font-family: 'aifont', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #1a1a1a;
background: #fff;
@@ -1506,9 +1506,151 @@ body {
display: flex;
justify-content: flex-end;
align-items: center;
+ gap: 4px;
margin-bottom: 8px;
}
+/* Font Toggle */
+.font-selector {
+ display: flex;
+ align-items: center;
+}
+
+.font-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ padding: 8px 16px;
+}
+
+.font-btn:hover {
+ background: #f0f0f0;
+}
+
+.font-icon {
+ width: 20px;
+ height: 20px;
+ opacity: 0.6;
+}
+
+.font-btn:hover .font-icon {
+ opacity: 0.9;
+}
+
+.font-btn.active .font-icon {
+ opacity: 1.0;
+}
+
+.font-selector {
+ position: relative;
+}
+
+.font-dropdown {
+ display: none;
+ position: absolute;
+ top: 100%;
+ right: 0;
+ margin-top: 4px;
+ background: #fff;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ min-width: 100px;
+ z-index: 100;
+ overflow: hidden;
+}
+
+.font-dropdown.show {
+ display: block;
+}
+
+.font-option {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background 0.15s;
+}
+
+.font-option:hover {
+ background: #f5f5f5;
+}
+
+.font-option.selected {
+ background: linear-gradient(135deg, #f0f7ff 0%, #e8f4ff 100%);
+}
+
+.font-name {
+ color: #333;
+ font-weight: 500;
+}
+
+.font-check {
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ border: 2px solid #ccc;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ transition: all 0.2s;
+}
+
+.font-option.selected .font-check {
+ background: var(--btn-color);
+ border-color: var(--btn-color);
+ color: #fff;
+}
+
+.font-option:not(.selected) .font-check {
+ color: transparent;
+}
+
+/* Font class on body */
+body.font-aifont {
+ font-family: 'aifont', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+ letter-spacing: -0.08em;
+}
+
+body.font-aifont code,
+body.font-aifont pre,
+body.font-aifont kbd,
+body.font-aifont samp {
+ letter-spacing: normal;
+}
+
+@media (prefers-color-scheme: dark) {
+ .font-btn:hover {
+ background: #2a2a2a;
+ }
+ .font-icon {
+ filter: invert(0.7);
+ }
+ .font-btn.active .font-icon {
+ filter: invert(1);
+ }
+ .font-dropdown {
+ background: #1a1a1a;
+ border-color: #333;
+ }
+ .font-option:hover {
+ background: #2a2a2a;
+ }
+ .font-option.selected {
+ background: linear-gradient(135deg, #1a2a3a 0%, #1a3040 100%);
+ }
+ .font-name {
+ color: #e0e0e0;
+ }
+}
+
@media (prefers-color-scheme: dark) {
.lang-btn {
background: transparent;