@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.
@@ -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=loc==='zh'?'zh':'en';document.cookie='locale='+loc+';path=/;max-age=31536000;SameSite=Lax'}catch(e){}})();`,
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
- import { Field, Select } from './Primitives';
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-5">
14
- <Field label={t.settings.appearance.readingFont}>
15
- <Select value={font} onChange={e => setFont(e.target.value)}>
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
- <option key={f.value} value={f.value}>{f.label}</option>
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
- </Select>
20
- <p className="text-xs text-muted-foreground mt-1.5 px-0.5" style={{ fontFamily: FONTS.find(f => f.value === font)?.style.fontFamily }}>
21
- {t.settings.appearance.fontPreview}
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
- </Field>
85
+ </SettingGroup>
24
86
 
25
- <Field label={t.settings.appearance.contentWidth}>
26
- <div className="grid grid-cols-2 gap-2">
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-2 text-sm rounded-lg border transition-colors text-left ${
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:border-border/80 hover:bg-muted'
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
- </Field>
105
+ </SettingGroup>
43
106
 
44
- <Field label={t.settings.appearance.colorTheme}>
45
- <div className="grid grid-cols-2 gap-2">
46
- {[
47
- { value: 'dark', label: t.settings.appearance.dark },
48
- { value: 'light', label: t.settings.appearance.light },
49
- ].map(opt => (
50
- <button
51
- key={opt.value}
52
- type="button"
53
- onClick={() => {
54
- const isDark = opt.value === 'dark';
55
- setDark(isDark);
56
- document.documentElement.classList.toggle('dark', isDark);
57
- localStorage.setItem('theme', opt.value);
58
- }}
59
- className={`px-3 py-2 text-sm rounded-lg border transition-colors text-left ${
60
- (opt.value === 'dark') === dark
61
- ? 'border-amber-500 bg-amber-500/10 text-foreground'
62
- : 'border-border text-muted-foreground hover:border-border/80 hover:bg-muted'
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
- <Field label={t.settings.appearance.language}>
72
- <div className="grid grid-cols-2 gap-2">
73
- {([['en', 'English'], ['zh', '中文']] as [Locale, string][]).map(([code, label]) => (
74
- <button
75
- key={code}
76
- type="button"
77
- onClick={() => setLocale(code)}
78
- className={`px-3 py-2 text-sm rounded-lg border transition-colors text-left ${
79
- locale === code
80
- ? 'border-amber-500 bg-amber-500/10 text-foreground'
81
- : 'border-border text-muted-foreground hover:border-border/80 hover:bg-muted'
82
- }`}
83
- >
84
- {label}
85
- </button>
86
- ))}
87
- </div>
88
- </Field>
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">{t.settings.appearance.browserNote}</p>
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 uppercase tracking-wider hover:text-foreground transition-colors"
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-1">
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-2 border-b border-border last:border-0">
106
- <span className="text-sm text-foreground">{s.description}</span>
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-2 py-0.5 text-xs font-mono bg-muted border border-border rounded text-foreground">{k}</kbd>
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
- return saved === 'zh' ? 'zh' : 'en';
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
- localStorage.setItem('locale', l);
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'));
@@ -248,6 +248,7 @@ export const en = {
248
248
  readingFont: 'Reading font',
249
249
  contentWidth: 'Content width',
250
250
  colorTheme: 'Color theme',
251
+ system: 'System',
251
252
  dark: 'Dark',
252
253
  light: 'Light',
253
254
  language: 'Language',
@@ -273,6 +273,7 @@ export const zh = {
273
273
  readingFont: '正文字体',
274
274
  contentWidth: '内容宽度',
275
275
  colorTheme: '颜色主题',
276
+ system: '系统',
276
277
  dark: '深色',
277
278
  light: '浅色',
278
279
  language: '语言',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.5.42",
3
+ "version": "0.5.43",
4
4
  "description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
5
5
  "keywords": [
6
6
  "mindos",