yana-web 0.1.0

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.
@@ -0,0 +1,355 @@
1
+ // Yana Mobile — Agents + Providers + Settings
2
+ /* ---------- Agents ---------- */
3
+ function MAgentCard({ a }) {
4
+ return (
5
+ <div className="glass" style={{ borderRadius: "var(--r-lg)", padding: "15px 16px", display: "flex", flexDirection: "column", gap: 10 }}>
6
+ <div style={{ display: "flex", alignItems: "center", gap: 11 }}>
7
+ <div style={{
8
+ width: 36, height: 36, borderRadius: 12, flex: "none", display: "grid", placeItems: "center",
9
+ fontSize: 14.5, fontWeight: 500, color: "var(--primary)",
10
+ background: "var(--primary-soft)", boxShadow: "inset 0 1px 0 rgba(255,255,255,.5)",
11
+ }}>{a.name[0]}</div>
12
+ <div style={{ lineHeight: 1.25, minWidth: 0, flex: 1 }}>
13
+ <div style={{ fontSize: 14.5, fontWeight: 500, display: "flex", alignItems: "center", gap: 7 }}>
14
+ {a.name}
15
+ {a.core && <span className="chip gold" style={{ fontSize: 10, padding: "1px 7px" }}>Core</span>}
16
+ </div>
17
+ <div style={{ fontSize: 12, color: "var(--ink-3)" }}>{a.role}</div>
18
+ </div>
19
+ <span className={"dot " + (a.status === "active" ? "on" : "idle")}></span>
20
+ </div>
21
+ <div style={{ fontSize: 12.5, color: "var(--ink-2)", lineHeight: 1.5 }}>{a.specialty}</div>
22
+ <div style={{
23
+ fontSize: 12, color: a.status === "active" ? "var(--primary)" : "var(--ink-3)",
24
+ display: "flex", alignItems: "center", gap: 7, paddingTop: 9, borderTop: "1px solid var(--border)",
25
+ }}>
26
+ {a.status === "active" ? Icons.spark(13) : Icons.clock(13)} {a.load}
27
+ </div>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ function MAgents() {
33
+ const D = window.YANA;
34
+ const [filter, setFilter] = React.useState("all");
35
+ const filters = [["all", L("All", "Tất cả")], ["active", L("Active", "Đang chạy")], ["idle", L("Idle", "Nghỉ")]];
36
+ const list = filter === "all" ? D.agents : D.agents.filter((a) => a.status === filter);
37
+ const rest = Math.max(0, D.stats.agents - D.agents.length);
38
+ return (
39
+ <div data-screen-label="Agent Space" style={{ display: "flex", flexDirection: "column", gap: "var(--gap)" }}>
40
+ <MHead title={L("Agents", "Tác nhân")} sub={D.stats.agentsActive + L(" of ", " / ") + D.stats.agents + L(" active · Navigator orchestrates, Sentinel reviews", " hoạt động · Navigator điều phối, Sentinel giám sát")}>
41
+ <button className="pill-primary" style={{ padding: "8px 13px" }}>{Icons.plus(15)} {L("New", "Mới")}</button>
42
+ </MHead>
43
+ <div className="hscroll">
44
+ {filters.map(([id, lbl]) => (
45
+ <button key={id} className="fchip" data-on={filter === id ? "1" : "0"} onClick={() => setFilter(id)}>{lbl}</button>
46
+ ))}
47
+ </div>
48
+ <div style={{ display: "flex", flexDirection: "column", gap: 11 }}>
49
+ {list.map((a) => <MAgentCard key={a.id} a={a} />)}
50
+ {rest > 0 && <div style={{
51
+ borderRadius: "var(--r-lg)", border: "1.5px dashed var(--border-strong)",
52
+ display: "grid", placeItems: "center", minHeight: 64, color: "var(--ink-3)", fontSize: 12.5, textAlign: "center", padding: 14,
53
+ }}>+ {rest} {L("more specialist agents", "tác nhân chuyên môn khác")}</div>}
54
+ </div>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ /* ---------- Providers ---------- */
60
+ function MProviderCard({ p }) {
61
+ const keyless = p.id === "ollama" || p.id === "9router";
62
+ const vault = typeof YanaVault !== "undefined" ? YanaVault : null;
63
+ const [hasKey, setHasKey] = React.useState(() => keyless || !!(vault && vault.getKey(p.id)));
64
+ const connected = hasKey;
65
+
66
+ async function promptKey() {
67
+ if (!vault) { window.alert(L("Vault not available.", "Vault chưa sẵn sàng.")); return; }
68
+ const current = vault.getKey(p.id) || "";
69
+ const raw = window.prompt(
70
+ L("API key for ", "API key cho ") + p.name + L(" (leave blank to clear):", " (để trống để xóa):"),
71
+ current
72
+ );
73
+ if (raw === null) return;
74
+ const trimmed = raw.trim();
75
+ if (trimmed) { await vault.setKey(p.id, trimmed); setHasKey(true); }
76
+ else { vault.removeKey(p.id); setHasKey(false); }
77
+ }
78
+
79
+ const keyDisplay = keyless
80
+ ? L("Keyless", "Không cần key")
81
+ : hasKey
82
+ ? (vault && vault.getKey(p.id) || "").slice(0, 8) + "····"
83
+ : L("Not set", "Chưa đặt");
84
+
85
+ return (
86
+ <div className="glass" style={{ borderRadius: "var(--r-lg)", padding: "15px 16px", display: "flex", flexDirection: "column", gap: 11 }}>
87
+ <div style={{ display: "flex", alignItems: "center", gap: 11 }}>
88
+ <div style={{
89
+ width: 36, height: 36, borderRadius: 12, flex: "none", display: "grid", placeItems: "center",
90
+ fontSize: 14.5, fontWeight: 500, color: "var(--primary)",
91
+ background: "var(--primary-soft)", boxShadow: "inset 0 1px 0 rgba(255,255,255,.5)",
92
+ }}>{p.name[0]}</div>
93
+ <div style={{ lineHeight: 1.25, minWidth: 0, flex: 1 }}>
94
+ <div style={{ fontSize: 14.5, fontWeight: 500 }}>{p.name}</div>
95
+ <div style={{ fontSize: 12, color: "var(--ink-3)" }}>{p.company}</div>
96
+ </div>
97
+ <span className={"chip " + (connected ? "" : "gold")} style={{ fontSize: 11, flex: "none" }}>
98
+ <span className={"dot " + (connected ? "on" : "idle")} style={{ width: 6, height: 6, boxShadow: "none" }}></span>
99
+ {connected ? L("Connected", "Đã nối") : L("Standby", "Chờ")}
100
+ </span>
101
+ </div>
102
+ <div style={{ fontSize: 12.5, color: "var(--ink-2)", lineHeight: 1.5 }}>{p.role}</div>
103
+ <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
104
+ {p.models.map((m) => <span key={m} className="chip neutral" style={{ fontSize: 11 }}>{m}</span>)}
105
+ </div>
106
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, paddingTop: 10, borderTop: "1px solid var(--border)" }}>
107
+ <div style={{ lineHeight: 1.35, minWidth: 0 }}>
108
+ <div style={{ fontSize: 10.5, color: "var(--ink-3)" }}>{L("Key", "Khoá")}</div>
109
+ <div style={{ fontSize: 11.5, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: 160 }}>{keyDisplay}</div>
110
+ </div>
111
+ {!keyless && (
112
+ <button onClick={promptKey} className={"pill-" + (hasKey ? "neutral" : "primary")} style={{ padding: "6px 13px", fontSize: 12 }}>
113
+ {hasKey ? L("Change", "Đổi") : L("Set key", "Thêm key")}
114
+ </button>
115
+ )}
116
+ </div>
117
+ </div>
118
+ );
119
+ }
120
+
121
+ function MProviders() {
122
+ const D = window.YANA;
123
+ const vault = typeof YanaVault !== "undefined" ? YanaVault : null;
124
+ const connected = vault ? D.providers.filter((p) => p.id === "ollama" || p.id === "9router" || vault.getKey(p.id)).length : 0;
125
+ return (
126
+ <div data-screen-label="Providers" style={{ display: "flex", flexDirection: "column", gap: "var(--gap)" }}>
127
+ <MHead title={L("Providers", "Nhà cung cấp")} sub={connected + " / " + D.providers.length + L(" connected", " đã nối")} />
128
+ <div style={{ display: "flex", flexDirection: "column", gap: 11 }}>
129
+ {D.providers.map((p) => <MProviderCard key={p.id} p={p} />)}
130
+ </div>
131
+ </div>
132
+ );
133
+ }
134
+
135
+ /* ---------- Settings ---------- */
136
+ const M_THEME_PREVIEWS = [
137
+ { label: "Lotus Dawn 🌸", accent: "#b96b80", sky: "linear-gradient(160deg, #faf5f3 30%, #f2dfdc 100%)", wash: "rgba(236,196,134,.45)" },
138
+ { label: "Jade Lake 🌿", accent: "#2f7e6e", sky: "linear-gradient(160deg, #f6faf7 30%, #ddeee7 100%)", wash: "rgba(122,184,168,.40)" },
139
+ { label: "Morning Mist ☁️", accent: "#4a7a6a", sky: "linear-gradient(160deg, #f8f7f4 30%, #ecebe5 100%)", wash: "rgba(214,222,214,.55)" },
140
+ { label: "Glass Silver ✨", accent: "#3a7ca5", sky: "linear-gradient(160deg, #f3f6fa 30%, #dde6ef 100%)", wash: "rgba(168,199,224,.45)" },
141
+ ];
142
+
143
+ function MThemeCard({ p, active, onPick }) {
144
+ return (
145
+ <button onClick={onPick} style={{ background: "none", border: "none", padding: 0, cursor: "pointer", textAlign: "center", color: "inherit", flex: "none" }}>
146
+ <div style={{
147
+ width: 96, height: 60, borderRadius: 12, background: p.sky, position: "relative", overflow: "hidden",
148
+ boxShadow: active ? `0 0 0 2px var(--bg-base), 0 0 0 4px ${p.accent}` : "inset 0 0 0 1px var(--border)",
149
+ transition: "box-shadow .15s",
150
+ }}>
151
+ <div style={{ position: "absolute", inset: 0, background: `radial-gradient(70px 36px at 80% 100%, ${p.wash}, transparent 70%)` }}></div>
152
+ <div style={{ position: "absolute", left: 7, top: 7, bottom: 7, width: 22, borderRadius: 5, background: "rgba(255,255,255,.65)" }}></div>
153
+ <div style={{ position: "absolute", left: 34, top: 7, right: 7, height: 18, borderRadius: 5, background: "rgba(255,255,255,.6)" }}></div>
154
+ <div style={{ position: "absolute", left: 34, top: 29, right: 7, bottom: 7, borderRadius: 5, background: "rgba(255,255,255,.5)" }}></div>
155
+ <div style={{ position: "absolute", left: 11, top: 11, width: 9, height: 9, borderRadius: 3, background: p.accent, opacity: .9 }}></div>
156
+ </div>
157
+ <div style={{ fontSize: 11.5, marginTop: 7, fontWeight: active ? 500 : 400, color: active ? "var(--ink)" : "var(--ink-2)" }}>{p.label}</div>
158
+ </button>
159
+ );
160
+ }
161
+
162
+ function MSwitch({ value, onChange }) {
163
+ return (
164
+ <button onClick={() => onChange(!value)} aria-pressed={value} style={{
165
+ width: 42, height: 25, borderRadius: 99, border: "none", cursor: "pointer", flex: "none",
166
+ background: value ? "var(--primary)" : "rgba(var(--shadow-rgb), .15)",
167
+ position: "relative", transition: "background .18s",
168
+ }}>
169
+ <span style={{
170
+ position: "absolute", top: 2, left: value ? 19 : 2, width: 21, height: 21, borderRadius: "50%",
171
+ background: "white", boxShadow: "0 1px 3px rgba(0,0,0,.25)", transition: "left .18s",
172
+ }}></span>
173
+ </button>
174
+ );
175
+ }
176
+
177
+ function MSeg({ options, value, onChange }) {
178
+ return (
179
+ <div style={{ display: "inline-flex", gap: 2, padding: 3, borderRadius: 10, background: "rgba(var(--shadow-rgb), .07)" }}>
180
+ {options.map((o) => (
181
+ <button key={o} onClick={() => onChange(o)} style={{
182
+ padding: "6px 13px", borderRadius: 8, border: "none", cursor: "pointer", fontSize: 12.5,
183
+ fontWeight: value === o ? 500 : 400,
184
+ background: value === o ? "rgba(var(--surface-rgb), .95)" : "transparent",
185
+ boxShadow: value === o ? "0 1px 3px rgba(var(--shadow-rgb), .15)" : "none",
186
+ color: "var(--ink)", transition: "background .15s",
187
+ }}>{o}</button>
188
+ ))}
189
+ </div>
190
+ );
191
+ }
192
+
193
+ function MSliderRow({ label, value, onChange }) {
194
+ return (
195
+ <div style={{ display: "grid", gridTemplateColumns: "92px 1fr 38px", alignItems: "center", gap: 12, padding: "8px 0" }}>
196
+ <span style={{ fontSize: 13 }}>{label}</span>
197
+ <input type="range" min="0" max="100" value={value} onChange={(e) => onChange(+e.target.value)}
198
+ style={{ width: "100%", accentColor: "var(--primary)", height: 4 }} />
199
+ <span style={{ fontSize: 12, color: "var(--ink-3)", textAlign: "right" }}>{value}%</span>
200
+ </div>
201
+ );
202
+ }
203
+
204
+ const M_ACCENTS = ["#2f7e6e", "#56949f", "#3a7ca5", "#7d6aa8", "#b96b80", "#b07a4f", "#b78f3d", "#6f8f5a", "#5b7282"];
205
+
206
+ function MAboutField({ id, label, hint, placeholder }) {
207
+ const key = "yana.about." + id;
208
+ const [v, setV] = React.useState(() => localStorage.getItem(key) || "");
209
+ const [saved, setSaved] = React.useState(false);
210
+ const timer = React.useRef(null);
211
+ function onChange(e) {
212
+ const val = e.target.value;
213
+ setV(val);
214
+ localStorage.setItem(key, val);
215
+ setSaved(false);
216
+ clearTimeout(timer.current);
217
+ timer.current = setTimeout(() => setSaved(true), 800);
218
+ }
219
+ return (
220
+ <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
221
+ <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 10 }}>
222
+ <label style={{ fontSize: 13, fontWeight: 500 }}>{label}</label>
223
+ <span style={{ fontSize: 10.5, color: "var(--good)", opacity: saved ? 1 : 0, transition: "opacity .3s", display: "inline-flex", alignItems: "center", gap: 4, flex: "none" }}>
224
+ {Icons.check(11)} {L("Planted", "Đã trồng")}
225
+ </span>
226
+ </div>
227
+ {hint && <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: -3 }}>{hint}</div>}
228
+ <textarea value={v} onChange={onChange} rows={3} placeholder={placeholder}
229
+ style={{
230
+ width: "100%", resize: "vertical", padding: "10px 13px", borderRadius: "var(--r-sm)",
231
+ border: "1px solid var(--border)", background: "rgba(var(--surface-rgb), .6)", color: "var(--ink)",
232
+ fontFamily: "inherit", fontSize: 13.5, lineHeight: 1.55, outline: "none",
233
+ }}
234
+ onFocus={(e) => e.target.style.borderColor = "var(--primary)"}
235
+ onBlur={(e) => e.target.style.borderColor = "var(--border)"} />
236
+ </div>
237
+ );
238
+ }
239
+
240
+ function MSettingRow({ label, desc, value }) {
241
+ return (
242
+ <div className="mrow" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 14 }}>
243
+ <div style={{ lineHeight: 1.35, minWidth: 0 }}>
244
+ <div style={{ fontSize: 13.5, fontWeight: 500 }}>{label}</div>
245
+ {desc && <div style={{ fontSize: 11.5, color: "var(--ink-3)" }}>{desc}</div>}
246
+ </div>
247
+ <span className="chip neutral" style={{ flex: "none", fontSize: 11 }}>{value}</span>
248
+ </div>
249
+ );
250
+ }
251
+
252
+ function _mDetectTz() {
253
+ try {
254
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
255
+ const offMin = -new Date().getTimezoneOffset();
256
+ const sign = offMin >= 0 ? "+" : "−";
257
+ const h = Math.floor(Math.abs(offMin) / 60);
258
+ const m = Math.abs(offMin) % 60;
259
+ return "GMT" + sign + h + (m ? ":" + String(m).padStart(2, "0") : "") + " · " + tz.split("/").pop().replace(/_/g, " ");
260
+ } catch (_) { return "UTC"; }
261
+ }
262
+
263
+ const M_PROVIDER_NAMES = { claude: "Claude", openai: "OpenAI", gemini: "Gemini", groq: "Groq", deepseek: "DeepSeek", openrouter: "OpenRouter", "9router": "9Router", ollama: "Ollama" };
264
+
265
+ function MSettings({ t, setTweak }) {
266
+ const _p = mGetProviderConfig().provider;
267
+ const _orchModel = M_CHAT_MODELS[_p] || _p;
268
+ const _wname = localStorage.getItem("yana.workspace.name") ||
269
+ ((window.YANA.username ? window.YANA.username + "'s Lake" : L("Yana's Lake", "Mặt hồ của Yana")));
270
+ const _chain = (() => {
271
+ const order = ["claude", "openai", "gemini", "groq", "deepseek", "openrouter", "9router"];
272
+ if (typeof YanaVault === "undefined") return "—";
273
+ const found = order.filter((id) => YanaVault.getKey(id));
274
+ return found.map((id) => M_PROVIDER_NAMES[id] || id).join(" → ") || L("None — add key in Providers", "Chưa có — thêm key ở Providers");
275
+ })();
276
+ return (
277
+ <div data-screen-label="Settings" style={{ display: "flex", flexDirection: "column", gap: "var(--gap)" }}>
278
+ <MHead title={L("Settings", "Cài đặt")} sub={L("Quiet defaults. Everything supervised by YAMTAM Core.", "Mặc định tĩnh lặng. Mọi thứ do YAMTAM Core giám sát.")} />
279
+
280
+ <MCard title={L("Appearance", "Giao diện")}>
281
+ <div className="hscroll" style={{ marginBottom: 6 }}>
282
+ {M_THEME_PREVIEWS.map((p) => (
283
+ <MThemeCard key={p.label} p={p} active={t.theme === p.label} onPick={() => setTweak("theme", p.label)} />
284
+ ))}
285
+ </div>
286
+
287
+ <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "13px 0", borderTop: "1px solid var(--border)", flexWrap: "wrap" }}>
288
+ <span style={{ fontSize: 13, fontWeight: 500 }}>{L("Accent", "Màu nhấn")}</span>
289
+ <div style={{ display: "flex", gap: 9, alignItems: "center", flexWrap: "wrap" }}>
290
+ <button onClick={() => setTweak("accent", "")} title="Theme default" style={{
291
+ width: 24, height: 24, borderRadius: "50%", cursor: "pointer", padding: 0, border: "none",
292
+ background: "conic-gradient(#2f7e6e, #3a7ca5, #b96b80, #b78f3d, #2f7e6e)",
293
+ boxShadow: !t.accent ? "0 0 0 2px var(--bg-base), 0 0 0 4px var(--ink-3)" : "inset 0 0 0 1px rgba(0,0,0,.08)",
294
+ }}></button>
295
+ {M_ACCENTS.map((c) => (
296
+ <button key={c} onClick={() => setTweak("accent", c)} style={{
297
+ width: 24, height: 24, borderRadius: "50%", cursor: "pointer", padding: 0, border: "none", background: c,
298
+ boxShadow: t.accent === c ? `0 0 0 2px var(--bg-base), 0 0 0 4px ${c}` : "inset 0 0 0 1px rgba(0,0,0,.08)",
299
+ }}></button>
300
+ ))}
301
+ </div>
302
+ </div>
303
+
304
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, padding: "13px 0", borderTop: "1px solid var(--border)" }}>
305
+ <span style={{ fontSize: 13, fontWeight: 500 }}>{L("Density", "Mật độ")}</span>
306
+ <MSeg options={["Compact", "Regular", "Spacious"]} value={t.layout} onChange={(v) => setTweak("layout", v)} />
307
+ </div>
308
+
309
+ <div style={{ padding: "8px 0 4px", borderTop: "1px solid var(--border)" }}>
310
+ <MSliderRow label={L("Blur", "Mờ")} value={t.blur} onChange={(v) => setTweak("blur", v)} />
311
+ <MSliderRow label={L("Transparency", "Trong suốt")} value={t.transparency} onChange={(v) => setTweak("transparency", v)} />
312
+ <MSliderRow label={L("Reflection", "Phản chiếu")} value={t.reflection} onChange={(v) => setTweak("reflection", v)} />
313
+ <MSliderRow label={L("Depth", "Chiều sâu")} value={t.depth} onChange={(v) => setTweak("depth", v)} />
314
+ </div>
315
+
316
+ <div style={{ paddingTop: 8, borderTop: "1px solid var(--border)" }}>
317
+ {[[L("Show agents on Lake", "Tác nhân trên Mặt hồ"), "showAgents"], [L("Show missions on Lake", "Nhiệm vụ trên Mặt hồ"), "showMissions"], [L("Show Memory Garden", "Vườn ký ức"), "showMemory"], [L("Show system status", "Trạng thái hệ thống"), "showSystem"]].map(([label, key]) => (
318
+ <div key={key} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, padding: "9px 0" }}>
319
+ <span style={{ fontSize: 13 }}>{label}</span>
320
+ <MSwitch value={t[key]} onChange={(v) => setTweak(key, v)} />
321
+ </div>
322
+ ))}
323
+ </div>
324
+ </MCard>
325
+
326
+ <MCard title={L("About you", "Về bạn")} aside={<span className="chip pink" style={{ fontSize: 10.5 }}>{Icons.memory(12)} {L("Pinned", "Đã ghim")}</span>}>
327
+ <p style={{ margin: "0 0 14px", fontSize: 12.5, color: "var(--ink-2)", lineHeight: 1.55 }}>
328
+ {L("Yana reads this before every mission. The more honestly you describe yourself, the better she routes and plans for you.", "Yana đọc phần này trước mỗi nhiệm vụ. Bạn mô tả càng thật, Yana càng định tuyến và lập kế hoạch tốt hơn.")}
329
+ </p>
330
+ <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
331
+ <MAboutField id="who" label={L("Who you are", "Bạn là ai")} hint={L("Role, what you're building, how you work", "Vai trò, đang xây gì, cách làm việc")} placeholder={L("e.g. I'm a system builder. I think in workflows, not code.", "vd: Tôi là người dựng hệ thống. Tôi nghĩ theo quy trình, không phải code.")} />
332
+ <MAboutField id="strengths" label={L("Strengths", "Điểm mạnh")} hint={L("What Yana should lean on", "Điều Yana nên dựa vào")} placeholder={L("e.g. Big-picture architecture, fast decisions.", "vd: Kiến trúc tổng thể, quyết định nhanh.")} />
333
+ <MAboutField id="style" label={L("How Yana should respond", "Cách Yana trả lời")} hint={L("Tone, length, language", "Giọng, độ dài, ngôn ngữ")} placeholder={L("e.g. Calm and brief. No hype.", "vd: Bình tĩnh, ngắn gọn. Không hô hào.")} />
334
+ </div>
335
+ </MCard>
336
+
337
+ <MCard title={L("Workspace", "Không gian")}>
338
+ <MSettingRow label={L("Workspace name", "Tên không gian")} value={_wname} />
339
+ <MSettingRow label={L("Timezone", "Múi giờ")} desc={L("Detected from device", "Phát hiện từ thiết bị")} value={_mDetectTz()} />
340
+ </MCard>
341
+ <MCard title={L("Orchestration", "Điều phối")}>
342
+ <MSettingRow label={L("Default orchestrator", "Điều phối mặc định")} desc={L("Plans and delegates missions", "Lập kế hoạch & giao việc")} value={"Navigator · " + _orchModel} />
343
+ <MSettingRow label={L("Active provider", "Nhà cung cấp hiện dùng")} desc={L("First key you have set", "Key đầu tiên đã cài")} value={M_PROVIDER_NAMES[_p] || _p} />
344
+ <MSettingRow label={L("Fallback chain", "Chuỗi dự phòng")} value={_chain} />
345
+ </MCard>
346
+ <MCard title={L("Safety", "An toàn")}>
347
+ <MSettingRow label={L("Gate mode", "Chế độ cổng")} desc={L("Every action is reviewed", "Mọi hành động được duyệt")} value={L("Strict", "Nghiêm ngặt")} />
348
+ <MSettingRow label={L("Merge protection", "Bảo vệ merge")} desc={L("Sentinel sign-off before main", "Sentinel duyệt trước main")} value={L("On", "Bật")} />
349
+ <MSettingRow label={L("Incident retention", "Lưu sự cố")} value={L("90 days", "90 ngày")} />
350
+ </MCard>
351
+ </div>
352
+ );
353
+ }
354
+
355
+ Object.assign(window, { MAgents, MProviders, MSettings });
@@ -0,0 +1,180 @@
1
+ /* ============================================================
2
+ Yana Mobile — layout shell on top of yana/themes.css tokens.
3
+ A phone-shaped app column, centered on the lake on wider screens,
4
+ full-bleed on a real handset. Translucent top bar + tab bar,
5
+ slide-up sheets — native iOS rhythm over Yana's liquid glass.
6
+ ============================================================ */
7
+
8
+ html, body { height: 100%; overflow: hidden; }
9
+ #root { height: 100%; }
10
+ body { overscroll-behavior: none; }
11
+
12
+ /* centers the app column on the lake */
13
+ .app-stage { position: fixed; inset: 0; z-index: 1; display: grid; place-items: center; }
14
+
15
+ .app-mobile {
16
+ position: relative;
17
+ width: 100%;
18
+ max-width: 460px;
19
+ height: 100dvh;
20
+ display: flex;
21
+ flex-direction: column;
22
+ overflow: hidden;
23
+ }
24
+ @media (min-width: 500px) {
25
+ .app-mobile {
26
+ height: min(900px, calc(100dvh - 36px));
27
+ border-radius: 34px;
28
+ border: 1px solid var(--border);
29
+ box-shadow:
30
+ 0 1px 0 rgba(255,255,255,.5) inset,
31
+ 0 40px 90px rgba(var(--shadow-rgb), .22);
32
+ }
33
+ }
34
+
35
+ /* ---------- Top bar (translucent) ---------- */
36
+ .mtopbar {
37
+ flex: none;
38
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
39
+ padding: calc(env(safe-area-inset-top, 0px) + 11px) 16px 11px;
40
+ background: rgba(var(--surface-rgb), calc(0.72 - var(--alpha) * 0.18));
41
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-max) * var(--blur))) saturate(1.2);
42
+ backdrop-filter: blur(calc(var(--glass-blur-max) * var(--blur))) saturate(1.2);
43
+ border-bottom: 1px solid var(--border);
44
+ z-index: 20;
45
+ }
46
+ .mtopbar-l { display: flex; align-items: center; gap: 8px; min-width: 0; }
47
+ .mtopbar-title { font-size: 17px; font-weight: 500; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
48
+ .mtopbar-r { display: flex; align-items: center; gap: 8px; flex: none; }
49
+
50
+ .icon-btn {
51
+ width: 36px; height: 36px; border-radius: 50%; border: none; cursor: pointer; flex: none;
52
+ display: grid; place-items: center; color: var(--ink-2);
53
+ background: rgba(var(--surface-rgb), .5);
54
+ }
55
+ .icon-btn:active { background: var(--primary-soft); color: var(--primary); }
56
+
57
+ .lang-pill {
58
+ height: 30px; padding: 0 12px; border-radius: 99px; border: 1px solid var(--border);
59
+ background: rgba(var(--surface-rgb), .5); color: var(--ink-2);
60
+ font: 600 11.5px/1 var(--font-ui); letter-spacing: .06em; cursor: pointer;
61
+ }
62
+ .lang-pill:active { color: var(--primary); border-color: var(--primary-soft); }
63
+
64
+ .avatar-btn {
65
+ width: 32px; height: 32px; border-radius: 50%; border: none; cursor: pointer; flex: none;
66
+ color: white; font: 600 12px/1 var(--font-ui); display: grid; place-items: center;
67
+ background: linear-gradient(145deg, var(--gold), color-mix(in oklab, var(--gold) 55%, white));
68
+ box-shadow: 0 2px 8px color-mix(in oklab, var(--gold) 30%, transparent);
69
+ }
70
+
71
+ /* ---------- Main scroll region ---------- */
72
+ .mmain {
73
+ flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
74
+ display: flex; flex-direction: column; gap: var(--gap);
75
+ padding: 18px 16px calc(22px + env(safe-area-inset-bottom, 0px));
76
+ }
77
+ .mmain.flush { padding: 0; gap: 0; overflow: hidden; }
78
+ .mmain::-webkit-scrollbar { width: 0; }
79
+
80
+ /* large title block */
81
+ .mhead { margin: 2px 0 4px; }
82
+ .mhead-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; }
83
+ .mhead-title { margin: 0; font-size: 27px; }
84
+ .mhead-sub { margin: 6px 0 0; font-size: 13px; color: var(--ink-2); line-height: 1.45; }
85
+
86
+ /* ---------- Bottom tab bar (translucent) ---------- */
87
+ .mtabbar {
88
+ flex: none;
89
+ display: flex; gap: 2px;
90
+ padding: 7px 6px calc(env(safe-area-inset-bottom, 0px) + 7px);
91
+ background: rgba(var(--surface-rgb), calc(0.76 - var(--alpha) * 0.16));
92
+ -webkit-backdrop-filter: blur(calc(var(--glass-blur-max) * var(--blur))) saturate(1.25);
93
+ backdrop-filter: blur(calc(var(--glass-blur-max) * var(--blur))) saturate(1.25);
94
+ border-top: 1px solid var(--border);
95
+ z-index: 20;
96
+ }
97
+ .mtab {
98
+ flex: 1; min-width: 0; border: none; background: none; cursor: pointer;
99
+ display: flex; flex-direction: column; align-items: center; gap: 3px;
100
+ padding: 6px 2px 4px; min-height: 50px; color: var(--ink-3);
101
+ font: 500 10px/1 var(--font-ui); letter-spacing: .01em;
102
+ transition: color .15s;
103
+ }
104
+ .mtab[data-on="1"] { color: var(--primary); }
105
+ .mtab span { white-space: nowrap; }
106
+ .mtab .tab-ic { transition: transform .18s cubic-bezier(.3,.7,.4,1); }
107
+ .mtab[data-on="1"] .tab-ic { transform: translateY(-1px); }
108
+
109
+ /* ---------- Slide-up sheet ---------- */
110
+ .sheet-backdrop {
111
+ position: absolute; inset: 0; z-index: 40;
112
+ background: rgba(var(--shadow-rgb), .32);
113
+ -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
114
+ opacity: 0; transition: opacity .28s ease;
115
+ }
116
+ .sheet-backdrop[data-show="1"] { opacity: 1; }
117
+ .sheet {
118
+ position: absolute; left: 0; right: 0; bottom: 0; z-index: 41;
119
+ max-height: 86%; display: flex; flex-direction: column;
120
+ padding: 8px 18px calc(20px + env(safe-area-inset-bottom, 0px));
121
+ background: rgba(var(--surface-rgb), calc(0.95 - var(--alpha) * 0.18));
122
+ -webkit-backdrop-filter: blur(34px) saturate(1.5); backdrop-filter: blur(34px) saturate(1.5);
123
+ border-top: 1px solid rgba(255,255,255,.5);
124
+ border-radius: 26px 26px 0 0;
125
+ box-shadow: 0 -10px 40px rgba(var(--shadow-rgb), .18);
126
+ transform: translateY(100%); transition: transform .3s cubic-bezier(.32,.72,.3,1);
127
+ }
128
+ .sheet[data-show="1"] { transform: translateY(0); }
129
+ .sheet-grab { width: 38px; height: 5px; border-radius: 99px; background: var(--border-strong); margin: 4px auto 12px; flex: none; }
130
+ .sheet-title { font-size: 12px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); margin-bottom: 12px; }
131
+ .sheet-body { overflow-y: auto; display: flex; flex-direction: column; gap: 12px; min-height: 0; }
132
+ .sheet-body::-webkit-scrollbar { width: 0; }
133
+
134
+ /* ---------- More sheet ---------- */
135
+ .more-grid { display: flex; flex-direction: column; gap: 6px; }
136
+ .more-item {
137
+ display: flex; align-items: center; gap: 13px; width: 100%; text-align: left;
138
+ padding: 13px 14px; border-radius: var(--r-md); border: none; cursor: pointer; color: inherit;
139
+ background: rgba(var(--surface-rgb), .55);
140
+ }
141
+ .more-item:active { background: var(--primary-soft); }
142
+ .more-ic { width: 34px; height: 34px; border-radius: 11px; flex: none; display: grid; place-items: center; color: var(--primary); background: var(--primary-soft); }
143
+ .more-lbl { flex: 1; font-size: 14.5px; font-weight: 500; }
144
+ .more-chev { color: var(--ink-3); display: inline-flex; }
145
+
146
+ /* ---------- Horizontal scroll rails ---------- */
147
+ .hscroll {
148
+ display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none;
149
+ margin: 0 -16px; padding: 2px 16px;
150
+ }
151
+ .hscroll::-webkit-scrollbar { height: 0; }
152
+ .hscroll > * { flex: none; }
153
+
154
+ /* ---------- Pill / filter chips ---------- */
155
+ .fchip {
156
+ padding: 7px 15px; border-radius: 99px; font: 500 13px/1 var(--font-ui); cursor: pointer;
157
+ border: 1px solid var(--border); background: rgba(var(--surface-rgb), .55); color: var(--ink-2);
158
+ white-space: nowrap;
159
+ }
160
+ .fchip[data-on="1"] { background: var(--primary); color: white; border-color: transparent; }
161
+
162
+ /* primary action pill */
163
+ .pill-primary {
164
+ display: inline-flex; align-items: center; gap: 7px; padding: 9px 16px; border-radius: 99px;
165
+ border: none; cursor: pointer; background: var(--primary); color: white;
166
+ font: 500 13px/1 var(--font-ui); flex: none;
167
+ box-shadow: 0 4px 14px color-mix(in oklab, var(--primary) 30%, transparent);
168
+ }
169
+ .pill-ghost {
170
+ display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: 99px;
171
+ border: 1px solid var(--border); cursor: pointer; background: rgba(var(--surface-rgb), .5);
172
+ color: var(--ink-2); font: 500 12.5px/1 var(--font-ui); flex: none;
173
+ }
174
+
175
+ /* hairline list rows inside cards */
176
+ .mrow { padding: 11px 0; border-bottom: 1px solid var(--border); }
177
+ .mrow:last-child { border-bottom: none; }
178
+ .mrow:first-child { padding-top: 2px; }
179
+
180
+ input, textarea, select, button { font-family: var(--font-ui); }