feat: 8 AI providers, rich text editor, agent notifications, UI contrast & font settings
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m25s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m25s
- Add DeepSeek, OpenRouter, Mistral, Z.AI, LM Studio as AI providers with editable model names via Combobox in admin settings - Fix OpenRouter broken by normalizeProvider bug in config.ts - Convert agent-created notes from Markdown to HTML (TipTap rich text) - Add Notification model + in-app notifications for agent results - Agent notification click opens the created note directly - Add note count display on notebook and inbox headers - Fix checklist toggle in card view (persist state via localCheckItems) - Add checklist creation option in tabs/list view (dropdown on + button) - Fix image description ENOENT error with HTTP fallback - Improve UI contrast across all themes (input, border, checkbox visibility) - Add font family setting (Inter vs System Default) in Appearance settings - Fix CSS font-sans variable conflict (removed dead Geist references) - Update README with new features and 8 providers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import { AdminProvidersWrapper } from '@/components/admin-providers-wrapper'
|
||||
import { detectUserLanguage } from '@/lib/i18n/detect-user-language'
|
||||
import { headers } from 'next/headers'
|
||||
import { detectUserLanguage, parseAcceptLanguage } from '@/lib/i18n/detect-user-language'
|
||||
import { loadTranslations } from '@/lib/i18n/load-translations'
|
||||
|
||||
// No <Suspense> here intentionally: combining a Suspense boundary with <Link>
|
||||
@@ -10,7 +11,9 @@ export default async function AdminGroupLayout({
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const initialLanguage = await detectUserLanguage()
|
||||
const headersList = await headers()
|
||||
const browserLang = parseAcceptLanguage(headersList.get('accept-language'))
|
||||
const initialLanguage = await detectUserLanguage(browserLang)
|
||||
const initialTranslations = await loadTranslations(initialLanguage)
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,8 @@ import { HeaderWrapper } from "@/components/header-wrapper";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
import { ProvidersWrapper } from "@/components/providers-wrapper";
|
||||
import { auth } from "@/auth";
|
||||
import { detectUserLanguage } from "@/lib/i18n/detect-user-language";
|
||||
import { headers } from "next/headers";
|
||||
import { detectUserLanguage, parseAcceptLanguage } from "@/lib/i18n/detect-user-language";
|
||||
import { loadTranslations } from "@/lib/i18n/load-translations";
|
||||
import { getAISettings } from "@/app/actions/ai-settings";
|
||||
|
||||
@@ -14,10 +15,14 @@ export default async function MainLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
// Read browser language hint from Accept-Language header
|
||||
const headersList = await headers();
|
||||
const browserLang = parseAcceptLanguage(headersList.get("accept-language"));
|
||||
|
||||
// Run auth + language detection + translation loading in parallel
|
||||
const [session, initialLanguage] = await Promise.all([
|
||||
auth(),
|
||||
detectUserLanguage(),
|
||||
detectUserLanguage(browserLang),
|
||||
]);
|
||||
|
||||
// Load initial translations server-side to prevent hydration mismatch
|
||||
|
||||
@@ -12,14 +12,16 @@ interface AppearanceSettingsClientProps {
|
||||
initialTheme: string
|
||||
initialNotesViewMode: 'masonry' | 'tabs'
|
||||
initialCardSizeMode?: 'variable' | 'uniform'
|
||||
initialFontFamily?: string
|
||||
}
|
||||
|
||||
export function AppearanceSettingsClient({ initialFontSize, initialTheme, initialNotesViewMode, initialCardSizeMode = 'variable' }: AppearanceSettingsClientProps) {
|
||||
export function AppearanceSettingsClient({ initialFontSize, initialTheme, initialNotesViewMode, initialCardSizeMode = 'variable', initialFontFamily = 'inter' }: AppearanceSettingsClientProps) {
|
||||
const { t } = useLanguage()
|
||||
const [theme, setTheme] = useState(initialTheme || 'light')
|
||||
const [fontSize, setFontSize] = useState(initialFontSize || 'medium')
|
||||
const [notesViewMode, setNotesViewMode] = useState<'masonry' | 'tabs'>(initialNotesViewMode)
|
||||
const [cardSizeMode, setCardSizeMode] = useState<'variable' | 'uniform'>(initialCardSizeMode)
|
||||
const [fontFamily, setFontFamily] = useState(initialFontFamily)
|
||||
|
||||
const handleThemeChange = async (value: string) => {
|
||||
setTheme(value)
|
||||
@@ -69,6 +71,20 @@ export function AppearanceSettingsClient({ initialFontSize, initialTheme, initia
|
||||
toast.success(t('settings.settingsSaved') || 'Saved')
|
||||
}
|
||||
|
||||
const handleFontFamilyChange = async (value: string) => {
|
||||
const font = value === 'system' ? 'system' : 'inter'
|
||||
setFontFamily(font)
|
||||
localStorage.setItem('font-family', font)
|
||||
const root = document.documentElement
|
||||
if (font === 'system') {
|
||||
root.classList.add('font-system')
|
||||
} else {
|
||||
root.classList.remove('font-system')
|
||||
}
|
||||
await updateAISettings({ fontFamily: font })
|
||||
toast.success(t('settings.settingsSaved') || 'Saved')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
@@ -116,6 +132,23 @@ export function AppearanceSettingsClient({ initialFontSize, initialTheme, initia
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t('appearance.fontFamilyLabel') || 'Font Family'}
|
||||
icon={<span className="text-2xl">🔤</span>}
|
||||
description={t('appearance.fontFamilyDescription') || 'Choose the font used throughout the app'}
|
||||
>
|
||||
<SettingSelect
|
||||
label={t('appearance.fontFamilyLabel') || 'Font Family'}
|
||||
description={t('appearance.selectFontFamily') || 'Inter is optimized for readability, System uses your OS native font'}
|
||||
value={fontFamily}
|
||||
options={[
|
||||
{ value: 'inter', label: 'Inter' },
|
||||
{ value: 'system', label: t('appearance.fontSystem') || 'System Default' },
|
||||
]}
|
||||
onChange={handleFontFamilyChange}
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
title={t('appearance.notesViewLabel')}
|
||||
icon={<span className="text-2xl">📋</span>}
|
||||
|
||||
@@ -21,6 +21,7 @@ export default async function AppearanceSettingsPage() {
|
||||
initialTheme={userSettings.theme}
|
||||
initialNotesViewMode={aiSettings.notesViewMode === 'masonry' ? 'masonry' : 'tabs'}
|
||||
initialCardSizeMode={userSettings.cardSizeMode}
|
||||
initialFontFamily={aiSettings.fontFamily || 'inter'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { auth } from '@/auth'
|
||||
import { sendEmail } from '@/lib/mail'
|
||||
import { updateTag } from 'next/cache'
|
||||
|
||||
async function checkAdmin() {
|
||||
const session = await auth()
|
||||
@@ -62,9 +61,6 @@ export async function updateSystemConfig(data: Record<string, string>) {
|
||||
|
||||
await prisma.$transaction(operations)
|
||||
|
||||
// Invalidate cache after update
|
||||
updateTag('system-config')
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Failed to update settings:', error)
|
||||
|
||||
@@ -23,6 +23,7 @@ export type UserAISettingsData = {
|
||||
autoLabeling?: boolean
|
||||
noteHistory?: boolean
|
||||
noteHistoryMode?: 'manual' | 'auto'
|
||||
fontFamily?: 'inter' | 'system'
|
||||
}
|
||||
|
||||
/** Only fields that exist on `UserAISettings` in Prisma (excludes e.g. `theme`, which lives on `User`). */
|
||||
@@ -45,6 +46,7 @@ const USER_AI_SETTINGS_PRISMA_KEYS = [
|
||||
'autoLabeling',
|
||||
'noteHistory',
|
||||
'noteHistoryMode',
|
||||
'fontFamily',
|
||||
] as const
|
||||
|
||||
type UserAISettingsPrismaKey = (typeof USER_AI_SETTINGS_PRISMA_KEYS)[number]
|
||||
@@ -157,6 +159,7 @@ const getCachedAISettings = unstable_cache(
|
||||
autoLabeling: true,
|
||||
noteHistory: false,
|
||||
noteHistoryMode: 'manual' as const,
|
||||
fontFamily: 'inter' as const,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +191,7 @@ const getCachedAISettings = unstable_cache(
|
||||
autoLabeling: settings.autoLabeling ?? true,
|
||||
noteHistory: settings.noteHistory ?? false,
|
||||
noteHistoryMode: (settings.noteHistoryMode ?? 'manual') as 'manual' | 'auto',
|
||||
fontFamily: (settings.fontFamily || 'inter') as 'inter' | 'system',
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting AI settings:', error)
|
||||
@@ -212,6 +216,7 @@ const getCachedAISettings = unstable_cache(
|
||||
autoLabeling: true,
|
||||
noteHistory: false,
|
||||
noteHistoryMode: 'manual' as const,
|
||||
fontFamily: 'inter' as const,
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -249,6 +254,7 @@ export async function getAISettings(userId?: string) {
|
||||
autoLabeling: true,
|
||||
noteHistory: false,
|
||||
noteHistoryMode: 'manual' as const,
|
||||
fontFamily: 'inter' as const,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use server'
|
||||
|
||||
import { detectUserLanguage } from '@/lib/i18n/detect-user-language'
|
||||
import { headers } from 'next/headers'
|
||||
import { detectUserLanguage, parseAcceptLanguage } from '@/lib/i18n/detect-user-language'
|
||||
import { SupportedLanguage } from '@/lib/i18n/load-translations'
|
||||
|
||||
/**
|
||||
@@ -8,5 +9,11 @@ import { SupportedLanguage } from '@/lib/i18n/load-translations'
|
||||
* Called on app load to set initial language
|
||||
*/
|
||||
export async function getInitialLanguage(): Promise<SupportedLanguage> {
|
||||
return await detectUserLanguage()
|
||||
try {
|
||||
const headersList = await headers()
|
||||
const browserLang = parseAcceptLanguage(headersList.get('accept-language'))
|
||||
return await detectUserLanguage(browserLang)
|
||||
} catch {
|
||||
return await detectUserLanguage()
|
||||
}
|
||||
}
|
||||
|
||||
76
memento-note/app/actions/notifications.ts
Normal file
76
memento-note/app/actions/notifications.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
'use server'
|
||||
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { auth } from '@/auth'
|
||||
|
||||
export interface AppNotification {
|
||||
id: string
|
||||
type: string
|
||||
title: string
|
||||
message: string | null
|
||||
read: boolean
|
||||
actionUrl: string | null
|
||||
relatedId: string | null
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export async function getUnreadNotifications(): Promise<AppNotification[]> {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return []
|
||||
|
||||
try {
|
||||
const notifications = await prisma.notification.findMany({
|
||||
where: { userId: session.user.id, read: false },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 20,
|
||||
})
|
||||
return notifications
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function markNotificationRead(id: string): Promise<void> {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return
|
||||
|
||||
await prisma.notification.updateMany({
|
||||
where: { id, userId: session.user.id },
|
||||
data: { read: true },
|
||||
})
|
||||
}
|
||||
|
||||
export async function markAllNotificationsRead(): Promise<void> {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return
|
||||
|
||||
await prisma.notification.updateMany({
|
||||
where: { userId: session.user.id, read: false },
|
||||
data: { read: true },
|
||||
})
|
||||
}
|
||||
|
||||
/** Create a notification (called from server-side code, not exposed to client) */
|
||||
export async function createNotification(data: {
|
||||
userId: string
|
||||
type: string
|
||||
title: string
|
||||
message?: string
|
||||
actionUrl?: string
|
||||
relatedId?: string
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: data.userId,
|
||||
type: data.type,
|
||||
title: data.title,
|
||||
message: data.message || null,
|
||||
actionUrl: data.actionUrl || null,
|
||||
relatedId: data.relatedId || null,
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('[Notification] Failed to create:', e)
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,13 @@ async function requireAdmin() {
|
||||
/**
|
||||
* GET /api/admin/models?type=ollama&url=<base_url>
|
||||
* GET /api/admin/models?type=custom&url=<base_url>&key=<api_key>&kind=tags|embeddings
|
||||
* GET /api/admin/models?type=deepseek&key=<api_key>&kind=tags|embeddings
|
||||
* GET /api/admin/models?type=openrouter&key=<api_key>&kind=tags|embeddings
|
||||
* GET /api/admin/models?type=mistral&key=<api_key>&kind=tags|embeddings
|
||||
* GET /api/admin/models?type=zai&key=<api_key>&kind=tags|embeddings
|
||||
* GET /api/admin/models?type=lmstudio&url=<base_url>
|
||||
*
|
||||
* Route API (not a Server Action) for fetching AI model lists from Ollama or
|
||||
* OpenAI-compatible providers. Using a Route Handler instead of a Server Action
|
||||
* is the correct architecture for client-side GET requests: Server Actions are
|
||||
* for data mutations, and calling them from useEffect pushes items into the
|
||||
* App Router's internal action queue, which is drained during render (inside
|
||||
* AppRouter's useMemo), triggering React Error #310 when multiple calls stack up.
|
||||
* Route API for fetching AI model lists from providers.
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
if (!(await requireAdmin())) {
|
||||
@@ -29,14 +29,22 @@ export async function GET(request: NextRequest) {
|
||||
const apiKey = searchParams.get('key') ?? undefined
|
||||
const kind = searchParams.get('kind') ?? 'tags'
|
||||
|
||||
if (!rawUrl) {
|
||||
return NextResponse.json({ success: false, models: [], error: 'url parameter is required' })
|
||||
// Provider-specific base URLs (used when url param is empty)
|
||||
const PROVIDER_URLS: Record<string, string> = {
|
||||
deepseek: 'https://api.deepseek.com/v1',
|
||||
openrouter: 'https://openrouter.ai/api/v1',
|
||||
mistral: 'https://api.mistral.ai/v1',
|
||||
zai: 'https://api.zukijourney.com/v1',
|
||||
lmstudio: 'http://localhost:1234/v1',
|
||||
}
|
||||
|
||||
const baseUrl = rawUrl.replace(/\/$/, '').replace(/\/v1$/, '')
|
||||
|
||||
try {
|
||||
// Ollama: uses native /api/tags endpoint
|
||||
if (type === 'ollama') {
|
||||
if (!rawUrl) {
|
||||
return NextResponse.json({ success: false, models: [], error: 'url parameter is required for Ollama' })
|
||||
}
|
||||
const baseUrl = rawUrl.replace(/\/$/, '').replace(/\/api$/, '')
|
||||
const res = await fetch(`${baseUrl}/api/tags`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
@@ -49,58 +57,69 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ success: true, models })
|
||||
}
|
||||
|
||||
if (type === 'custom') {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
|
||||
// All other providers: use OpenAI-compatible /v1/models endpoint
|
||||
const baseUrl = rawUrl
|
||||
? rawUrl.replace(/\/$/, '')
|
||||
: (PROVIDER_URLS[type || ''] || '')
|
||||
|
||||
if (kind === 'embeddings') {
|
||||
// Try provider-specific embeddings endpoint first (e.g. OpenRouter)
|
||||
try {
|
||||
const embRes = await fetch(`${baseUrl}/v1/embeddings/models`, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
})
|
||||
if (embRes.ok) {
|
||||
const embData = await embRes.json()
|
||||
const embModels: string[] = (embData.data ?? [])
|
||||
.map((m: { id: string }) => m.id)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
if (embModels.length > 0) {
|
||||
return NextResponse.json({ success: true, models: embModels })
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall through to /v1/models with keyword filter
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(`${baseUrl}/v1/models`, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
})
|
||||
if (!res.ok) {
|
||||
return NextResponse.json({ success: false, models: [], error: `Provider ${res.status}` })
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
let models: string[] = (data.data ?? [])
|
||||
.map((m: { id: string }) => m.id)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
|
||||
if (kind === 'embeddings') {
|
||||
const keywords = ['embed', 'embedding', 'ada', 'e5', 'bge', 'gte', 'minilm']
|
||||
const filtered = models.filter((id) =>
|
||||
keywords.some((kw) => id.toLowerCase().includes(kw))
|
||||
)
|
||||
if (filtered.length > 0) models = filtered
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, models })
|
||||
if (!baseUrl) {
|
||||
return NextResponse.json({ success: false, models: [], error: 'url parameter is required' })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: false, models: [], error: `Unknown type: ${type}` })
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
|
||||
|
||||
// For OpenRouter, add required headers
|
||||
if (type === 'openrouter') {
|
||||
headers['HTTP-Referer'] = 'https://localhost:3000'
|
||||
headers['X-Title'] = 'Memento AI'
|
||||
}
|
||||
|
||||
// Try provider-specific embeddings endpoint first for embeddings kind
|
||||
if (kind === 'embeddings') {
|
||||
try {
|
||||
const embRes = await fetch(`${baseUrl}/embeddings/models`, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
})
|
||||
if (embRes.ok) {
|
||||
const embData = await embRes.json()
|
||||
const embModels: string[] = (embData.data ?? [])
|
||||
.map((m: { id: string }) => m.id)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
if (embModels.length > 0) {
|
||||
return NextResponse.json({ success: true, models: embModels })
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall through to /v1/models with keyword filter
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(`${baseUrl}/models`, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
})
|
||||
if (!res.ok) {
|
||||
return NextResponse.json({ success: false, models: [], error: `Provider ${res.status}` })
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
let models: string[] = (data.data ?? [])
|
||||
.map((m: { id: string }) => m.id)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
|
||||
if (kind === 'embeddings') {
|
||||
const keywords = ['embed', 'embedding', 'ada', 'e5', 'bge', 'gte', 'minilm']
|
||||
const filtered = models.filter((id) =>
|
||||
keywords.some((kw) => id.toLowerCase().includes(kw))
|
||||
)
|
||||
if (filtered.length > 0) models = filtered
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, models })
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ success: false, models: [], error: err.message ?? 'Request failed' })
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-sans: var(--font-inter);
|
||||
--font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Monaco, Consolas, monospace;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
@@ -117,11 +117,11 @@
|
||||
--secondary-foreground: #1e293b;
|
||||
--muted: #f1f5f9;
|
||||
--muted-foreground: #64748b;
|
||||
--accent: #f8fafc;
|
||||
--accent: #f1f5f9;
|
||||
--accent-foreground: #0284c7;
|
||||
--destructive: oklch(0.6 0.18 25); /* Rouge */
|
||||
--border: #e2e8f0; /* Gris-bleu très clair */
|
||||
--input: #ffffff;
|
||||
--border: #cbd5e1; /* Gris-bleu visible */
|
||||
--input: #cbd5e1; /* Bordure visible pour inputs/checkbox */
|
||||
--ring: #0284c7;
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
@@ -151,11 +151,11 @@
|
||||
--secondary-foreground: oklch(0.2 0.02 230);
|
||||
--muted: oklch(0.92 0.005 230);
|
||||
--muted-foreground: oklch(0.6 0.01 230);
|
||||
--accent: oklch(0.94 0.005 230);
|
||||
--accent: oklch(0.92 0.005 230);
|
||||
--accent-foreground: oklch(0.2 0.02 230);
|
||||
--destructive: oklch(0.6 0.18 25); /* Rouge */
|
||||
--border: oklch(0.9 0.008 230); /* Gris-bleu très clair */
|
||||
--input: oklch(0.98 0.003 230);
|
||||
--border: oklch(0.85 0.008 230); /* Gris-bleu visible */
|
||||
--input: oklch(0.85 0.008 230); /* Bordure visible */
|
||||
--ring: oklch(0.7 0.005 230);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.2 0.02 230);
|
||||
@@ -165,7 +165,7 @@
|
||||
--sidebar-primary-foreground: oklch(0.99 0 0);
|
||||
--sidebar-accent: oklch(0.94 0.005 230);
|
||||
--sidebar-accent-foreground: oklch(0.2 0.02 230);
|
||||
--sidebar-border: oklch(0.9 0.008 230);
|
||||
--sidebar-border: oklch(0.85 0.008 230);
|
||||
--sidebar-ring: oklch(0.7 0.005 230);
|
||||
}
|
||||
|
||||
@@ -182,11 +182,11 @@
|
||||
--secondary-foreground: oklch(0.97 0.003 230);
|
||||
--muted: oklch(0.22 0.006 230);
|
||||
--muted-foreground: oklch(0.55 0.01 230);
|
||||
--accent: oklch(0.24 0.006 230);
|
||||
--accent: oklch(0.26 0.008 230);
|
||||
--accent-foreground: oklch(0.97 0.003 230);
|
||||
--destructive: oklch(0.65 0.18 25);
|
||||
--border: oklch(0.28 0.01 230);
|
||||
--input: oklch(0.2 0.006 230);
|
||||
--border: oklch(0.33 0.01 230);
|
||||
--input: oklch(0.33 0.01 230);
|
||||
--ring: oklch(0.6 0.01 230);
|
||||
--popover: oklch(0.18 0.006 230);
|
||||
--popover-foreground: oklch(0.97 0.003 230);
|
||||
@@ -213,11 +213,11 @@
|
||||
--secondary-foreground: oklch(0.97 0.003 230);
|
||||
--muted: oklch(0.22 0.006 230);
|
||||
--muted-foreground: oklch(0.55 0.01 230);
|
||||
--accent: oklch(0.24 0.006 230);
|
||||
--accent: oklch(0.26 0.008 230);
|
||||
--accent-foreground: oklch(0.97 0.003 230);
|
||||
--destructive: oklch(0.65 0.18 25);
|
||||
--border: oklch(0.28 0.01 230);
|
||||
--input: oklch(0.2 0.006 230);
|
||||
--border: oklch(0.33 0.01 230);
|
||||
--input: oklch(0.33 0.01 230);
|
||||
--ring: oklch(0.6 0.01 230);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
@@ -248,8 +248,8 @@
|
||||
--accent: oklch(0.25 0.015 250);
|
||||
--accent-foreground: oklch(0.18 0.03 250);
|
||||
--destructive: oklch(0.6 0.22 25);
|
||||
--border: oklch(0.85 0.015 250);
|
||||
--input: oklch(0.25 0.01 250);
|
||||
--border: oklch(0.82 0.015 250);
|
||||
--input: oklch(0.82 0.015 250);
|
||||
--ring: oklch(0.65 0.015 250);
|
||||
--popover: oklch(0.97 0.006 250);
|
||||
--popover-foreground: oklch(0.18 0.03 250);
|
||||
@@ -274,11 +274,11 @@
|
||||
--secondary-foreground: oklch(0.96 0.005 250);
|
||||
--muted: oklch(0.2 0.015 250);
|
||||
--muted-foreground: oklch(0.5 0.02 250);
|
||||
--accent: oklch(0.22 0.02 250);
|
||||
--accent: oklch(0.26 0.02 250);
|
||||
--accent-foreground: oklch(0.96 0.005 250);
|
||||
--destructive: oklch(0.65 0.2 25);
|
||||
--border: oklch(0.3 0.02 250);
|
||||
--input: oklch(0.22 0.02 250);
|
||||
--border: oklch(0.33 0.02 250);
|
||||
--input: oklch(0.33 0.02 250);
|
||||
--ring: oklch(0.55 0.02 250);
|
||||
--popover: oklch(0.15 0.015 250);
|
||||
--popover-foreground: oklch(0.96 0.005 250);
|
||||
@@ -306,8 +306,8 @@
|
||||
--accent: oklch(0.93 0.01 225);
|
||||
--accent-foreground: oklch(0.18 0.035 225);
|
||||
--destructive: oklch(0.6 0.2 25);
|
||||
--border: oklch(0.87 0.012 225);
|
||||
--input: oklch(0.95 0.01 225);
|
||||
--border: oklch(0.83 0.012 225);
|
||||
--input: oklch(0.83 0.012 225);
|
||||
--ring: oklch(0.65 0.015 225);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.18 0.035 225);
|
||||
@@ -332,11 +332,11 @@
|
||||
--secondary-foreground: oklch(0.97 0.006 225);
|
||||
--muted: oklch(0.25 0.02 225);
|
||||
--muted-foreground: oklch(0.52 0.018 225);
|
||||
--accent: oklch(0.25 0.025 225);
|
||||
--accent: oklch(0.28 0.025 225);
|
||||
--accent-foreground: oklch(0.97 0.006 225);
|
||||
--destructive: oklch(0.65 0.22 25);
|
||||
--border: oklch(0.32 0.018 225);
|
||||
--input: oklch(0.25 0.02 225);
|
||||
--border: oklch(0.35 0.018 225);
|
||||
--input: oklch(0.35 0.018 225);
|
||||
--ring: oklch(0.55 0.02 225);
|
||||
--popover: oklch(0.17 0.01 225);
|
||||
--popover-foreground: oklch(0.97 0.006 225);
|
||||
@@ -364,8 +364,8 @@
|
||||
--accent: oklch(0.93 0.01 45);
|
||||
--accent-foreground: oklch(0.2 0.015 45);
|
||||
--destructive: oklch(0.6 0.2 25);
|
||||
--border: oklch(0.88 0.012 45);
|
||||
--input: oklch(0.97 0.008 45);
|
||||
--border: oklch(0.83 0.012 45);
|
||||
--input: oklch(0.83 0.012 45);
|
||||
--ring: oklch(0.68 0.01 45);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.2 0.015 45);
|
||||
@@ -390,11 +390,11 @@
|
||||
--secondary-foreground: oklch(0.97 0.005 45);
|
||||
--muted: oklch(0.23 0.02 45);
|
||||
--muted-foreground: oklch(0.55 0.012 45);
|
||||
--accent: oklch(0.27 0.018 45);
|
||||
--accent: oklch(0.29 0.018 45);
|
||||
--accent-foreground: oklch(0.97 0.005 45);
|
||||
--destructive: oklch(0.65 0.2 25);
|
||||
--border: oklch(0.3 0.018 45);
|
||||
--input: oklch(0.27 0.02 45);
|
||||
--border: oklch(0.33 0.018 45);
|
||||
--input: oklch(0.33 0.018 45);
|
||||
--ring: oklch(0.58 0.02 45);
|
||||
--popover: oklch(0.19 0.01 45);
|
||||
--popover-foreground: oklch(0.97 0.005 45);
|
||||
@@ -408,6 +408,16 @@
|
||||
--sidebar-ring: oklch(0.58 0.02 45);
|
||||
}
|
||||
|
||||
/* System font mode — override Inter with native OS fonts.
|
||||
Must be outside @layer base to win over next/font's generated class. */
|
||||
html.font-system {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif !important;
|
||||
}
|
||||
html.font-system body,
|
||||
html.font-system * {
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
|
||||
@@ -89,7 +89,7 @@ export default async function RootLayout({
|
||||
<SessionProviderWrapper>
|
||||
<ErrorReporter />
|
||||
<DirectionInitializer />
|
||||
<ThemeInitializer theme={userSettings.theme} fontSize={aiSettings.fontSize} />
|
||||
<ThemeInitializer theme={userSettings.theme} fontSize={aiSettings.fontSize} fontFamily={aiSettings.fontFamily} />
|
||||
{children}
|
||||
<Toaster />
|
||||
</SessionProviderWrapper>
|
||||
|
||||
Reference in New Issue
Block a user