clay-server 5.1.0-beta.2 → 5.1.0-beta.3
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/bin/cli.js +9 -91
- package/lib/builtin-mates.js +7 -7
- package/lib/daemon-settings.js +11 -0
- package/lib/daemon.js +8 -37
- package/lib/linear-store.js +35 -8
- package/lib/mate-identity-file.js +39 -0
- package/lib/mate-knowledge-sync.js +1 -1
- package/lib/mate-ready-creation.js +2 -1
- package/lib/mates-identity.js +4 -4
- package/lib/mates-prompts.js +4 -3
- package/lib/mates.js +12 -11
- package/lib/os-users.js +5 -16
- package/lib/project-connection.js +0 -7
- package/lib/project-debate.js +3 -2
- package/lib/project-filesystem.js +21 -39
- package/lib/project-mate-interaction.js +7 -6
- package/lib/project-server-settings.js +31 -0
- package/lib/project-sessions.js +57 -184
- package/lib/project.js +3 -3
- package/lib/public/app.js +6 -37
- package/lib/public/css/input.css +3 -1
- package/lib/public/css/menus.css +3 -0
- package/lib/public/css/overlays.css +7 -0
- package/lib/public/css/project-settings-modal.css +56 -0
- package/lib/public/css/server-settings-system.css +125 -0
- package/lib/public/css/session-github.css +25 -16
- package/lib/public/css/settings-ui.css +247 -22
- package/lib/public/css/shared-browser.css +7 -3
- package/lib/public/css/title-bar.css +5 -1
- package/lib/public/index.html +173 -447
- package/lib/public/modules/account-island.js +9 -7
- package/lib/public/modules/admin-settings-email.js +197 -0
- package/lib/public/modules/admin-settings-invitations.js +183 -0
- package/lib/public/modules/admin-settings-projects.js +198 -0
- package/lib/public/modules/admin-settings-ui.js +100 -0
- package/lib/public/modules/admin-settings-users.js +331 -0
- package/lib/public/modules/admin.js +19 -949
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-messages.js +13 -52
- package/lib/public/modules/app-rate-limit.js +70 -12
- package/lib/public/modules/avatar.js +6 -1
- package/lib/public/modules/context-sources.js +27 -18
- package/lib/public/modules/default-ai.js +14 -7
- package/lib/public/modules/header-tui-font.js +10 -20
- package/lib/public/modules/home-mate-avatar-editor.js +2 -2
- package/lib/public/modules/linear-settings.js +3 -2
- package/lib/public/modules/markdown.js +5 -6
- package/lib/public/modules/notification-settings.js +81 -0
- package/lib/public/modules/notifications.js +44 -28
- package/lib/public/modules/personal-settings-dialog.js +51 -0
- package/lib/public/modules/profile.js +15 -6
- package/lib/public/modules/project-settings-environment.js +106 -0
- package/lib/public/modules/project-settings.js +72 -628
- package/lib/public/modules/server-settings-controls.js +316 -0
- package/lib/public/modules/server-settings-environment.js +194 -0
- package/lib/public/modules/server-settings-navigation.js +39 -0
- package/lib/public/modules/server-settings-state.js +54 -0
- package/lib/public/modules/server-settings.js +116 -788
- package/lib/public/modules/server-storage-settings.js +84 -0
- package/lib/public/modules/session-github.js +2 -1
- package/lib/public/modules/session-presence-ui.js +1 -1
- package/lib/public/modules/settings-env-values.js +34 -0
- package/lib/public/modules/settings-select.js +121 -0
- package/lib/public/modules/shared-browser.js +22 -12
- package/lib/public/modules/stt-picker.js +14 -5
- package/lib/public/modules/terminal-prefs.js +22 -0
- package/lib/public/modules/theme.js +88 -52
- package/lib/public/modules/user-settings-account.js +318 -0
- package/lib/public/modules/user-settings-appearance.js +55 -0
- package/lib/public/modules/user-settings-editors.js +24 -0
- package/lib/public/modules/user-settings-integrations.js +104 -0
- package/lib/public/modules/user-settings-layout.js +80 -0
- package/lib/public/modules/user-settings-preferences.js +48 -0
- package/lib/public/modules/user-settings-profile.js +53 -0
- package/lib/public/modules/user-settings-themes.js +38 -0
- package/lib/public/modules/user-settings-voice.js +75 -0
- package/lib/public/modules/user-settings.js +53 -480
- package/lib/public/style.css +4 -0
- package/lib/sdk-message-processor.js +16 -4
- package/lib/sdk-quota-snapshot.js +37 -0
- package/lib/server-appearance-settings.js +71 -0
- package/lib/server-linear.js +14 -8
- package/lib/server-mate-instructions.js +2 -2
- package/lib/server-mates.js +2 -1
- package/lib/server-notification-settings.js +67 -0
- package/lib/server-settings.js +4 -0
- package/lib/server-storage.js +57 -0
- package/lib/server.js +10 -2
- package/lib/sessions.js +3 -4
- package/lib/startup-projects.js +40 -0
- package/lib/storage-native-walk.js +85 -0
- package/lib/storage-usage.js +144 -0
- package/lib/users-preferences.js +0 -74
- package/lib/users.js +0 -4
- package/lib/ws-schema.js +2 -4
- package/lib/yoke/acp-process-manager.js +2 -2
- package/lib/yoke/adapters/codex.js +12 -52
- package/lib/yoke/codex-app-server.js +2 -2
- package/lib/yoke/codex-model-catalog.js +52 -0
- package/lib/yoke/codex-rate-limits.js +26 -0
- package/package.json +3 -3
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
|
|
3
|
+
function size(bytes) {
|
|
4
|
+
if (bytes < 1024) return bytes + ' B';
|
|
5
|
+
var units = ['KiB', 'MiB', 'GiB', 'TiB'];
|
|
6
|
+
var index = -1;
|
|
7
|
+
do { bytes /= 1024; index++; } while (bytes >= 1024 && index < units.length - 1);
|
|
8
|
+
return bytes.toFixed(1) + ' ' + units[index];
|
|
9
|
+
}
|
|
10
|
+
function cell(row, text, header) {
|
|
11
|
+
var el = document.createElement(header ? 'th' : 'td');
|
|
12
|
+
el.textContent = text;
|
|
13
|
+
if (header) el.scope = 'row';
|
|
14
|
+
row.appendChild(el);
|
|
15
|
+
}
|
|
16
|
+
function render(data) {
|
|
17
|
+
var projectMode = data.attribution === 'project';
|
|
18
|
+
document.getElementById('settings-storage-description').textContent = projectMode ? 'Project disk usage grouped by project owner, alongside shared Clay data.' : 'Disk usage across Clay data, registered projects and mapped Linux home directories.';
|
|
19
|
+
document.getElementById('settings-storage-owner-heading').textContent = projectMode ? 'Projects' : 'Linux account';
|
|
20
|
+
document.querySelector('.settings-storage-table').setAttribute('aria-label', projectMode ? 'Allocated disk usage by project owner' : 'Allocated disk usage by filesystem owner');
|
|
21
|
+
document.getElementById('settings-storage-explanation').textContent = (projectMode ? 'Project files are attributed to their registered project owner. Nested projects use their own owner; conflicting shared paths or hard links are unattributed. ' : 'Allocated space is attributed by Linux file owner, including directory metadata. ') + 'Shared paths count once. Symlinks and nested mounts are skipped. Results may be cached for 5 minutes. Refresh usage requests a new measurement.';
|
|
22
|
+
document.getElementById('settings-storage-attribution-note').textContent = projectMode ? 'System / unattributed includes Clay data outside projects, unowned projects and conflicting shared files. Home directories are included only if registered as projects.' : 'System / unattributed includes unmapped or shared Linux identities. Unavailable means the account cannot be uniquely mapped; it does not mean zero usage.';
|
|
23
|
+
var body = document.getElementById('settings-storage-users');
|
|
24
|
+
body.replaceChildren();
|
|
25
|
+
data.users.slice().sort(function (a, b) { return b.bytes - a.bytes; }).forEach(function (user) {
|
|
26
|
+
var row = document.createElement('tr');
|
|
27
|
+
cell(row, user.name, true);
|
|
28
|
+
cell(row, projectMode ? String(user.projects || 0) : user.linuxUser || 'No Linux account');
|
|
29
|
+
cell(row, user.mapped ? (data.scanning || data.partial ? 'At least ' : '') + size(user.bytes) : 'Unavailable');
|
|
30
|
+
cell(row, user.mapped ? user.files.toLocaleString() : '—');
|
|
31
|
+
body.appendChild(row);
|
|
32
|
+
});
|
|
33
|
+
var shared = document.createElement('tr');
|
|
34
|
+
cell(shared, 'System / unattributed', true); cell(shared, projectMode ? 'Shared / unowned' : 'Other filesystem owners');
|
|
35
|
+
cell(shared, size(data.unattributedBytes)); cell(shared, data.unattributedFiles.toLocaleString());
|
|
36
|
+
body.appendChild(shared);
|
|
37
|
+
document.getElementById('settings-storage-total').textContent = (data.partial || data.scanning ? 'At least ' : '') + size(data.totalBytes);
|
|
38
|
+
var status = document.getElementById('settings-storage-status');
|
|
39
|
+
status.textContent = (data.scanning ? 'Scan in progress; these totals are not final. ' : '') + (data.partial ? 'Partial scan: some paths were unreadable, on another filesystem, or exceeded the scan limit. ' : '') +
|
|
40
|
+
'Checked ' + new Date(data.checkedAt).toLocaleTimeString() + '. ' + data.roots + ' storage roots scanned.';
|
|
41
|
+
status.dataset.state = data.scanning ? 'pending' : data.partial ? 'error' : 'saved';
|
|
42
|
+
document.getElementById('settings-storage-results').hidden = false;
|
|
43
|
+
}
|
|
44
|
+
export async function loadStorageUsage(refresh) {
|
|
45
|
+
if (store.get('serverStorageLoading')) return;
|
|
46
|
+
clearTimeout(store.get('serverStorageTimer'));
|
|
47
|
+
var button = document.getElementById('settings-storage-refresh');
|
|
48
|
+
var status = document.getElementById('settings-storage-status');
|
|
49
|
+
store.set({ serverStorageLoading: true });
|
|
50
|
+
button.disabled = true;
|
|
51
|
+
button.textContent = 'Measuring…';
|
|
52
|
+
if (!store.get('serverStorageScanning')) status.textContent = 'Measuring disk usage. Large directories can take several seconds.';
|
|
53
|
+
status.dataset.state = 'pending';
|
|
54
|
+
try {
|
|
55
|
+
var response = await fetch('/api/server/storage' + (refresh === true ? '?refresh=1' : ''), { credentials: 'same-origin' });
|
|
56
|
+
var data = await response.json();
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
if (response.status === 401 || response.status === 403) {
|
|
59
|
+
store.set({ serverStorageUsage: null });
|
|
60
|
+
document.getElementById('settings-storage-results').hidden = true;
|
|
61
|
+
}
|
|
62
|
+
throw new Error(data.error || 'Disk usage is unavailable. Try refreshing.');
|
|
63
|
+
}
|
|
64
|
+
store.set({ serverStorageUsage: data, serverStorageScanning: !!data.scanning });
|
|
65
|
+
if (data.users) render(data);
|
|
66
|
+
if (data.scanning) {
|
|
67
|
+
status.textContent = 'Scan in progress. Project files, including node_modules, are being measured; totals are not final.';
|
|
68
|
+
store.set({ serverStorageTimer: setTimeout(function () {
|
|
69
|
+
if (store.get('serverSettingsPage') === 'storage' && !document.getElementById('server-settings').classList.contains('hidden')) loadStorageUsage();
|
|
70
|
+
}, 1500) });
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
store.set({ serverStorageScanning: false });
|
|
74
|
+
status.textContent = error.message + (store.get('serverStorageUsage') ? ' Previous measurements remain below.' : '');
|
|
75
|
+
status.dataset.state = 'error';
|
|
76
|
+
} finally {
|
|
77
|
+
store.set({ serverStorageLoading: false });
|
|
78
|
+
button.disabled = !!store.get('serverStorageScanning');
|
|
79
|
+
button.textContent = button.disabled ? 'Measuring…' : 'Refresh usage';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function initStorageUsage() {
|
|
83
|
+
document.getElementById('settings-storage-refresh').addEventListener('click', function () { loadStorageUsage(true); });
|
|
84
|
+
}
|
|
@@ -76,10 +76,11 @@ export function appendGithubWork(row, session, title) {
|
|
|
76
76
|
row.insertBefore(column, title);
|
|
77
77
|
column.appendChild(title);
|
|
78
78
|
var links = document.createElement('span');
|
|
79
|
+
links.className = 'session-work-links-slot';
|
|
79
80
|
links.innerHTML = html;
|
|
80
81
|
links.addEventListener('click', function (event) { if (event.target.closest('a')) event.stopPropagation(); });
|
|
81
82
|
links.addEventListener('contextmenu', function (event) { event.stopPropagation(); });
|
|
82
|
-
|
|
83
|
+
row.appendChild(links);
|
|
83
84
|
}
|
|
84
85
|
export function syncGithubSessions(sessions) {
|
|
85
86
|
bindGithubWorkPopover();
|
|
@@ -38,7 +38,7 @@ export function renderSessionPresence(row, sessionId) {
|
|
|
38
38
|
container.appendChild(more);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
var anchor = row.querySelector(".session-item-age, .mobile-worker-generation, .mobile-session-unread, .session-row-actions, .mobile-session-star");
|
|
41
|
+
var anchor = row.querySelector(".session-row-trailing, .session-item-age, .mobile-worker-generation, .mobile-session-unread, .session-row-actions, .mobile-session-star");
|
|
42
42
|
// Desktop age/actions now live inside one trailing slot. insertBefore only
|
|
43
43
|
// accepts a direct child, so promote a nested match to its row-level owner.
|
|
44
44
|
while (anchor && anchor.parentNode !== row) anchor = anchor.parentNode;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Check if text looks like env format: first line starts with a valid VAR_NAME=
|
|
2
|
+
export function looksLikeEnv(text) {
|
|
3
|
+
var first = text.split("\n")[0].trim();
|
|
4
|
+
if (first.indexOf("export ") === 0) first = first.substring(7);
|
|
5
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(first);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function parseEnvString(str) {
|
|
9
|
+
var pairs = [];
|
|
10
|
+
if (!str) return pairs;
|
|
11
|
+
var lines = str.split("\n");
|
|
12
|
+
for (var i = 0; i < lines.length; i++) {
|
|
13
|
+
var line = lines[i].trim();
|
|
14
|
+
if (!line || line.charAt(0) === "#") continue;
|
|
15
|
+
// Strip leading "export "
|
|
16
|
+
if (line.indexOf("export ") === 0) line = line.substring(7);
|
|
17
|
+
var eq = line.indexOf("=");
|
|
18
|
+
if (eq === -1) continue;
|
|
19
|
+
var key = line.substring(0, eq).trim();
|
|
20
|
+
var val = line.substring(eq + 1).trim();
|
|
21
|
+
// Saved values are JSON-quoted so shell metacharacters remain data.
|
|
22
|
+
if (val.charAt(0) === '"' && val.charAt(val.length - 1) === '"') {
|
|
23
|
+
try { val = JSON.parse(val); } catch (e) { val = val.substring(1, val.length - 1); }
|
|
24
|
+
} else if (val.charAt(0) === "'" && val.charAt(val.length - 1) === "'") {
|
|
25
|
+
val = val.substring(1, val.length - 1);
|
|
26
|
+
}
|
|
27
|
+
if (key) pairs.push({ key: key, value: val });
|
|
28
|
+
}
|
|
29
|
+
return pairs;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function serializeEnvValue(value) {
|
|
33
|
+
return JSON.stringify(value == null ? "" : String(value));
|
|
34
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Native selects own values and change events; this view adds desktop navigation.
|
|
2
|
+
export function enhanceSettingsSelects(root) {
|
|
3
|
+
if (!root) return;
|
|
4
|
+
root.querySelectorAll('select:not(.settings-nav-dropdown)').forEach(function (select) {
|
|
5
|
+
if (select.dataset.settingsPicker) return;
|
|
6
|
+
select.dataset.settingsPicker = 'true';
|
|
7
|
+
select.classList.add('settings-native-select');
|
|
8
|
+
var wrap = document.createElement('span');
|
|
9
|
+
wrap.className = 'settings-picker';
|
|
10
|
+
var button = document.createElement('button');
|
|
11
|
+
button.type = 'button';
|
|
12
|
+
button.className = 'settings-picker-trigger';
|
|
13
|
+
button.setAttribute('aria-haspopup', 'listbox');
|
|
14
|
+
button.setAttribute('aria-expanded', 'false');
|
|
15
|
+
var label = document.createElement('span');
|
|
16
|
+
label.className = 'settings-picker-value';
|
|
17
|
+
button.appendChild(label);
|
|
18
|
+
button.insertAdjacentHTML('beforeend', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>');
|
|
19
|
+
var menu = document.createElement('span');
|
|
20
|
+
menu.className = 'settings-picker-menu';
|
|
21
|
+
menu.id = (select.id || 'settings-select-' + document.querySelectorAll('.settings-picker').length) + '-menu';
|
|
22
|
+
menu.setAttribute('role', 'listbox');
|
|
23
|
+
menu.hidden = true;
|
|
24
|
+
button.setAttribute('aria-controls', menu.id);
|
|
25
|
+
var enclosingLabel = select.closest('label');
|
|
26
|
+
var associatedLabel = select.labels && select.labels[0];
|
|
27
|
+
var name = select.getAttribute('aria-label') || (enclosingLabel && enclosingLabel.querySelector('.settings-label') || {}).textContent || (associatedLabel && associatedLabel.textContent) || 'Choose a value';
|
|
28
|
+
name = name.trim();
|
|
29
|
+
menu.setAttribute('aria-label', name);
|
|
30
|
+
select.insertAdjacentElement('afterend', wrap);
|
|
31
|
+
wrap.append(button, menu);
|
|
32
|
+
var search = '';
|
|
33
|
+
var searchAt = 0;
|
|
34
|
+
function outside(event) { if (!wrap.contains(event.target)) close(false); }
|
|
35
|
+
function close(focus) {
|
|
36
|
+
document.removeEventListener('pointerdown', outside);
|
|
37
|
+
menu.hidden = true;
|
|
38
|
+
button.setAttribute('aria-expanded', 'false');
|
|
39
|
+
if (focus) button.focus();
|
|
40
|
+
}
|
|
41
|
+
function sync() {
|
|
42
|
+
label.textContent = (select.selectedOptions[0] || {}).textContent || 'Choose…';
|
|
43
|
+
button.setAttribute('aria-label', name + ': ' + label.textContent);
|
|
44
|
+
button.disabled = select.disabled;
|
|
45
|
+
if (button.disabled) close(false);
|
|
46
|
+
menu.replaceChildren();
|
|
47
|
+
Array.from(select.options).forEach(function (option) {
|
|
48
|
+
var item = document.createElement('button');
|
|
49
|
+
item.type = 'button';
|
|
50
|
+
item.tabIndex = -1;
|
|
51
|
+
item.setAttribute('role', 'option');
|
|
52
|
+
item.textContent = option.textContent;
|
|
53
|
+
item.setAttribute('aria-selected', String(option.selected));
|
|
54
|
+
item.disabled = option.disabled || (option.parentElement.tagName === 'OPTGROUP' && option.parentElement.disabled);
|
|
55
|
+
item.hidden = option.hidden;
|
|
56
|
+
item.onclick = function (event) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
select.value = option.value;
|
|
59
|
+
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
60
|
+
sync();
|
|
61
|
+
close(true);
|
|
62
|
+
};
|
|
63
|
+
menu.appendChild(item);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function items() { return Array.from(menu.querySelectorAll('button:not(:disabled):not([hidden])')); }
|
|
67
|
+
function open() {
|
|
68
|
+
sync();
|
|
69
|
+
var choices = items();
|
|
70
|
+
if (!choices.length || button.disabled) return;
|
|
71
|
+
menu.hidden = false;
|
|
72
|
+
button.setAttribute('aria-expanded', 'true');
|
|
73
|
+
document.addEventListener('pointerdown', outside);
|
|
74
|
+
var bounds = button.getBoundingClientRect();
|
|
75
|
+
var boundary = wrap.closest('.server-settings-content, .us-modal-content, .us-content, .ps-content, .admin-modal');
|
|
76
|
+
var bottom = Math.min(window.innerHeight - 8, boundary ? boundary.getBoundingClientRect().bottom - 8 : window.innerHeight - 8);
|
|
77
|
+
var above = bounds.top - Math.max(8, boundary ? boundary.getBoundingClientRect().top + 8 : 8);
|
|
78
|
+
var below = bottom - bounds.bottom - 4;
|
|
79
|
+
var flip = below < 180 && above > below;
|
|
80
|
+
menu.classList.toggle('settings-picker-menu-above', flip);
|
|
81
|
+
menu.style.maxHeight = Math.max(64, Math.min(240, flip ? above : below)) + 'px';
|
|
82
|
+
var selected = menu.querySelector('[aria-selected="true"]:not(:disabled):not([hidden])');
|
|
83
|
+
(selected || choices[0]).focus();
|
|
84
|
+
}
|
|
85
|
+
button.onclick = function (event) {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
if (menu.hidden) open(); else close(false);
|
|
88
|
+
};
|
|
89
|
+
wrap.addEventListener('keydown', function (event) {
|
|
90
|
+
if (event.key === 'Escape' && !menu.hidden) {
|
|
91
|
+
event.preventDefault(); event.stopPropagation(); close(true); return;
|
|
92
|
+
}
|
|
93
|
+
// Return to the trigger before native Tab traversal leaves this control.
|
|
94
|
+
if (event.key === 'Tab' && !menu.hidden) { close(true); return; }
|
|
95
|
+
var navigation = ['ArrowDown', 'ArrowUp', 'Home', 'End'].indexOf(event.key) >= 0;
|
|
96
|
+
var typing = event.key.length === 1 && event.key !== ' ' && !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
97
|
+
if (!navigation && !typing) return;
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
var wasClosed = menu.hidden;
|
|
100
|
+
if (wasClosed) open();
|
|
101
|
+
var choices = items();
|
|
102
|
+
if (!choices.length) return;
|
|
103
|
+
var index = choices.indexOf(document.activeElement);
|
|
104
|
+
if (typing) {
|
|
105
|
+
search = Date.now() - searchAt > 700 ? event.key : search + event.key;
|
|
106
|
+
searchAt = Date.now();
|
|
107
|
+
var match = choices.find(function (item) { return item.textContent.trim().toLowerCase().indexOf(search.toLowerCase()) === 0; });
|
|
108
|
+
if (match) match.focus();
|
|
109
|
+
} else if (!wasClosed || event.key === 'Home' || event.key === 'End') {
|
|
110
|
+
index = event.key === 'Home' ? 0 : event.key === 'End' ? choices.length - 1 : (index + (event.key === 'ArrowDown' ? 1 : -1) + choices.length) % choices.length;
|
|
111
|
+
choices[index].focus();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
wrap.addEventListener('focusout', function (event) { if (!wrap.contains(event.relatedTarget)) close(false); });
|
|
115
|
+
select.addEventListener('change', sync);
|
|
116
|
+
select.addEventListener('settings-sync', sync);
|
|
117
|
+
// Async option loading and pending/failed saves retain native control behavior.
|
|
118
|
+
new MutationObserver(sync).observe(select, { attributes: true, attributeFilter: ['disabled'], childList: true, subtree: true, characterData: true });
|
|
119
|
+
sync();
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -8,10 +8,11 @@ import { activityHtml, renderBrowserActivity } from './shared-browser-activity.j
|
|
|
8
8
|
import { viewportMenuHtml, bindViewportMenu, renderViewportMenu, closeViewportMenu } from './shared-browser-viewport-menu.js';
|
|
9
9
|
|
|
10
10
|
function desktop() { return window.matchMedia('(min-width: 1024px)').matches; }
|
|
11
|
-
function
|
|
11
|
+
function visibleEntry() {
|
|
12
12
|
var permissions = store.get('permissions');
|
|
13
|
-
return desktop() && !!store.get('currentSlug') &&
|
|
13
|
+
return desktop() && !!store.get('currentSlug') && !store.get('dmMode') && (!permissions || permissions.terminal !== false);
|
|
14
14
|
}
|
|
15
|
+
function eligible() { return visibleEntry() && !!store.get('activeSessionId'); }
|
|
15
16
|
function panel() { return document.getElementById('shared-browser-panel'); }
|
|
16
17
|
function ui(patch) { store.set({ sharedBrowserUi: Object.assign({}, store.get('sharedBrowserUi'), patch) }); }
|
|
17
18
|
function send(type, fields) {
|
|
@@ -76,14 +77,19 @@ function ensurePanel() {
|
|
|
76
77
|
refreshIcons();
|
|
77
78
|
}
|
|
78
79
|
function renderEntry() {
|
|
79
|
-
var button = document.getElementById('shared-browser-btn');
|
|
80
|
-
if (!button) return;
|
|
81
|
-
button.classList.toggle('hidden', !eligible());
|
|
82
80
|
var browser = store.get('sharedBrowser');
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
var ready = !!store.get('connected') && !!store.get('activeSessionId');
|
|
82
|
+
var label = !store.get('connected') ? 'Browser · Connecting…' : !store.get('activeSessionId') ? 'Browser · Loading session…' : browser ? 'View browser' : 'Open browser';
|
|
83
|
+
['shared-browser-btn', 'shared-browser-toggle-btn'].forEach(function (id) {
|
|
84
|
+
var button = document.getElementById(id);
|
|
85
|
+
if (!button) return;
|
|
86
|
+
var title = id === 'shared-browser-toggle-btn' && ready && store.get('sharedBrowserOpen') ? 'Hide browser' : label;
|
|
87
|
+
button.classList.toggle('hidden', !visibleEntry());
|
|
88
|
+
button.title = ready && browser ? title + ' · ' + browser.phase : title;
|
|
89
|
+
button.setAttribute('aria-label', title);
|
|
90
|
+
button.setAttribute('aria-expanded', String(!!store.get('sharedBrowserOpen')));
|
|
91
|
+
button.disabled = !ready;
|
|
92
|
+
});
|
|
87
93
|
}
|
|
88
94
|
function controlText(browser, live, connected) {
|
|
89
95
|
if (!connected) return 'Disconnected. Status returns on reconnect.';
|
|
@@ -158,7 +164,7 @@ function selectBrowserTab(id) {
|
|
|
158
164
|
send('view', { visible: desktop() && !document.hidden }); render();
|
|
159
165
|
}
|
|
160
166
|
export function openSharedBrowser() {
|
|
161
|
-
if (!eligible()) return;
|
|
167
|
+
if (!eligible() || !store.get('connected')) return;
|
|
162
168
|
ensurePanel();
|
|
163
169
|
claimRightWorkbench('browser');
|
|
164
170
|
store.set({ sharedBrowserOpen: true });
|
|
@@ -212,8 +218,12 @@ export function handleSharedBrowserMessage(msg) {
|
|
|
212
218
|
}
|
|
213
219
|
export function initSharedBrowser() {
|
|
214
220
|
var button = document.getElementById('shared-browser-btn');
|
|
215
|
-
if (
|
|
216
|
-
|
|
221
|
+
if (button) button.onclick = openSharedBrowser;
|
|
222
|
+
var toggle = document.getElementById('shared-browser-toggle-btn');
|
|
223
|
+
if (toggle) toggle.onclick = function () {
|
|
224
|
+
if (store.get('sharedBrowserOpen')) hideSharedBrowser();
|
|
225
|
+
else openSharedBrowser();
|
|
226
|
+
};
|
|
217
227
|
window.matchMedia('(min-width: 1024px)').addEventListener('change', function () { if (!desktop()) hideSharedBrowser(); renderEntry(); });
|
|
218
228
|
document.addEventListener('visibilitychange', function () { if (store.get('sharedBrowserOpen')) send('view', { visible: !document.hidden && desktop() }); });
|
|
219
229
|
refreshIcons(); renderEntry();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { confirmSettingsAction as showConfirmDialog } from './server-settings-state.js';
|
|
2
|
+
import { enhanceSettingsSelects } from './settings-select.js';
|
|
1
3
|
import { store } from './store.js';
|
|
2
4
|
import { escapeHtml } from './utils.js';
|
|
3
5
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
@@ -5,7 +7,10 @@ import { speechLanguages, voiceModel, loadSpeechSettings, saveSpeechSettings, sa
|
|
|
5
7
|
import { stopSTT } from './stt-controller.js';
|
|
6
8
|
import { browserSpeechInfo, compareSpeechModels } from './stt-browser.js';
|
|
7
9
|
|
|
8
|
-
export function closeSpeechPicker() {
|
|
10
|
+
export function closeSpeechPicker(force) {
|
|
11
|
+
if(document.querySelector('.admin-modal-overlay, #settings-confirm') && !force)return;
|
|
12
|
+
var input = document.querySelector("#speech-api-key");
|
|
13
|
+
if (!force && input && input.value) {showConfirmDialog("Discard the unsaved voice API key?", "Discard", function () {closeSpeechPicker(true);});return;}
|
|
9
14
|
var state = store.get('speechPopup');
|
|
10
15
|
store.set({ speechPopup: null });
|
|
11
16
|
if (state && state.anchor.isConnected) { state.anchor.setAttribute('aria-expanded', 'false'); state.anchor.focus(); }
|
|
@@ -59,8 +64,10 @@ function render() {
|
|
|
59
64
|
popup.style.left = Math.max(8, Math.min(bounds.right - 324, window.innerWidth - 332)) + 'px';
|
|
60
65
|
popup.style.bottom = Math.max(8, window.innerHeight - bounds.top + 8) + 'px';
|
|
61
66
|
popup.style.maxHeight = Math.max(150, bounds.top - 16) + 'px';
|
|
67
|
+
if (state.anchor.closest('#user-settings')) {popup.style.bottom = 'auto';popup.style.top = 'max(16px, calc(50dvh - 260px))';popup.style.maxHeight = 'calc(100dvh - 48px)';}
|
|
62
68
|
popup.addEventListener('click', function (event) {
|
|
63
69
|
var button = event.target.closest('button'); if (!button || button.disabled) return;
|
|
70
|
+
if (popup.querySelector("#speech-api-key") && popup.querySelector("#speech-api-key").value && (button.dataset.mode || button.dataset.provider || button.dataset.model)) {showConfirmDialog('Discard the unsaved voice API key?', 'Discard', function () {form.elements.key.value = '';button.click();});return;}
|
|
64
71
|
if (button.dataset.mode) patch({ mode: button.dataset.mode, provider: null, keyModel: null });
|
|
65
72
|
else if (button.dataset.provider) {
|
|
66
73
|
var keyModel = button.parentElement.querySelector('[data-model]').dataset.model;
|
|
@@ -80,7 +87,7 @@ function render() {
|
|
|
80
87
|
var form = popup.querySelector('form');
|
|
81
88
|
if (form && previousProvider === state.provider) form.elements.key.value = unsavedKey;
|
|
82
89
|
if (form) form.addEventListener('submit', function (event) {
|
|
83
|
-
event.preventDefault(); var key = form.elements.key.value;
|
|
90
|
+
event.preventDefault(); var key = form.elements.key.value;
|
|
84
91
|
save({ provider: state.provider, key: key }, false);
|
|
85
92
|
});
|
|
86
93
|
var select = popup.querySelector('select');
|
|
@@ -89,6 +96,7 @@ function render() {
|
|
|
89
96
|
try { await saveSpeechLanguage(code); if (store.get('speechPopup')) patch({ busy: false, error: '' }); }
|
|
90
97
|
catch (error) { if (store.get('speechPopup')) patch({ busy: false, error: error.message }); }
|
|
91
98
|
});
|
|
99
|
+
enhanceSettingsSelects(popup);
|
|
92
100
|
var focus = Array.from(popup.querySelectorAll('[data-focus]')).find(function (el) { return el.dataset.focus === focusKey && !el.disabled; });
|
|
93
101
|
(form && previousProvider !== state.provider ? form.elements.key : (focus || popup.querySelector('input,button'))).focus();
|
|
94
102
|
}
|
|
@@ -108,14 +116,15 @@ export function initSpeechPicker() {
|
|
|
108
116
|
});
|
|
109
117
|
document.addEventListener('pointerdown', function (event) {
|
|
110
118
|
var state = store.get('speechPopup'); var popup = document.getElementById('speech-picker');
|
|
119
|
+
if(event.target.closest('.admin-modal-overlay, #settings-confirm'))return;
|
|
111
120
|
if (state && popup && !popup.contains(event.target) && !state.anchor.contains(event.target)) closeSpeechPicker();
|
|
112
121
|
});
|
|
113
122
|
document.addEventListener('keydown', function (event) {
|
|
114
|
-
if (!store.get('speechPopup')) return;
|
|
115
|
-
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); closeSpeechPicker(); }
|
|
123
|
+
if (!store.get('speechPopup') || document.querySelector('.admin-modal-overlay, #settings-confirm')) return;
|
|
124
|
+
if (event.key === 'Escape') { if(document.activeElement.closest('.settings-picker') && document.activeElement.closest('.settings-picker').querySelector('[aria-expanded=true]'))return; event.preventDefault(); event.stopPropagation(); closeSpeechPicker(); }
|
|
116
125
|
if (event.key === 'Tab') {
|
|
117
126
|
var popup = document.getElementById('speech-picker');
|
|
118
|
-
var items = popup.querySelectorAll('button:not(:disabled),input:not(:disabled),select:not(:disabled),a[href]');
|
|
127
|
+
var items = Array.from(popup.querySelectorAll('button:not(:disabled),input:not(:disabled),select:not(:disabled),a[href]')).filter(function(el){return el.getClientRects().length && el.tabIndex!==-1;});
|
|
119
128
|
var first = items[0]; var last = items[items.length - 1];
|
|
120
129
|
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
|
121
130
|
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
export var TERMINAL_FONT_OPTIONS = [
|
|
2
|
+
{ label: "SF Mono", family: "'SF Mono', Menlo, Monaco, 'Courier New', monospace" },
|
|
3
|
+
{ label: "JetBrains Mono", family: "'JetBrains Mono', 'SF Mono', Menlo, monospace" },
|
|
4
|
+
{ label: "Fira Code", family: "'Fira Code', 'SF Mono', Menlo, monospace" },
|
|
5
|
+
{ label: "Cascadia Code", family: "'Cascadia Code', 'SF Mono', Menlo, monospace" },
|
|
6
|
+
{ label: "IBM Plex Mono", family: "'IBM Plex Mono', 'SF Mono', Menlo, monospace" },
|
|
7
|
+
{ label: "Source Code Pro", family: "'Source Code Pro', 'SF Mono', Menlo, monospace" },
|
|
8
|
+
{ label: "Roboto Mono", family: "'Roboto Mono', 'SF Mono', Menlo, monospace" },
|
|
9
|
+
{ label: "D2 Coding", family: "'D2 coding', 'D2Coding', 'SF Mono', Menlo, monospace" },
|
|
10
|
+
{ label: "System mono", family: "ui-monospace, monospace" },
|
|
11
|
+
];
|
|
12
|
+
|
|
1
13
|
// terminal-prefs.js
|
|
2
14
|
//
|
|
3
15
|
// Single source of truth for terminal font preferences (family + size).
|
|
@@ -98,3 +110,13 @@ export function applyTerminalFont(family, size) {
|
|
|
98
110
|
export function onTerminalFontChange(fn) {
|
|
99
111
|
if (typeof fn === "function") listeners.push(fn);
|
|
100
112
|
}
|
|
113
|
+
|
|
114
|
+
// Publish only acknowledged values, shared by settings and the terminal toolbar.
|
|
115
|
+
export async function saveTerminalFont(family, size) {
|
|
116
|
+
var response = await fetch('/api/user/terminal-font', {
|
|
117
|
+
method: 'PUT', headers: {'Content-Type':'application/json'},
|
|
118
|
+
body: JSON.stringify({family: family, size: size})
|
|
119
|
+
});
|
|
120
|
+
if (!response.ok) throw new Error('Could not save terminal typography. Try again.');
|
|
121
|
+
applyTerminalFont(family, size);
|
|
122
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
1
2
|
import { refreshIcons } from './icons.js';
|
|
2
3
|
import { setTerminalTheme } from './terminal.js';
|
|
3
4
|
import { setTuiSessionTheme } from './session-tui-view.js';
|
|
@@ -315,19 +316,7 @@ export function applyTheme(themeId, fromPicker) {
|
|
|
315
316
|
var mermaidVars = computeMermaidVars(theme);
|
|
316
317
|
try { updateMermaidTheme(mermaidVars); } catch (e) {}
|
|
317
318
|
|
|
318
|
-
|
|
319
|
-
localStorage.setItem(STORAGE_KEY, themeId);
|
|
320
|
-
localStorage.setItem(STORAGE_KEY + "-vars", JSON.stringify(vars));
|
|
321
|
-
localStorage.setItem(STORAGE_KEY + "-variant", theme.variant || "dark");
|
|
322
|
-
} catch (e) {}
|
|
323
|
-
|
|
324
|
-
// When picked from skin selector, save as skin preference and sync mode
|
|
325
|
-
if (fromPicker) {
|
|
326
|
-
try {
|
|
327
|
-
localStorage.setItem(SKIN_KEY, themeId);
|
|
328
|
-
localStorage.setItem(MODE_KEY, isLight ? "light" : "dark");
|
|
329
|
-
} catch (e) {}
|
|
330
|
-
}
|
|
319
|
+
if (fromPicker) saveAppearance(themeId, isLight ? 'light' : 'dark');
|
|
331
320
|
|
|
332
321
|
updateToggleIcon();
|
|
333
322
|
|
|
@@ -417,7 +406,7 @@ function getSystemMode() {
|
|
|
417
406
|
// Returns the effective mode: user override or system
|
|
418
407
|
function getEffectiveMode() {
|
|
419
408
|
var saved = null;
|
|
420
|
-
|
|
409
|
+
saved = (store.get('appearanceDraft') || store.get('appearancePreference') || {}).mode;
|
|
421
410
|
if (saved === "light" || saved === "dark") return saved;
|
|
422
411
|
return getSystemMode();
|
|
423
412
|
}
|
|
@@ -426,7 +415,10 @@ function getEffectiveMode() {
|
|
|
426
415
|
// If user has a custom skin selected, find its dark/light counterpart
|
|
427
416
|
function themeIdForMode(mode) {
|
|
428
417
|
var skin = null;
|
|
429
|
-
|
|
418
|
+
var preference = store.get('appearanceDraft') || store.get('appearancePreference') || {};
|
|
419
|
+
var paired = preference[mode + 'Theme'];
|
|
420
|
+
if (paired && getTheme(paired) && getTheme(paired).variant === mode) return paired;
|
|
421
|
+
skin = preference.theme;
|
|
430
422
|
|
|
431
423
|
// Default Clay Studio skin pair.
|
|
432
424
|
if (!skin) {
|
|
@@ -457,24 +449,12 @@ function themeIdForMode(mode) {
|
|
|
457
449
|
|
|
458
450
|
// --- Wide view ---
|
|
459
451
|
|
|
460
|
-
export function isWideView() {
|
|
461
|
-
|
|
462
|
-
var v = localStorage.getItem(WIDE_KEY);
|
|
463
|
-
return v === null ? true : v !== "bubble"; // default: Channel (wide)
|
|
464
|
-
} catch (e) { return true; }
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
export function getChatLayout() {
|
|
468
|
-
try {
|
|
469
|
-
var v = localStorage.getItem(WIDE_KEY);
|
|
470
|
-
return v === "bubble" ? "bubble" : "channel";
|
|
471
|
-
} catch (e) { return "channel"; }
|
|
472
|
-
}
|
|
473
|
-
|
|
452
|
+
export function isWideView() { return getChatLayout() === 'channel'; }
|
|
453
|
+
export function getChatLayout() { return store.get('personalChatLayout') || 'channel'; }
|
|
474
454
|
export function setChatLayout(layout) {
|
|
475
|
-
var val =
|
|
476
|
-
|
|
477
|
-
document.body.classList.toggle(
|
|
455
|
+
var val = layout === 'bubble' ? 'bubble' : 'channel';
|
|
456
|
+
store.set({ personalChatLayout: val });
|
|
457
|
+
document.body.classList.toggle('wide-view', val === 'channel');
|
|
478
458
|
}
|
|
479
459
|
|
|
480
460
|
export function setWideView(enabled) {
|
|
@@ -485,9 +465,10 @@ export function setWideView(enabled) {
|
|
|
485
465
|
export function toggleDarkMode() {
|
|
486
466
|
var current = getEffectiveMode();
|
|
487
467
|
var next = current === "dark" ? "light" : "dark";
|
|
488
|
-
|
|
468
|
+
|
|
489
469
|
var tid = themeIdForMode(next);
|
|
490
470
|
applyTheme(tid);
|
|
471
|
+
saveAppearance(tid, next);
|
|
491
472
|
updateToggleIcon();
|
|
492
473
|
}
|
|
493
474
|
|
|
@@ -633,26 +614,11 @@ function rebuildPicker() {
|
|
|
633
614
|
|
|
634
615
|
// --- Init ---
|
|
635
616
|
export function initTheme() {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
var saved = null;
|
|
639
|
-
var savedSkin = null;
|
|
640
|
-
try { saved = localStorage.getItem(STORAGE_KEY); } catch (e) {}
|
|
641
|
-
try { savedSkin = localStorage.getItem(SKIN_KEY); } catch (e) {}
|
|
642
|
-
|
|
643
|
-
if (saved && savedSkin) {
|
|
644
|
-
currentThemeId = saved;
|
|
645
|
-
} else {
|
|
646
|
-
// No explicit skin — use the Clay Studio pair in the system mode.
|
|
647
|
-
var mode = getSystemMode();
|
|
648
|
-
currentThemeId = mode === "light" ? DEFAULT_LIGHT_THEME_ID : DEFAULT_DARK_THEME_ID;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Apply wide view state from localStorage
|
|
652
|
-
document.body.classList.toggle("wide-view", isWideView());
|
|
617
|
+
currentThemeId = getSystemMode() === 'light' ? DEFAULT_LIGHT_THEME_ID : DEFAULT_DARK_THEME_ID;
|
|
618
|
+
document.body.classList.toggle('wide-view', isWideView());
|
|
653
619
|
|
|
654
620
|
// Load all themes from server, then apply properly
|
|
655
|
-
loadThemes();
|
|
621
|
+
loadThemes().then(loadAppearance);
|
|
656
622
|
|
|
657
623
|
// The theme picker is mounted in User Settings by user-settings.js.
|
|
658
624
|
|
|
@@ -661,7 +627,7 @@ export function initTheme() {
|
|
|
661
627
|
var mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
662
628
|
var handler = function () {
|
|
663
629
|
var userMode = null;
|
|
664
|
-
|
|
630
|
+
userMode = (store.get('appearanceDraft') || store.get('appearancePreference') || {}).mode;
|
|
665
631
|
if (!userMode) {
|
|
666
632
|
// No manual override — follow system
|
|
667
633
|
var sysMode = getSystemMode();
|
|
@@ -680,3 +646,73 @@ export function initTheme() {
|
|
|
680
646
|
updateToggleIcon();
|
|
681
647
|
|
|
682
648
|
}
|
|
649
|
+
|
|
650
|
+
function appearanceStatus(text, error) {
|
|
651
|
+
store.set({ appearanceSaveStatus: text, appearanceSaveError: !!error });
|
|
652
|
+
var status = document.getElementById('us-appearance-status');
|
|
653
|
+
if (status) { status.textContent = text; status.dataset.state = error ? 'error' : ''; }
|
|
654
|
+
}
|
|
655
|
+
export function getAppearanceChoices() {
|
|
656
|
+
return { mode: (store.get('appearanceDraft') || store.get('appearancePreference') || {}).mode || 'system', lightTheme: themeIdForMode('light'), darkTheme: themeIdForMode('dark') };
|
|
657
|
+
}
|
|
658
|
+
export function retryAppearanceSave() {
|
|
659
|
+
var draft = store.get('appearanceDraft');
|
|
660
|
+
if (draft) saveAppearance(draft.theme, draft.mode, draft);
|
|
661
|
+
}
|
|
662
|
+
export function selectAppearanceMode(mode) {
|
|
663
|
+
var effective = mode === 'system' ? getSystemMode() : mode;
|
|
664
|
+
var id = themeIdForMode(effective);
|
|
665
|
+
saveAppearance(id, mode === 'system' ? null : mode);
|
|
666
|
+
applyTheme(id);
|
|
667
|
+
}
|
|
668
|
+
export function selectAppearanceTheme(mode, id) {
|
|
669
|
+
if (!getTheme(id) || getTheme(id).variant !== mode) return;
|
|
670
|
+
var effective = getEffectiveMode();
|
|
671
|
+
var current = store.get('appearanceDraft') || store.get('appearancePreference') || {};
|
|
672
|
+
var change = {}; change[mode + 'Theme'] = id;
|
|
673
|
+
saveAppearance(mode === effective ? id : currentThemeId, current.mode || null, change);
|
|
674
|
+
if (mode === effective) applyTheme(id);
|
|
675
|
+
}
|
|
676
|
+
function saveAppearance(theme, mode, overrides) {
|
|
677
|
+
var values = Object.assign({}, store.get('appearanceDraft') || store.get('appearancePreference') || {}, {theme:theme, mode:mode}, overrides || {});
|
|
678
|
+
delete values.configured;
|
|
679
|
+
var selected = getTheme(theme);
|
|
680
|
+
if (selected) values[selected.variant + 'Theme'] = theme;
|
|
681
|
+
store.set({appearanceDraft:values});
|
|
682
|
+
var queue = store.get('appearanceSaveQueue') || Promise.resolve();
|
|
683
|
+
var next = queue.catch(function () {}).then(function () {return persistAppearance(values);});
|
|
684
|
+
store.set({appearanceSaveQueue:next});
|
|
685
|
+
return next;
|
|
686
|
+
}
|
|
687
|
+
async function persistAppearance(values) {
|
|
688
|
+
var request = (store.get('appearanceSaveRequest') || 0) + 1;
|
|
689
|
+
store.set({ appearanceSaveRequest: request });
|
|
690
|
+
appearanceStatus('Saving appearance…');
|
|
691
|
+
try {
|
|
692
|
+
var response = await fetch('/api/user/appearance', {method:'PUT',headers:{'Content-Type':'application/json','X-Clay-Settings':'1'},body:JSON.stringify(values)});
|
|
693
|
+
if (!response.ok) throw new Error('Could not save appearance. Your preview remains. Retry to save it.');
|
|
694
|
+
if (store.get('appearanceSaveRequest') !== request) return;
|
|
695
|
+
var saved = await response.json();
|
|
696
|
+
store.set({ appearancePreference: saved, appearanceDraft: store.get('appearanceDraft') === values ? null : store.get('appearanceDraft') });
|
|
697
|
+
[STORAGE_KEY,SKIN_KEY,MODE_KEY,STORAGE_KEY+'-vars',STORAGE_KEY+'-variant'].forEach(function (key) {try {localStorage.removeItem(key);}catch(error){}});
|
|
698
|
+
appearanceStatus('Saved. Applied to this view; other devices use it when they reload.');
|
|
699
|
+
} catch(error) {if(store.get('appearanceSaveRequest') === request) appearanceStatus(error.message,true);}
|
|
700
|
+
}
|
|
701
|
+
async function loadAppearance() {
|
|
702
|
+
try {
|
|
703
|
+
var response = await fetch('/api/user/appearance');
|
|
704
|
+
if (!response.ok) throw new Error('Appearance preferences unavailable.');
|
|
705
|
+
var data = await response.json();
|
|
706
|
+
if(store.get('appearanceSaveRequest'))return;
|
|
707
|
+
if (data.configured) {store.set({appearancePreference:data});applyTheme(themeIdForMode(data.mode || getSystemMode()));}
|
|
708
|
+
else {
|
|
709
|
+
var legacy = null;try {legacy = localStorage.getItem(STORAGE_KEY);}catch(error){}
|
|
710
|
+
if(legacy && getTheme(legacy))store.set({legacyAppearanceAvailable:legacy});
|
|
711
|
+
}
|
|
712
|
+
} catch(error) {appearanceStatus(error.message,true);}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function importLegacyAppearance() {
|
|
716
|
+
var legacy=store.get('legacyAppearanceAvailable');
|
|
717
|
+
if(legacy) {applyTheme(legacy,true);store.set({legacyAppearanceAvailable:null});}
|
|
718
|
+
}
|