clay-server 5.2.0-beta.10 → 5.2.0-beta.11
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/README.md +0 -4
- package/lib/clay-product-control-tools.js +1 -0
- package/lib/clay-product-control.js +14 -11
- package/lib/clay-vendor-usage.js +44 -0
- package/lib/github-star-count.js +147 -0
- package/lib/notes-checklist.js +43 -1
- package/lib/notes.js +2 -1
- package/lib/project-session-notes.js +33 -8
- package/lib/project-sessions.js +63 -24
- package/lib/project-start-protocol.js +29 -0
- package/lib/project-vendor-login.js +2 -0
- package/lib/project.js +3 -0
- package/lib/public/app.js +25 -17
- package/lib/public/css/contextual-clay.css +1 -1
- package/lib/public/css/filebrowser.css +1 -1
- package/lib/public/css/mate-loading-scene.css +21 -0
- package/lib/public/css/overlays.css +64 -256
- package/lib/public/css/project-mate-creation.css +5 -6
- package/lib/public/css/project-start.css +108 -0
- package/lib/public/css/projects-hub.css +53 -14
- package/lib/public/css/settings-shell.css +103 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/css/title-bar.css +79 -0
- package/lib/public/index.html +50 -63
- package/lib/public/modules/admin-settings-invitations.js +2 -2
- package/lib/public/modules/admin-settings-projects.js +2 -2
- package/lib/public/modules/admin-settings-ui.js +20 -4
- package/lib/public/modules/admin-settings-users.js +4 -4
- package/lib/public/modules/app-home-hub.js +2 -1
- package/lib/public/modules/app-messages.js +10 -2
- package/lib/public/modules/app-projects.js +1 -407
- package/lib/public/modules/app-rate-limit.js +12 -1
- package/lib/public/modules/clay-tool-presentation.js +1 -0
- package/lib/public/modules/contextual-clay.js +3 -1
- package/lib/public/modules/github-star.js +66 -0
- package/lib/public/modules/mate-loading-scene.js +17 -0
- package/lib/public/modules/profile.js +11 -3
- package/lib/public/modules/project-mate-creation-workspace.js +13 -4
- package/lib/public/modules/project-session-visibility-settings.js +19 -16
- package/lib/public/modules/project-settings.js +119 -129
- package/lib/public/modules/project-start-dialog.js +130 -0
- package/lib/public/modules/project-start-folder-state.js +78 -0
- package/lib/public/modules/project-start-folder-view.js +190 -0
- package/lib/public/modules/project-start-forms.js +66 -0
- package/lib/public/modules/project-start-path.js +60 -0
- package/lib/public/modules/project-start-state.js +54 -0
- package/lib/public/modules/project-start-submit.js +67 -0
- package/lib/public/modules/projects-hub-view.js +33 -14
- package/lib/public/modules/projects-hub.js +10 -6
- package/lib/public/modules/server-settings.js +36 -91
- package/lib/public/modules/settings-project-target.js +56 -0
- package/lib/public/modules/settings-select.js +1 -1
- package/lib/public/modules/settings-shell-registry.js +88 -0
- package/lib/public/modules/settings-shell-view.js +104 -0
- package/lib/public/modules/settings-shell.js +342 -0
- package/lib/public/modules/sidebar-mobile.js +8 -30
- package/lib/public/modules/sidebar-projects.js +2 -1
- package/lib/public/modules/theme.js +10 -2
- package/lib/public/modules/user-settings-layout.js +8 -33
- package/lib/public/modules/user-settings-profile.js +1 -1
- package/lib/public/modules/user-settings.js +18 -85
- package/lib/public/modules/vendor-usage-format.js +93 -0
- package/lib/public/modules/vendor-usage-list.js +45 -0
- package/lib/public/modules/vendor-usage-model.js +215 -0
- package/lib/public/modules/vendor-usage-overview.js +312 -0
- package/lib/public/style.css +10 -7
- package/lib/public/welcome-art/conversation-desk.png +0 -0
- package/lib/public/welcome-art/mate-clay.png +0 -0
- package/lib/public/welcome-art/project-clay.png +0 -0
- package/lib/sdk-bridge.js +6 -1
- package/lib/sdk-message-processor.js +9 -1
- package/lib/sdk-quota-snapshot.js +20 -2
- package/lib/server-github-stars.js +39 -0
- package/lib/server-global-ws.js +21 -17
- package/lib/server.js +4 -0
- package/lib/session-notes-mcp-server.js +26 -4
- package/lib/session-notes-writes.js +88 -0
- package/lib/session-visibility.js +20 -8
- package/lib/sessions.js +2 -0
- package/lib/vendor-usage-acp-readers.js +97 -0
- package/lib/vendor-usage-capabilities.js +88 -0
- package/lib/vendor-usage-http-readers.js +214 -0
- package/lib/vendor-usage-metrics.js +47 -0
- package/lib/vendor-usage-native-readers.js +106 -0
- package/lib/vendor-usage-overview.js +406 -0
- package/lib/vendor-usage-service.js +281 -0
- package/lib/ws-schema.js +4 -2
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex-query-contract.js +1 -0
- package/lib/yoke/adapters/codex.js +45 -12
- package/lib/yoke/claude-hook-installer.js +1 -0
- package/lib/yoke/codex-dynamic-tool-names.js +80 -0
- package/lib/yoke/codex-model-catalog.js +37 -3
- package/lib/yoke/muse-services.js +16 -3
- package/lib/yoke/providers/claude-model-descriptions.js +22 -2
- package/lib/yoke/vendor-registry.js +6 -2
- package/package.json +2 -1
- package/lib/public/modules/server-settings-navigation.js +0 -39
|
@@ -1,46 +1,28 @@
|
|
|
1
1
|
import { initStorageUsage, loadStorageUsage } from './server-storage-settings.js';
|
|
2
2
|
import { initAccountAuthSettings, loadAccountAuthSettings } from './account-auth-settings.js';
|
|
3
|
-
//
|
|
3
|
+
// Server pane of the unified settings shell. Page state survives navigation and closing.
|
|
4
4
|
import { store } from './store.js';
|
|
5
5
|
import { getWs } from './ws-ref.js';
|
|
6
6
|
import { refreshIcons } from './icons.js';
|
|
7
7
|
import { closeFileViewer } from './filebrowser.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { loadAdminSection } from './admin.js';
|
|
9
|
+
import { registerSettingsScope, isSettingsShellOpen, activeSettingsScope } from './settings-shell.js';
|
|
10
10
|
import { initNetworkSettings, loadNetworkSettings } from './server-network-settings.js';
|
|
11
11
|
import { initServerControls, setServerPinAvailability } from './server-settings-controls.js';
|
|
12
12
|
import { initServerEnvironment, loadSharedEnv } from './server-settings-environment.js';
|
|
13
|
-
import {
|
|
14
|
-
import { hasSettingsDrafts, confirmSettingsAction, trapSettingsFocus } from './server-settings-state.js';
|
|
13
|
+
import { hasSettingsDrafts } from './server-settings-state.js';
|
|
15
14
|
import { enhanceSettingsSelects } from './settings-select.js';
|
|
16
15
|
export { updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleServerSettingResult } from './server-settings-controls.js';
|
|
17
16
|
export { handleSharedEnv, handleSharedEnvSaved } from './server-settings-environment.js';
|
|
18
17
|
|
|
19
|
-
function isolateSettings(active) {
|
|
20
|
-
if (active) {
|
|
21
|
-
var entries = Array.from(document.body.children).filter(function (el) { return el !== root() && el.id !== 'confirm-modal' && el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE'; }).map(function (el) { var entry = { el: el, inert: el.inert, hidden: el.getAttribute('aria-hidden') }; el.inert = true; el.setAttribute('aria-hidden', 'true'); return entry; });
|
|
22
|
-
store.set({ serverSettingsInert: entries });
|
|
23
|
-
} else {
|
|
24
|
-
(store.get('serverSettingsInert') || []).forEach(function (entry) { entry.el.inert = entry.inert; if (entry.hidden === null) entry.el.removeAttribute('aria-hidden'); else entry.el.setAttribute('aria-hidden', entry.hidden); });
|
|
25
|
-
store.set({ serverSettingsInert: [] });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
18
|
function root() { return document.getElementById('server-settings'); }
|
|
29
|
-
export function isSettingsOpen() { return
|
|
19
|
+
export function isSettingsOpen() { return isSettingsShellOpen() && activeSettingsScope() === 'server'; }
|
|
30
20
|
function send(type) { var ws = getWs(); if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: type })); }
|
|
31
21
|
|
|
32
|
-
function selectPage(id
|
|
22
|
+
function selectPage(id) {
|
|
33
23
|
if (!store.get('serverSettingsAuthorized')) return;
|
|
34
|
-
if (id === 'admin-invites') id = 'admin-users';
|
|
35
|
-
var nav = document.getElementById('settings-page-nav');
|
|
36
|
-
if (!nav.querySelector('[data-section="' + id + '"]')) id = 'overview';
|
|
37
24
|
store.set({ serverSettingsPage: id });
|
|
38
25
|
root().querySelectorAll('.server-settings-section').forEach(function (page) { page.classList.toggle('active', page.dataset.section === id); });
|
|
39
|
-
nav.querySelectorAll('button').forEach(function (button) {
|
|
40
|
-
button.classList.toggle('active', button.dataset.section === id);
|
|
41
|
-
if (button.dataset.section === id) button.setAttribute('aria-current', 'page'); else button.removeAttribute('aria-current');
|
|
42
|
-
});
|
|
43
|
-
document.getElementById('settings-nav-dropdown').value = id;
|
|
44
26
|
if (id === 'network') loadNetworkSettings();
|
|
45
27
|
if (id === 'security') loadAccountAuthSettings();
|
|
46
28
|
if (id === 'storage') loadStorageUsage();
|
|
@@ -49,85 +31,48 @@ function selectPage(id, focus) {
|
|
|
49
31
|
var body = document.getElementById(id + '-body');
|
|
50
32
|
if (id !== 'admin-smtp' || !(store.get('serverSettingsDrafts') || {}).smtp) loadAdminSection(id, body);
|
|
51
33
|
}
|
|
52
|
-
|
|
53
|
-
var heading = root().querySelector('.server-settings-section.active h2');
|
|
54
|
-
heading.tabIndex = -1; heading.focus();
|
|
55
|
-
}
|
|
34
|
+
enhanceSettingsSelects(root()); refreshIcons(root());
|
|
56
35
|
}
|
|
57
36
|
|
|
58
|
-
|
|
59
|
-
|
|
37
|
+
// The shell has already verified administrator (or solo-owner) access.
|
|
38
|
+
function enterServerSettings(ctx) {
|
|
60
39
|
closeFileViewer();
|
|
61
|
-
isolateSettings(true);
|
|
62
|
-
store.set({ serverSettingsReturnFocus: document.activeElement });
|
|
63
|
-
root().classList.remove('hidden');
|
|
64
|
-
document.getElementById('server-settings-btn').classList.add('active');
|
|
65
|
-
document.getElementById('server-settings-close').focus();
|
|
66
40
|
var status = document.getElementById('settings-access-status');
|
|
67
|
-
status.textContent = '
|
|
68
|
-
store.set({ serverSettingsAuthorized:
|
|
69
|
-
|
|
70
|
-
if (serverPinSection) serverPinSection.hidden = true;
|
|
41
|
+
status.textContent = '';
|
|
42
|
+
store.set({ serverSettingsAuthorized: true });
|
|
43
|
+
setServerPinAvailability(!ctx.access.multiUser);
|
|
71
44
|
root().querySelectorAll('.server-settings-section').forEach(function (page) { page.classList.remove('active'); });
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!isSettingsOpen()) return;
|
|
84
|
-
store.set({ serverSettingsAuthorized: true });
|
|
85
|
-
setServerPinAvailability(!data.multiUser);
|
|
86
|
-
status.textContent = '';
|
|
87
|
-
renderSettingsNavigation(!!data.multiUser, selectPage);
|
|
88
|
-
selectPage(store.get('serverSettingsPage') || 'overview', false);
|
|
89
|
-
if (!hasSettingsDrafts()) send('get_daemon_config');
|
|
90
|
-
setTimeout(function () {
|
|
91
|
-
if (isSettingsOpen() && !store.get('serverSettingsConfigLoaded')) status.textContent = 'Server configuration has not loaded. Reconnect, then reopen Settings to retry.';
|
|
92
|
-
}, 12000);
|
|
93
|
-
send('process_stats');
|
|
94
|
-
clearInterval(store.get('serverSettingsStatsTimer'));
|
|
95
|
-
store.set({ serverSettingsStatsTimer: setInterval(function () { send('process_stats'); }, 5000) });
|
|
96
|
-
document.getElementById('settings-project-slug').textContent = store.get('currentSlug') || '(none)';
|
|
97
|
-
document.getElementById('settings-ws-path').textContent = store.get('wsPath') || '/ws';
|
|
98
|
-
var pill = document.getElementById('skip-perms-pill');
|
|
99
|
-
document.getElementById('settings-skip-perms').textContent = pill && !pill.classList.contains('hidden') ? 'Skip' : 'Permission checks';
|
|
100
|
-
enhanceSettingsSelects(root()); refreshIcons(root());
|
|
101
|
-
} catch (error) { status.textContent = error.message; }
|
|
45
|
+
if (!hasSettingsDrafts()) send('get_daemon_config');
|
|
46
|
+
setTimeout(function () {
|
|
47
|
+
if (isSettingsOpen() && !store.get('serverSettingsConfigLoaded')) status.textContent = 'Server configuration has not loaded. Reconnect, then reopen Settings to retry.';
|
|
48
|
+
}, 12000);
|
|
49
|
+
send('process_stats');
|
|
50
|
+
clearInterval(store.get('serverSettingsStatsTimer'));
|
|
51
|
+
store.set({ serverSettingsStatsTimer: setInterval(function () { send('process_stats'); }, 5000) });
|
|
52
|
+
document.getElementById('settings-project-slug').textContent = store.get('currentSlug') || '(none)';
|
|
53
|
+
document.getElementById('settings-ws-path').textContent = store.get('wsPath') || '/ws';
|
|
54
|
+
var pill = document.getElementById('skip-perms-pill');
|
|
55
|
+
document.getElementById('settings-skip-perms').textContent = pill && !pill.classList.contains('hidden') ? 'Skip' : 'Permission checks';
|
|
102
56
|
}
|
|
103
57
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
root().classList.add('hidden');
|
|
108
|
-
document.getElementById('server-settings-btn').classList.remove('active');
|
|
109
|
-
clearInterval(store.get('serverSettingsStatsTimer'));
|
|
110
|
-
var previous = store.get('serverSettingsReturnFocus');
|
|
111
|
-
if (previous && previous.isConnected) previous.focus();
|
|
112
|
-
}
|
|
113
|
-
if (hasSettingsDrafts()) confirmSettingsAction('You have unsaved changes. Closing keeps these drafts in this tab, but reloading the page will lose them.', 'Close with drafts', close);
|
|
114
|
-
else close();
|
|
58
|
+
function leaveServerSettings() {
|
|
59
|
+
clearInterval(store.get('serverSettingsStatsTimer'));
|
|
60
|
+
store.set({ serverSettingsAuthorized: false });
|
|
115
61
|
}
|
|
116
62
|
|
|
117
63
|
export function initServerSettings() {
|
|
118
64
|
if (!root()) return;
|
|
119
65
|
initNetworkSettings(); initServerControls(); initServerEnvironment(); initStorageUsage(); initAccountAuthSettings();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
trapSettingsFocus(event, root());
|
|
66
|
+
registerSettingsScope('server', {
|
|
67
|
+
pane: root(),
|
|
68
|
+
enter: enterServerSettings,
|
|
69
|
+
activate: selectPage,
|
|
70
|
+
leave: leaveServerSettings,
|
|
71
|
+
leavePrompt: function (kind) {
|
|
72
|
+
if (!hasSettingsDrafts() || kind === 'page' || kind === 'project') return null;
|
|
73
|
+
if (kind === 'close') return { message: 'You have unsaved changes. Closing keeps these drafts in this tab, but reloading the page will lose them.', action: 'Close with drafts' };
|
|
74
|
+
return { message: 'Your server settings drafts will remain in this tab while you visit another settings area.', action: 'Leave Server settings' };
|
|
75
|
+
}
|
|
131
76
|
});
|
|
132
77
|
new MutationObserver(function () { enhanceSettingsSelects(root()); }).observe(root(), { childList: true, subtree: true });
|
|
133
78
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Project selection for settings. The settings target is independent of the
|
|
2
|
+
// open workspace: choosing a project here never changes currentSlug or the
|
|
3
|
+
// workspace WebSocket. Pure helpers plus a generation guard for async replies.
|
|
4
|
+
|
|
5
|
+
export function settingsProjectLabel(project) {
|
|
6
|
+
return (project && (project.title || project.project || project.name || project.slug)) || '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Mates and the Clay project have their own settings surfaces.
|
|
10
|
+
export function isSettingsProject(project, permissions) {
|
|
11
|
+
if (!project || !project.slug || project.isMate || /^mate-/.test(project.slug)) return false;
|
|
12
|
+
return !permissions || permissions.projectSettings !== false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Authorised projects for the signed-in user (the server-filtered project
|
|
16
|
+
// list), the open workspace first, the rest in their existing order.
|
|
17
|
+
export function listSettingsProjects(projects, currentSlug, permissions) {
|
|
18
|
+
var eligible = (Array.isArray(projects) ? projects : []).filter(function (project) { return isSettingsProject(project, permissions); });
|
|
19
|
+
var current = eligible.filter(function (project) { return project.slug === currentSlug; });
|
|
20
|
+
var others = eligible.filter(function (project) { return project.slug !== currentSlug; });
|
|
21
|
+
return current.concat(others).map(function (project, index) {
|
|
22
|
+
return { slug: project.slug, label: settingsProjectLabel(project), isCurrent: project.slug === currentSlug, project: project, first: index === 0 };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A requested slug must still be authorised; otherwise use the current
|
|
27
|
+
// workspace, then the first choice, then nothing.
|
|
28
|
+
export function resolveTargetSlug(requested, choices) {
|
|
29
|
+
var list = choices || [];
|
|
30
|
+
for (var i = 0; i < list.length; i++) if (list[i].slug === requested) return requested;
|
|
31
|
+
return list.length ? list[0].slug : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function findChoice(choices, slug) {
|
|
35
|
+
var list = choices || [];
|
|
36
|
+
for (var i = 0; i < list.length; i++) if (list[i].slug === slug) return list[i];
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function optionLabel(choice) {
|
|
41
|
+
return choice.isCurrent ? choice.label + ' (open workspace)' : choice.label;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Every settings open, scope change and project switch takes a new token.
|
|
45
|
+
// Async work captures the token and ignores its result once it is stale.
|
|
46
|
+
export function createTargetBinding() {
|
|
47
|
+
var generation = 0;
|
|
48
|
+
var slug = null;
|
|
49
|
+
return {
|
|
50
|
+
bind: function (next) { generation += 1; slug = next || null; return generation; },
|
|
51
|
+
invalidate: function () { generation += 1; slug = null; return generation; },
|
|
52
|
+
isCurrent: function (token, expectedSlug) { return token === generation && (expectedSlug === undefined || expectedSlug === slug); },
|
|
53
|
+
slug: function () { return slug; },
|
|
54
|
+
generation: function () { return generation; }
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -72,7 +72,7 @@ export function enhanceSettingsSelects(root) {
|
|
|
72
72
|
button.setAttribute('aria-expanded', 'true');
|
|
73
73
|
document.addEventListener('pointerdown', outside);
|
|
74
74
|
var bounds = button.getBoundingClientRect();
|
|
75
|
-
var boundary = wrap.closest('.server-settings-content, .us-modal-content, .us-content, .ps-content, .admin-modal');
|
|
75
|
+
var boundary = wrap.closest('.settings-shell-content, .server-settings-content, .us-modal-content, .us-content, .ps-content, .admin-modal');
|
|
76
76
|
var bottom = Math.min(window.innerHeight - 8, boundary ? boundary.getBoundingClientRect().bottom - 8 : window.innerHeight - 8);
|
|
77
77
|
var above = bounds.top - Math.max(8, boundary ? boundary.getBoundingClientRect().top + 8 : 8);
|
|
78
78
|
var below = bottom - bounds.bottom - 4;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Pure routing model for the unified settings shell: scopes, pages, legacy
|
|
2
|
+
// aliases and access-based fallbacks. No DOM or store access, so every entry
|
|
3
|
+
// point resolves to one exact { scope, page, slug } destination.
|
|
4
|
+
|
|
5
|
+
export var SETTINGS_SCOPES = [
|
|
6
|
+
{ id: 'personal', label: 'Personal', hint: 'Applies to your account on every device' },
|
|
7
|
+
{ id: 'project', label: 'Project', hint: 'Applies to the selected project' },
|
|
8
|
+
{ id: 'server', label: 'Server', hint: 'Applies to this Clay server for everyone' }
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export var PERSONAL_PAGES = [
|
|
12
|
+
{ id: 'us-account', label: 'Account' }, { id: 'us-appearance', label: 'Appearance' },
|
|
13
|
+
{ id: 'us-chat', label: 'Conversation' }, { id: 'us-notifications', label: 'Notifications' },
|
|
14
|
+
{ id: 'us-ai', label: 'AI preferences' }, { id: 'us-integrations', label: 'Integrations' }
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export var PROJECT_PAGES = [
|
|
18
|
+
{ id: 'profile', label: 'General' }, { id: 'access', label: 'Access' }, { id: 'environment', label: 'Environment' }
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export var SERVER_PAGES = [
|
|
22
|
+
{ id: 'overview', category: 'Service', label: 'Overview' },
|
|
23
|
+
{ id: 'network', category: 'Service', label: 'Network' },
|
|
24
|
+
{ id: 'storage', category: 'Service', label: 'Storage' },
|
|
25
|
+
{ id: 'security', category: 'Administration', label: 'Access & security' },
|
|
26
|
+
{ id: 'admin-users', category: 'Administration', label: 'Users', multiUser: true },
|
|
27
|
+
{ id: 'admin-projects', category: 'Administration', label: 'Project access', multiUser: true },
|
|
28
|
+
{ id: 'admin-smtp', category: 'Administration', label: 'Email delivery', multiUser: true },
|
|
29
|
+
{ id: 'environment', category: 'Agent configuration', label: 'Environment' },
|
|
30
|
+
{ id: 'operations', category: 'Maintenance', label: 'Operations' }
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
var PERSONAL_ALIASES = {
|
|
34
|
+
'us-mates': 'us-chat', 'us-experimental': 'us-chat', 'us-connections': 'us-integrations',
|
|
35
|
+
'us-email': 'us-integrations', 'us-logout': 'us-account'
|
|
36
|
+
};
|
|
37
|
+
var SERVER_ALIASES = { 'admin-invites': 'admin-users' };
|
|
38
|
+
|
|
39
|
+
export var DEFAULT_PERSONAL_PAGE = 'us-account';
|
|
40
|
+
|
|
41
|
+
// access: { server: boolean, multiUser: boolean, project: boolean }
|
|
42
|
+
export function defaultAccess() { return { server: false, multiUser: false, project: true }; }
|
|
43
|
+
|
|
44
|
+
// The authoritative role rule: a multi-user server needs an admin; a solo
|
|
45
|
+
// (single-user) server's owner has server access.
|
|
46
|
+
export function accessFromMe(me, permissions) {
|
|
47
|
+
var multiUser = !!(me && me.multiUser);
|
|
48
|
+
var admin = !!(me && me.user && me.user.role === 'admin');
|
|
49
|
+
return { server: !!me && (!multiUser || admin), multiUser: multiUser, project: !permissions || permissions.projectSettings !== false };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function pagesForScope(scope, access) {
|
|
53
|
+
access = access || defaultAccess();
|
|
54
|
+
if (scope === 'personal') return PERSONAL_PAGES.slice();
|
|
55
|
+
if (scope === 'project') return PROJECT_PAGES.slice();
|
|
56
|
+
if (scope === 'server') return SERVER_PAGES.filter(function (page) { return !page.multiUser || access.multiUser; });
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function scopeAvailable(scope, access) {
|
|
61
|
+
access = access || defaultAccess();
|
|
62
|
+
if (scope === 'personal') return true;
|
|
63
|
+
if (scope === 'project') return !!access.project;
|
|
64
|
+
if (scope === 'server') return !!access.server;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolvePage(scope, page, access) {
|
|
69
|
+
var alias = scope === 'personal' ? PERSONAL_ALIASES[page] : scope === 'server' ? SERVER_ALIASES[page] : null;
|
|
70
|
+
var id = alias || page;
|
|
71
|
+
var pages = pagesForScope(scope, access);
|
|
72
|
+
for (var i = 0; i < pages.length; i++) if (pages[i].id === id) return id;
|
|
73
|
+
return pages.length ? pages[0].id : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Unknown or unavailable scopes fall back to Personal, which is always allowed.
|
|
77
|
+
export function resolveRoute(route, access) {
|
|
78
|
+
route = route || {};
|
|
79
|
+
var scope = scopeAvailable(route.scope, access) ? route.scope : 'personal';
|
|
80
|
+
var requested = scope === route.scope ? route.page : null;
|
|
81
|
+
return {
|
|
82
|
+
scope: scope,
|
|
83
|
+
page: resolvePage(scope, requested, access),
|
|
84
|
+
slug: scope === 'project' ? (route.slug || null) : null,
|
|
85
|
+
requestedScope: route.scope || 'personal',
|
|
86
|
+
redirected: scope !== (route.scope || 'personal')
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// DOM rendering for the unified settings shell chrome: scope tabs, page
|
|
2
|
+
// navigation (desktop list + narrow select) and the project selector.
|
|
3
|
+
import { SETTINGS_SCOPES } from './settings-shell-registry.js';
|
|
4
|
+
import { optionLabel } from './settings-project-target.js';
|
|
5
|
+
import { enhanceSettingsSelects } from './settings-select.js';
|
|
6
|
+
|
|
7
|
+
function el(id) { return document.getElementById(id); }
|
|
8
|
+
|
|
9
|
+
export function setShellStatus(text, state) {
|
|
10
|
+
var status = el('settings-shell-status');
|
|
11
|
+
status.textContent = text || '';
|
|
12
|
+
status.hidden = !text;
|
|
13
|
+
if (state) status.dataset.state = state; else delete status.dataset.state;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function renderScopeTabs(access, activeScope, onSelect) {
|
|
17
|
+
var tabs = el('settings-scope-tabs');
|
|
18
|
+
tabs.replaceChildren();
|
|
19
|
+
SETTINGS_SCOPES.forEach(function (scope) {
|
|
20
|
+
if (scope.id === 'server' && !access.server) return;
|
|
21
|
+
if (scope.id === 'project' && !access.project) return;
|
|
22
|
+
var button = document.createElement('button');
|
|
23
|
+
button.type = 'button'; button.className = 'settings-scope-tab'; button.dataset.scope = scope.id;
|
|
24
|
+
button.textContent = scope.label; button.title = scope.hint;
|
|
25
|
+
if (scope.id === activeScope) button.setAttribute('aria-current', 'page');
|
|
26
|
+
button.classList.toggle('active', scope.id === activeScope);
|
|
27
|
+
button.onclick = function () { onSelect(scope.id); };
|
|
28
|
+
tabs.appendChild(button);
|
|
29
|
+
});
|
|
30
|
+
var active = SETTINGS_SCOPES.filter(function (scope) { return scope.id === activeScope; })[0];
|
|
31
|
+
el('settings-scope-title').textContent = active ? active.label + ' settings' : 'Settings';
|
|
32
|
+
el('settings-scope-hint').textContent = active ? active.hint : '';
|
|
33
|
+
el('settings-server-name').hidden = activeScope !== 'server';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function renderPageNav(pages, activePage, onSelect) {
|
|
37
|
+
var nav = el('settings-page-nav');
|
|
38
|
+
var select = el('settings-page-select');
|
|
39
|
+
nav.replaceChildren(); select.replaceChildren();
|
|
40
|
+
var category; var group = select;
|
|
41
|
+
pages.forEach(function (page) {
|
|
42
|
+
if (page.category && page.category !== category) {
|
|
43
|
+
category = page.category;
|
|
44
|
+
var heading = document.createElement('div'); heading.className = 'settings-nav-category'; heading.textContent = category; nav.appendChild(heading);
|
|
45
|
+
group = document.createElement('optgroup'); group.label = category; select.appendChild(group);
|
|
46
|
+
}
|
|
47
|
+
var button = document.createElement('button');
|
|
48
|
+
button.type = 'button'; button.className = 'settings-nav-item'; button.dataset.section = page.id; button.textContent = page.label;
|
|
49
|
+
button.onclick = function () { onSelect(page.id); };
|
|
50
|
+
nav.appendChild(button);
|
|
51
|
+
var option = document.createElement('option'); option.value = page.id; option.textContent = page.label; group.appendChild(option);
|
|
52
|
+
});
|
|
53
|
+
select.onchange = function () { onSelect(this.value); };
|
|
54
|
+
nav.onkeydown = function (event) {
|
|
55
|
+
if (['ArrowUp', 'ArrowDown', 'Home', 'End'].indexOf(event.key) < 0) return;
|
|
56
|
+
var items = Array.from(nav.querySelectorAll('button'));
|
|
57
|
+
var index = items.indexOf(document.activeElement);
|
|
58
|
+
if (index < 0) return;
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
index = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : (index + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
61
|
+
items[index].focus();
|
|
62
|
+
};
|
|
63
|
+
markActivePage(activePage);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function markActivePage(activePage) {
|
|
67
|
+
Array.from(el('settings-page-nav').querySelectorAll('button')).forEach(function (button) {
|
|
68
|
+
var active = button.dataset.section === activePage;
|
|
69
|
+
button.classList.toggle('active', active);
|
|
70
|
+
if (active) button.setAttribute('aria-current', 'page'); else button.removeAttribute('aria-current');
|
|
71
|
+
});
|
|
72
|
+
var select = el('settings-page-select');
|
|
73
|
+
if (activePage && select.value !== activePage) select.value = activePage;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The selector is a plain select; settings-select upgrades it to the shared
|
|
77
|
+
// desktop picker and leaves the native control on narrow screens.
|
|
78
|
+
export function renderProjectSelect(choices, selectedSlug, onChange, placeholder) {
|
|
79
|
+
var wrap = el('settings-project-picker');
|
|
80
|
+
var select = el('settings-project-select');
|
|
81
|
+
select.replaceChildren();
|
|
82
|
+
choices.forEach(function (choice) {
|
|
83
|
+
var option = document.createElement('option');
|
|
84
|
+
option.value = choice.slug; option.textContent = optionLabel(choice);
|
|
85
|
+
select.appendChild(option);
|
|
86
|
+
});
|
|
87
|
+
if (!choices.length) {
|
|
88
|
+
var none = document.createElement('option'); none.value = ''; none.textContent = placeholder || 'No projects available';
|
|
89
|
+
select.appendChild(none);
|
|
90
|
+
}
|
|
91
|
+
select.disabled = !choices.length;
|
|
92
|
+
select.value = selectedSlug || (choices[0] && choices[0].slug) || '';
|
|
93
|
+
select.onchange = function () { onChange(this.value); };
|
|
94
|
+
enhanceSettingsSelects(wrap);
|
|
95
|
+
select.dispatchEvent(new Event('settings-sync'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function showProjectPicker(visible) { el('settings-project-picker').hidden = !visible; }
|
|
99
|
+
export function selectedProjectValue() { return el('settings-project-select').value; }
|
|
100
|
+
export function syncProjectSelect(slug) {
|
|
101
|
+
var select = el('settings-project-select');
|
|
102
|
+
select.value = slug || '';
|
|
103
|
+
select.dispatchEvent(new Event('settings-sync'));
|
|
104
|
+
}
|