@geminilight/mindos 0.5.42 → 0.5.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app/app/layout.tsx
CHANGED
|
@@ -90,7 +90,7 @@ export default async function RootLayout({
|
|
|
90
90
|
{/* Apply user appearance settings before first paint, preventing flash */}
|
|
91
91
|
<script
|
|
92
92
|
dangerouslySetInnerHTML={{
|
|
93
|
-
__html: `(function(){try{var s=localStorage.getItem('theme');var dark=s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',dark);var cw=localStorage.getItem('content-width');if(cw)document.documentElement.style.setProperty('--content-width-override',cw);var pf=localStorage.getItem('prose-font');var fm={lora:'"Lora", Georgia, serif','ibm-plex-sans':'"IBM Plex Sans", sans-serif',geist:'var(--font-geist-sans), sans-serif','ibm-plex-mono':'"IBM Plex Mono", monospace'};if(pf&&fm[pf])document.documentElement.style.setProperty('--prose-font-override',fm[pf]);var loc=localStorage.getItem('locale')||'en';document.documentElement.lang=
|
|
93
|
+
__html: `(function(){try{var s=localStorage.getItem('theme');var dark=s&&s!=='system'?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',dark);var cw=localStorage.getItem('content-width');if(cw)document.documentElement.style.setProperty('--content-width-override',cw);var pf=localStorage.getItem('prose-font');var fm={lora:'"Lora", Georgia, serif','ibm-plex-sans':'"IBM Plex Sans", sans-serif',geist:'var(--font-geist-sans), sans-serif','ibm-plex-mono':'"IBM Plex Mono", monospace'};if(pf&&fm[pf])document.documentElement.style.setProperty('--prose-font-override',fm[pf]);var loc=localStorage.getItem('locale')||'system';var rl=loc==='system'?(navigator.language.startsWith('zh')?'zh':'en'):loc;document.documentElement.lang=rl==='zh'?'zh':'en';document.cookie='locale='+rl+';path=/;max-age=31536000;SameSite=Lax'}catch(e){}})();`,
|
|
94
94
|
}}
|
|
95
95
|
/>
|
|
96
96
|
</head>
|
|
@@ -12,7 +12,7 @@ export default function ThemeToggle() {
|
|
|
12
12
|
},
|
|
13
13
|
() => {
|
|
14
14
|
const stored = localStorage.getItem('theme');
|
|
15
|
-
return stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
15
|
+
return stored && stored !== 'system' ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
16
16
|
},
|
|
17
17
|
() => document.documentElement.classList.contains('dark'),
|
|
18
18
|
);
|
|
@@ -1,112 +1,172 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
4
|
+
import { ChevronDown, ChevronRight, Sun, Moon, Monitor, Type, Columns3, Globe } from 'lucide-react';
|
|
5
5
|
import { Locale } from '@/lib/i18n';
|
|
6
6
|
import { CONTENT_WIDTHS, FONTS, AppearanceTabProps } from './types';
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
/* ── Segmented Control ── */
|
|
9
|
+
function SegmentedControl<T extends string>({ options, value, onChange }: {
|
|
10
|
+
options: { value: T; label: string; icon?: React.ReactNode }[];
|
|
11
|
+
value: T;
|
|
12
|
+
onChange: (v: T) => void;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex rounded-lg border border-border bg-muted/30 p-0.5 gap-0.5">
|
|
16
|
+
{options.map(opt => (
|
|
17
|
+
<button
|
|
18
|
+
key={opt.value}
|
|
19
|
+
type="button"
|
|
20
|
+
onClick={() => onChange(opt.value)}
|
|
21
|
+
className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-all ${
|
|
22
|
+
value === opt.value
|
|
23
|
+
? 'bg-card text-foreground shadow-sm'
|
|
24
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
25
|
+
}`}
|
|
26
|
+
>
|
|
27
|
+
{opt.icon}
|
|
28
|
+
{opt.label}
|
|
29
|
+
</button>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* ── Setting Group ── */
|
|
36
|
+
function SettingGroup({ icon, label, children }: { icon: React.ReactNode; label: string; children: React.ReactNode }) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="space-y-2.5">
|
|
39
|
+
<div className="flex items-center gap-2">
|
|
40
|
+
<span className="text-muted-foreground">{icon}</span>
|
|
41
|
+
<span className="text-xs font-medium text-foreground">{label}</span>
|
|
42
|
+
</div>
|
|
43
|
+
{children}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
8
47
|
|
|
9
48
|
export function AppearanceTab({ font, setFont, contentWidth, setContentWidth, dark, setDark, locale, setLocale, t }: AppearanceTabProps) {
|
|
10
49
|
const [showShortcuts, setShowShortcuts] = useState(false);
|
|
50
|
+
const [themePref, setThemePref] = useState<string>(() =>
|
|
51
|
+
typeof window !== 'undefined' ? (localStorage.getItem('theme') ?? 'system') : 'system'
|
|
52
|
+
);
|
|
53
|
+
const [localePref, setLocalePref] = useState<string>(() =>
|
|
54
|
+
typeof window !== 'undefined' ? (localStorage.getItem('locale') ?? 'system') : 'system'
|
|
55
|
+
);
|
|
56
|
+
const a = t.settings.appearance;
|
|
11
57
|
|
|
12
58
|
return (
|
|
13
|
-
<div className="space-y-
|
|
14
|
-
|
|
15
|
-
|
|
59
|
+
<div className="space-y-6">
|
|
60
|
+
{/* Font */}
|
|
61
|
+
<SettingGroup icon={<Type size={14} />} label={a.readingFont}>
|
|
62
|
+
<div className="flex flex-wrap gap-1.5">
|
|
16
63
|
{FONTS.map(f => (
|
|
17
|
-
<
|
|
64
|
+
<button
|
|
65
|
+
key={f.value}
|
|
66
|
+
type="button"
|
|
67
|
+
onClick={() => setFont(f.value)}
|
|
68
|
+
className={`px-3 py-1.5 text-xs rounded-lg border transition-all ${
|
|
69
|
+
font === f.value
|
|
70
|
+
? 'border-amber-500 bg-amber-500/10 text-foreground font-medium shadow-sm'
|
|
71
|
+
: 'border-border text-muted-foreground hover:text-foreground hover:bg-muted'
|
|
72
|
+
}`}
|
|
73
|
+
style={{ fontFamily: f.style.fontFamily }}
|
|
74
|
+
>
|
|
75
|
+
{f.label}
|
|
76
|
+
</button>
|
|
18
77
|
))}
|
|
19
|
-
</
|
|
20
|
-
<p
|
|
21
|
-
|
|
78
|
+
</div>
|
|
79
|
+
<p
|
|
80
|
+
className="text-sm text-muted-foreground leading-relaxed px-0.5 mt-1"
|
|
81
|
+
style={{ fontFamily: FONTS.find(f => f.value === font)?.style.fontFamily }}
|
|
82
|
+
>
|
|
83
|
+
{a.fontPreview}
|
|
22
84
|
</p>
|
|
23
|
-
</
|
|
85
|
+
</SettingGroup>
|
|
24
86
|
|
|
25
|
-
|
|
26
|
-
|
|
87
|
+
{/* Content Width */}
|
|
88
|
+
<SettingGroup icon={<Columns3 size={14} />} label={a.contentWidth}>
|
|
89
|
+
<div className="flex flex-wrap gap-1.5">
|
|
27
90
|
{CONTENT_WIDTHS.map(w => (
|
|
28
91
|
<button
|
|
29
92
|
key={w.value}
|
|
30
93
|
type="button"
|
|
31
94
|
onClick={() => setContentWidth(w.value)}
|
|
32
|
-
className={`px-3 py-
|
|
95
|
+
className={`px-3 py-1.5 text-xs rounded-lg border transition-all ${
|
|
33
96
|
contentWidth === w.value
|
|
34
|
-
? 'border-amber-500 bg-amber-500/10 text-foreground'
|
|
35
|
-
: 'border-border text-muted-foreground hover:
|
|
97
|
+
? 'border-amber-500 bg-amber-500/10 text-foreground font-medium shadow-sm'
|
|
98
|
+
: 'border-border text-muted-foreground hover:text-foreground hover:bg-muted'
|
|
36
99
|
}`}
|
|
37
100
|
>
|
|
38
101
|
{w.label}
|
|
39
102
|
</button>
|
|
40
103
|
))}
|
|
41
104
|
</div>
|
|
42
|
-
</
|
|
105
|
+
</SettingGroup>
|
|
43
106
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{ value: '
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
>
|
|
65
|
-
{opt.label}
|
|
66
|
-
</button>
|
|
67
|
-
))}
|
|
68
|
-
</div>
|
|
69
|
-
</Field>
|
|
107
|
+
{/* Theme */}
|
|
108
|
+
<SettingGroup icon={<Sun size={14} />} label={a.colorTheme}>
|
|
109
|
+
<SegmentedControl
|
|
110
|
+
options={[
|
|
111
|
+
{ value: 'system', label: a.system, icon: <Monitor size={12} /> },
|
|
112
|
+
{ value: 'dark', label: a.dark, icon: <Moon size={12} /> },
|
|
113
|
+
{ value: 'light', label: a.light, icon: <Sun size={12} /> },
|
|
114
|
+
]}
|
|
115
|
+
value={themePref}
|
|
116
|
+
onChange={v => {
|
|
117
|
+
setThemePref(v);
|
|
118
|
+
localStorage.setItem('theme', v);
|
|
119
|
+
const isDark = v === 'system'
|
|
120
|
+
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
121
|
+
: v === 'dark';
|
|
122
|
+
setDark(isDark);
|
|
123
|
+
document.documentElement.classList.toggle('dark', isDark);
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
</SettingGroup>
|
|
70
127
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
128
|
+
{/* Language */}
|
|
129
|
+
<SettingGroup icon={<Globe size={14} />} label={a.language}>
|
|
130
|
+
<SegmentedControl
|
|
131
|
+
options={[
|
|
132
|
+
{ value: 'system', label: a.system, icon: <Monitor size={12} /> },
|
|
133
|
+
{ value: 'en', label: 'English' },
|
|
134
|
+
{ value: 'zh', label: '中文' },
|
|
135
|
+
]}
|
|
136
|
+
value={localePref}
|
|
137
|
+
onChange={v => {
|
|
138
|
+
setLocalePref(v);
|
|
139
|
+
localStorage.setItem('locale', v);
|
|
140
|
+
const resolved: Locale = v === 'system'
|
|
141
|
+
? (navigator.language.startsWith('zh') ? 'zh' : 'en')
|
|
142
|
+
: v as Locale;
|
|
143
|
+
setLocale(resolved);
|
|
144
|
+
// Sync cookie for SSR (write resolved value, not 'system')
|
|
145
|
+
document.cookie = `locale=${resolved};path=/;max-age=31536000;SameSite=Lax`;
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
</SettingGroup>
|
|
89
149
|
|
|
90
|
-
<p className="text-xs text-muted-foreground">{
|
|
150
|
+
<p className="text-xs text-muted-foreground/60 px-0.5">{a.browserNote}</p>
|
|
91
151
|
|
|
92
152
|
{/* Keyboard Shortcuts */}
|
|
93
153
|
<div className="border-t border-border pt-4">
|
|
94
154
|
<button
|
|
95
155
|
type="button"
|
|
96
156
|
onClick={() => setShowShortcuts(!showShortcuts)}
|
|
97
|
-
className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground
|
|
157
|
+
className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
|
|
98
158
|
>
|
|
99
159
|
{showShortcuts ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
100
160
|
{t.settings.tabs.shortcuts}
|
|
101
161
|
</button>
|
|
102
162
|
{showShortcuts && (
|
|
103
|
-
<div className="mt-3 space-y-
|
|
163
|
+
<div className="mt-3 space-y-0.5">
|
|
104
164
|
{t.shortcuts.map((s: { readonly description: string; readonly keys: readonly string[] }, i: number) => (
|
|
105
|
-
<div key={i} className="flex items-center justify-between py-
|
|
106
|
-
<span className="text-
|
|
165
|
+
<div key={i} className="flex items-center justify-between py-1.5 px-1">
|
|
166
|
+
<span className="text-xs text-foreground">{s.description}</span>
|
|
107
167
|
<div className="flex items-center gap-1">
|
|
108
168
|
{s.keys.map((k: string, j: number) => (
|
|
109
|
-
<kbd key={j} className="px-
|
|
169
|
+
<kbd key={j} className="px-1.5 py-0.5 text-2xs font-mono bg-muted border border-border rounded text-muted-foreground">{k}</kbd>
|
|
110
170
|
))}
|
|
111
171
|
</div>
|
|
112
172
|
</div>
|
|
@@ -15,10 +15,13 @@ const LocaleContext = createContext<LocaleContextValue>({
|
|
|
15
15
|
t: messages['en'],
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
/** Read locale from localStorage (canonical client source) */
|
|
18
|
+
/** Read locale from localStorage (canonical client source), resolving 'system' */
|
|
19
19
|
function getLocaleSnapshot(): Locale {
|
|
20
20
|
const saved = localStorage.getItem('locale');
|
|
21
|
-
|
|
21
|
+
if (saved === 'zh') return 'zh';
|
|
22
|
+
if (saved === 'en') return 'en';
|
|
23
|
+
// 'system' or null — detect from browser
|
|
24
|
+
return navigator.language.startsWith('zh') ? 'zh' : 'en';
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
interface LocaleProviderProps {
|
|
@@ -39,8 +42,7 @@ export function LocaleProvider({ children, ssrLocale = 'en' }: LocaleProviderPro
|
|
|
39
42
|
);
|
|
40
43
|
|
|
41
44
|
const setLocale = (l: Locale) => {
|
|
42
|
-
|
|
43
|
-
// Sync cookie so next SSR render matches
|
|
45
|
+
// Only write resolved locale to cookie + html lang (not localStorage — caller manages that)
|
|
44
46
|
document.cookie = `locale=${l};path=/;max-age=31536000;SameSite=Lax`;
|
|
45
47
|
document.documentElement.lang = l === 'zh' ? 'zh' : 'en';
|
|
46
48
|
window.dispatchEvent(new Event('mindos-locale-change'));
|
package/app/lib/i18n-en.ts
CHANGED
package/app/lib/i18n-zh.ts
CHANGED
package/package.json
CHANGED