clay-server 5.5.0-beta.3 → 5.5.0-beta.5

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.
Files changed (108) hide show
  1. package/lib/github-connection-store.js +39 -0
  2. package/lib/github-runtime.js +43 -0
  3. package/lib/project-github-connection.js +164 -0
  4. package/lib/project.js +8 -0
  5. package/lib/public/appearance-bootstrap.js +6 -0
  6. package/lib/public/css/chrome-material.css +133 -0
  7. package/lib/public/css/menus.css +1 -17
  8. package/lib/public/css/navigation-island.css +261 -0
  9. package/lib/public/css/notifications-center.css +1 -17
  10. package/lib/public/css/settings-shell.css +0 -1
  11. package/lib/public/css/settings-ui.css +11 -2
  12. package/lib/public/css/shared-browser.css +1 -18
  13. package/lib/public/css/split-view-layout.css +9 -5
  14. package/lib/public/css/sticky-notes.css +1 -18
  15. package/lib/public/css/title-bar.css +1 -1
  16. package/lib/public/css/workspace-background-gallery.css +43 -0
  17. package/lib/public/css/you.css +13 -10
  18. package/lib/public/index.html +53 -46
  19. package/lib/public/modules/agent-behavior-nav.js +1 -0
  20. package/lib/public/modules/app-messages.js +5 -0
  21. package/lib/public/modules/appearance-preferences.js +81 -0
  22. package/lib/public/modules/context-sources.js +2 -282
  23. package/lib/public/modules/github-mark.js +2 -0
  24. package/lib/public/modules/pane-appearance-bridge.js +9 -1
  25. package/lib/public/modules/pane-header-tooltips.js +36 -9
  26. package/lib/public/modules/personal-email-editor.js +305 -0
  27. package/lib/public/modules/personal-integration-editor.js +70 -0
  28. package/lib/public/modules/personal-settings-dialog.js +15 -1
  29. package/lib/public/modules/session-list-material.js +25 -0
  30. package/lib/public/modules/settings-section-nav.js +2 -2
  31. package/lib/public/modules/settings-shell-registry.js +6 -5
  32. package/lib/public/modules/sidebar-collapse-focus.js +22 -0
  33. package/lib/public/modules/sidebar-sessions.js +2 -0
  34. package/lib/public/modules/sidebar.js +9 -10
  35. package/lib/public/modules/split-pane-renderer.js +5 -5
  36. package/lib/public/modules/split-view-shell-controls.js +9 -5
  37. package/lib/public/modules/split-view.js +2 -2
  38. package/lib/public/modules/theme.js +10 -28
  39. package/lib/public/modules/tool-pane-chrome.js +4 -1
  40. package/lib/public/modules/tool-pane-material.js +23 -0
  41. package/lib/public/modules/tooltip.js +7 -3
  42. package/lib/public/modules/tui-attention.js +9 -4
  43. package/lib/public/modules/user-settings-account.js +10 -1
  44. package/lib/public/modules/user-settings-github.js +176 -0
  45. package/lib/public/modules/user-settings-integrations.js +23 -12
  46. package/lib/public/modules/user-settings-layout.js +21 -18
  47. package/lib/public/modules/user-settings-themes.js +6 -2
  48. package/lib/public/modules/user-settings.js +2 -1
  49. package/lib/public/modules/workspace-background-gallery.js +92 -0
  50. package/lib/public/modules/workspace-custom-colors.js +132 -0
  51. package/lib/public/modules/you-activity.js +4 -2
  52. package/lib/public/pane-assets/app-C27ONMHZ.js +1 -0
  53. package/lib/public/pane-assets/chunk-4REWYEXI.js +1 -0
  54. package/lib/public/pane-assets/chunk-6EDR4PMU.js +1 -0
  55. package/lib/public/pane-assets/{chunk-4QFNLHW5.js → chunk-76JE6SO6.js} +1 -1
  56. package/lib/public/pane-assets/chunk-AFATDM32.js +1 -0
  57. package/lib/public/pane-assets/{chunk-P3AK4ORJ.js → chunk-DTNAWMGQ.js} +1 -1
  58. package/lib/public/pane-assets/chunk-EGCR2MH4.js +1 -0
  59. package/lib/public/pane-assets/chunk-ET4NUBYW.js +1 -0
  60. package/lib/public/pane-assets/chunk-EYJZ3R7Y.js +1 -0
  61. package/lib/public/pane-assets/{chunk-CXWAJEN5.js → chunk-I5Q4CZDJ.js} +1 -1
  62. package/lib/public/pane-assets/{chunk-JM46WGDH.js → chunk-KJN6KGEN.js} +1 -1
  63. package/lib/public/pane-assets/chunk-KZ3CYS5U.js +1 -0
  64. package/lib/public/pane-assets/chunk-MR4FHQFA.js +1 -0
  65. package/lib/public/pane-assets/chunk-NRC5RDIL.js +1 -0
  66. package/lib/public/pane-assets/chunk-NRP4NXVZ.js +1 -0
  67. package/lib/public/pane-assets/chunk-OERDYIHU.js +1 -0
  68. package/lib/public/pane-assets/{chunk-V5DLPSLT.js → chunk-OGIHAXLD.js} +1 -1
  69. package/lib/public/pane-assets/chunk-OQBKX36J.js +2 -0
  70. package/lib/public/pane-assets/chunk-Q2HJ5CWT.js +1 -0
  71. package/lib/public/pane-assets/{chunk-3EJ5IAHZ.js → chunk-QOL6PHMZ.js} +1 -1
  72. package/lib/public/pane-assets/{chunk-32YIYF7R.js → chunk-TVKTFEST.js} +1 -1
  73. package/lib/public/pane-assets/{chunk-XSSTR6GC.js → chunk-UMXZTYOY.js} +1 -1
  74. package/lib/public/pane-assets/chunk-W5MFUKXJ.js +1 -0
  75. package/lib/public/pane-assets/chunk-W7XXER5Z.js +174 -0
  76. package/lib/public/pane-assets/chunk-Y77DR2RU.js +1 -0
  77. package/lib/public/pane-assets/{chunk-XVESKMU3.js → chunk-YBYNU4VI.js} +1 -1
  78. package/lib/public/pane-assets/{chunk-JIV7O5GN.js → chunk-YHBTK56T.js} +1 -1
  79. package/lib/public/pane-assets/manifest.json +404 -390
  80. package/lib/public/pane-assets/style-F4MQRRX2.css +1 -0
  81. package/lib/public/style.css +4 -0
  82. package/lib/public/workspace-art/workshop-dark.webp +0 -0
  83. package/lib/public/workspace-art/workshop-light.webp +0 -0
  84. package/lib/public/workspace-background-catalog.js +40 -0
  85. package/lib/server-appearance-settings.js +16 -5
  86. package/lib/server.js +6 -0
  87. package/lib/session-github.js +1 -7
  88. package/lib/terminal-manager.js +5 -1
  89. package/lib/terminal.js +10 -1
  90. package/package.json +1 -1
  91. package/lib/public/pane-assets/app-ANJS2LSK.js +0 -1
  92. package/lib/public/pane-assets/chunk-3TIVBAKM.js +0 -1
  93. package/lib/public/pane-assets/chunk-5XMZMIFG.js +0 -1
  94. package/lib/public/pane-assets/chunk-6QFLYOVY.js +0 -1
  95. package/lib/public/pane-assets/chunk-6S2C7Q2N.js +0 -1
  96. package/lib/public/pane-assets/chunk-7B2GJI57.js +0 -1
  97. package/lib/public/pane-assets/chunk-BOFNCPTN.js +0 -1
  98. package/lib/public/pane-assets/chunk-EWTXBZJO.js +0 -2
  99. package/lib/public/pane-assets/chunk-G35ZOJGP.js +0 -1
  100. package/lib/public/pane-assets/chunk-HYT24UBT.js +0 -1
  101. package/lib/public/pane-assets/chunk-ISQEPZMY.js +0 -1
  102. package/lib/public/pane-assets/chunk-LBJG4MK2.js +0 -174
  103. package/lib/public/pane-assets/chunk-QKPGE7DN.js +0 -1
  104. package/lib/public/pane-assets/chunk-TS5JUEVH.js +0 -1
  105. package/lib/public/pane-assets/chunk-UGH5MCNK.js +0 -1
  106. package/lib/public/pane-assets/chunk-VFKDU64W.js +0 -1
  107. package/lib/public/pane-assets/chunk-VZ2OKDAO.js +0 -1
  108. package/lib/public/pane-assets/style-VBVCWV44.css +0 -1
@@ -1,3 +1,5 @@
1
+ import { selectIntegrationEditor, clearIntegrationEditor, bindIntegrationEditorFocus } from './personal-integration-editor.js';
2
+ import { initGithubIntegration } from './user-settings-github.js';
1
3
  import { linearIconMarkup } from './session-github.js';
2
4
  import { iconHtml } from './icons.js';
3
5
  import { store } from './store.js';
@@ -5,11 +7,13 @@ import { confirmSettingsAction } from './server-settings-state.js';
5
7
 
6
8
  export function initPersonalIntegrations() {
7
9
  var root = document.querySelector('[data-section="us-integrations"].us-section');
8
- root.innerHTML = '<h2>Integrations</h2><p class="settings-hint">Your private accounts and API keys.</p><h3>Your connections</h3><div id="us-email-list"></div><div id="us-linear-connections"></div><p id="us-connections-empty" class="settings-hint">No connections yet. Choose a service below to get started.</p><p id="us-linear-collection-status" class="settings-status" role="status" aria-live="polite"></p><button id="us-linear-retry" type="button" class="settings-btn-sm" hidden>Retry Linear</button><section class="us-integration-catalog"><h3>Available integrations</h3><div class="us-integration-tiles"><button type="button" class="us-integration-tile" data-email-provider="gmail"><img src="/icons/email/gmail.svg" alt=""><span>Gmail</span></button><button type="button" class="us-integration-tile" data-email-provider="outlook"><img src="/icons/email/outlook.svg" alt=""><span>Outlook Mail</span></button><button type="button" class="us-integration-tile" data-email-provider="yahoo"><img src="/icons/email/yahoo.svg" alt=""><span>Yahoo Mail</span></button><button type="button" class="us-integration-tile" data-email-provider="custom">' + iconHtml('mail') + '<span>Custom email</span></button><button type="button" class="us-integration-tile" id="us-linear-add" aria-label="Add Linear connection">' + linearIconMarkup() + '<span>Linear</span></button></div></section><div id="us-integration-setup"></div><form id="us-linear-editor" class="settings-form" hidden><div class="settings-field"><label class="settings-label" for="us-linear-name">Connection name</label><input id="us-linear-name" class="us-text-input" maxlength="80" required placeholder="Work, personal, or client name"></div><div class="settings-field"><label class="settings-label" for="us-linear-key">Personal API key</label><input id="us-linear-key" class="us-text-input" type="password" autocomplete="off" spellcheck="false" maxlength="4096"><p id="us-linear-key-help" class="settings-hint">Existing keys stay masked.</p></div><div class="settings-form-actions"><button type="submit" class="settings-btn-sm">Save connection</button><button type="button" class="settings-btn-sm" id="us-linear-cancel">Cancel</button><button type="button" class="settings-btn-sm" id="us-linear-remove">Disconnect</button></div></form>';
10
+ root.innerHTML = '<h2>Integrations</h2><p class="settings-hint">Connect accounts and services used by Clay Studio.</p><h3>Your connections</h3><div id="us-email-list"></div><div id="us-linear-connections"></div><p id="us-connections-empty" class="settings-hint">No connections yet. Choose a service below to get started.</p><p id="us-linear-collection-status" class="settings-status" role="status" aria-live="polite"></p><button id="us-linear-retry" type="button" class="settings-btn-sm" hidden>Retry Linear</button><section class="us-integration-catalog"><h3>Available integrations</h3><div class="us-integration-tiles"><button type="button" class="us-integration-tile" data-email-provider="gmail"><img src="/icons/email/gmail.svg" alt=""><span>Gmail</span></button><button type="button" class="us-integration-tile" data-email-provider="outlook"><img src="/icons/email/outlook.svg" alt=""><span>Outlook Mail</span></button><button type="button" class="us-integration-tile" data-email-provider="yahoo"><img src="/icons/email/yahoo.svg" alt=""><span>Yahoo Mail</span></button><button type="button" class="us-integration-tile" data-email-provider="custom">' + iconHtml('mail') + '<span>Custom email</span></button><button type="button" class="us-integration-tile" id="us-linear-add" aria-label="Add Linear connection">' + linearIconMarkup() + '<span>Linear</span></button></div></section><div id="us-integration-setup"></div><form id="us-linear-editor" class="settings-form" hidden><div class="settings-field"><label class="settings-label" for="us-linear-name">Connection name</label><input id="us-linear-name" class="us-text-input" maxlength="80" required placeholder="Work, personal, or client name"></div><div class="settings-field"><label class="settings-label" for="us-linear-key">Personal API key</label><input id="us-linear-key" class="us-text-input" type="password" autocomplete="off" spellcheck="false" maxlength="4096"><p id="us-linear-key-help" class="settings-hint">Existing keys stay masked.</p></div><div class="settings-form-actions"><button type="submit" class="settings-btn-sm">Save connection</button><button type="button" class="settings-btn-sm" id="us-linear-cancel">Cancel</button><button type="button" class="settings-btn-sm" id="us-linear-remove">Disconnect</button></div></form>';
11
+ bindIntegrationEditorFocus(root);
12
+ initGithubIntegration(root);
9
13
  var list = root.querySelector('#us-linear-connections'), form = root.querySelector('form');
10
14
  var name = root.querySelector('#us-linear-name'), key = root.querySelector('#us-linear-key');
11
15
  var add = root.querySelector('#us-linear-add'), remove = root.querySelector('#us-linear-remove');
12
- var status = root.querySelector('[role=status]'), retry = root.querySelector('#us-linear-retry');
16
+ var status = root.querySelector('#us-linear-collection-status'), retry = root.querySelector('#us-linear-retry');
13
17
  function message(text, error) { status.textContent = text; status.dataset.state = error ? 'error' : ''; }
14
18
  function busy(value) {
15
19
  store.set({ personalLinearSaving: value });
@@ -29,7 +33,7 @@ export function initPersonalIntegrations() {
29
33
  var button = document.createElement('button'); button.type = 'button'; button.className = 'settings-btn-sm'; button.textContent = 'Manage';
30
34
  button.setAttribute('aria-label', 'Manage ' + connection.name); button.setAttribute('aria-controls', 'us-linear-editor');
31
35
  button.setAttribute('aria-expanded', String(store.get('personalLinearEditing') === connection.id));
32
- button.onclick = function () { discardThen(function () { edit(connection); }); };
36
+ button.onclick = function () { selectIntegrationEditor('linear:' + connection.id, '#us-linear-editor', function () { edit(connection); }); };
33
37
  var brand = document.createElement('span'); brand.className = 'us-integration-brand'; brand.innerHTML = linearIconMarkup();
34
38
  var saved = document.createElement('span'); saved.className = 'us-integration-state'; saved.textContent = 'Key saved';
35
39
  controls.append(brand, copy, saved, button); row.appendChild(controls); list.appendChild(row);
@@ -46,14 +50,10 @@ export function initPersonalIntegrations() {
46
50
  }
47
51
  function close() {
48
52
  var id = store.get('personalLinearEditing');
49
- store.set({personalLinearEditing:null,personalLinearDirty:false}); form.reset(); form.hidden = true; render();
53
+ closePersonalLinearEditor(); render();
50
54
  var row = Array.from(list.children).find(function (el) { return el.dataset.connectionId === id; });
51
55
  (row ? row.querySelector('button') : add).focus();
52
56
  }
53
- function discardThen(action) {
54
- if (!store.get('personalLinearDirty')) { action(); return; }
55
- confirmSettingsAction('Discard this unsaved connection draft?', 'Discard', action);
56
- }
57
57
  // Only an open editor holds a draft; autofill or restore into the hidden form is not an edit.
58
58
  form.oninput = function () { if (!store.get('personalLinearEditing')) return; store.set({personalLinearDirty: !!key.value || name.value !== store.get('personalLinearOriginalName')}); };
59
59
  async function save(disconnect) {
@@ -69,20 +69,20 @@ export function initPersonalIntegrations() {
69
69
  if (!response.ok) throw new Error(data.error || 'Could not save connection.');
70
70
  var entries = (store.get('personalLinearConnections') || []).filter(function (entry) { return entry.id !== id; });
71
71
  if (!disconnect) entries.push(Object.assign({id:id,name:name.value.trim(),legacy:id === 'default'},data));
72
- store.set({personalLinearConnections:entries, githubWorkRefresh:null}); close(); message(disconnect ? 'Connection removed.' : 'Connection saved.');
72
+ store.set({personalLinearConnections:entries, githubWorkRefresh:null}); busy(false); close(); message(disconnect ? 'Connection removed.' : 'Connection saved.');
73
73
  } catch (error) { if (actor === store.get('myUserId') && generation === store.get('personalLinearGeneration')) message(error.message,true); }
74
74
  finally { if (generation === store.get('personalLinearGeneration')) busy(false); }
75
75
  }
76
76
  form.onsubmit = function (event) { event.preventDefault(); save(false); };
77
- add.onclick = function () { discardThen(function () { edit(null); }); };
78
- root.querySelector('#us-linear-cancel').onclick = function () { discardThen(close); };
77
+ add.onclick = function () { selectIntegrationEditor('linear:new', '#us-linear-editor', function () { edit(null); }); };
78
+ root.querySelector('#us-linear-cancel').onclick = function () { selectIntegrationEditor(null, null); };
79
79
  remove.onclick = function () { confirmSettingsAction('Disconnect this Linear connection? Work using this key will need to be reconnected. Existing issue links are retained.', 'Disconnect', function () { save(true); }); };
80
80
  retry.onclick = loadPersonalIntegrations;
81
81
  store.subscribe(function (state, previous) {
82
82
  if (state.myUserId !== previous.myUserId) {
83
83
  store.set({personalLinearConnections:null,personalLinearEditing:null,personalLinearDirty:false,personalLinearSaving:false,personalLinearGeneration:(state.personalLinearGeneration || 0)+1});
84
84
  root.dataset.connectionsLoaded = 'false'; form.reset(); form.hidden = true; busy(false); render(); message('');
85
- } else if (state.personalLinearConnections !== previous.personalLinearConnections) render();
85
+ } else if (state.personalLinearConnections !== previous.personalLinearConnections || state.personalLinearEditing !== previous.personalLinearEditing) render();
86
86
  });
87
87
  }
88
88
 
@@ -103,3 +103,14 @@ export async function loadPersonalIntegrations() {
103
103
  status.textContent = error.message; status.dataset.state = 'error'; retry.hidden = false;
104
104
  }
105
105
  }
106
+
107
+ export function closePersonalLinearEditor() {
108
+ var id = store.get('personalLinearEditing');
109
+ var form = document.getElementById('us-linear-editor');
110
+ store.set({personalLinearEditing:null,personalLinearDirty:false,personalLinearNewId:null});
111
+ if (form) { form.reset(); form.hidden = true; }
112
+ clearIntegrationEditor('linear:' + id);
113
+ var row = Array.from(document.querySelectorAll('#us-linear-connections [data-connection-id]')).find(function (el) { return el.dataset.connectionId === id; });
114
+ var trigger = row ? row.querySelector('button') : document.getElementById('us-linear-add');
115
+ if (trigger) trigger.focus();
116
+ }
@@ -1,3 +1,4 @@
1
+ import { loadGithubIntegration, leaveGithubIntegration } from './user-settings-github.js';
1
2
  import { initPersonalProviders, loadPersonalProviders, leavePersonalProviders } from './user-settings-providers.js';
2
3
  import { initPersonalIntegrations, loadPersonalIntegrations } from './user-settings-integrations.js';
3
4
  import { getWs } from './ws-ref.js';
@@ -23,22 +24,19 @@ export function buildUserSettingsLayout() {
23
24
  var accountNotifications=document.createElement('h3');accountNotifications.textContent='Account preferences';notifications.querySelector('.settings-card').before(accountNotifications);
24
25
  var chat = section('us-chat');
25
26
  var providers = document.createElement('div'); providers.className = 'us-section'; providers.dataset.section = 'us-providers';
26
- providers.innerHTML = '<h2>AI providers</h2><p class="settings-hint">Set up coding agents on the server and manage your personal audio transcription API keys.</p><div class="settings-section-heading"><h3 id="coding-agents" data-settings-section>Coding agents</h3><span id="us-provider-refresh"></span></div><div id="us-coding-agents"></div><h3 id="api-connections" data-settings-section>API connections</h3><div id="us-voice-settings"></div>';
27
+ providers.innerHTML = '<h2>AI providers</h2><p class="settings-hint">Set up coding agents, choose AI defaults and execution preferences, and manage your personal audio transcription API keys.</p><section class="settings-section-group"><div class="settings-section-heading"><h3 id="coding-agents" data-settings-section>Coding agents</h3><span id="us-provider-refresh"></span></div><div id="us-coding-agents"></div></section><section class="settings-section-group"><h3 id="ai-defaults" data-settings-section>AI defaults</h3><p class="settings-hint">Defaults for new Clay interviews and built-in AI assistance. Project defaults, individual Mates and existing conversation runtimes stay separate.</p><div id="us-default-ai"></div></section><section class="settings-section-group" id="us-execution-settings"><h3 id="execution-behavior" data-settings-section>Execution &amp; permissions</h3></section><section class="settings-section-group"><h3 id="api-connections" data-settings-section>API connections</h3><div id="us-voice-settings"></div></section><section class="settings-section-group" id="us-experimental-settings"><h3 id="experimental-features" data-settings-section>Experimental features</h3></section>';
27
28
  root.querySelector('.us-modal-content').appendChild(providers);
28
- var ai = document.createElement('div'); ai.className = 'us-section'; ai.dataset.section = 'us-ai';
29
- ai.innerHTML = '<h2>AI preferences</h2><p class="settings-hint">Defaults for new Clay interviews and built-in AI assistance. Project defaults, individual Mates and existing conversation runtimes stay separate.</p><div id="us-default-ai"></div>';
30
- root.querySelector('.us-modal-content').appendChild(ai);
31
- heading(ai, 'Execution behavior'); ai.lastElementChild.id = 'execution-behavior';
32
- ['us-auto-continue'].forEach(function (id) {ai.appendChild(document.getElementById(id).closest('.settings-card'));});
29
+ var execution = document.getElementById('us-execution-settings');
30
+ execution.appendChild(document.getElementById('us-auto-continue').closest('.settings-card'));
33
31
  document.getElementById('us-auto-continue').closest('label').querySelector('.settings-hint').textContent = 'When a rate-limited conversation becomes eligible to retry, Clay can send a continuation automatically. This may resume work and consume usage without another message from you.';
34
- heading(ai, 'Command permissions', 'These are personal Claude CLI allow rules, separate from model selection and Clay session approval modes. Managed defaults are shown for reference. Server access restrictions still apply. On an OS-user server these install in your OS home; otherwise they install in the shared server process home and can affect other Claude CLI launches there.');
35
- ai.appendChild(document.getElementById('us-claude-allow-list').closest('.settings-card'));
32
+ heading(execution, 'Command permissions', 'These are personal Claude CLI allow rules, separate from model selection and Clay session approval modes. Managed defaults are shown for reference. Server access restrictions still apply. On an OS-user server these install in your OS home; otherwise they install in the shared server process home and can affect other Claude CLI launches there.');
33
+ execution.appendChild(document.getElementById('us-claude-allow-list').closest('.settings-card'));
36
34
  var agent = document.createElement('div'); agent.className = 'us-section'; agent.dataset.section = 'us-agent';
37
35
  agent.innerHTML = '<h2>Agent behavior</h2><p class="settings-hint">Choose how your Project Drivers use Clay memory, context, tools and connections. These choices apply to your own Driver conversations.</p><div id="us-agent-behavior"></div>';
38
36
  root.querySelector('.us-modal-content').appendChild(agent);
39
37
  section('us-mates').remove();
40
- var experiments = document.createElement('details'); experiments.className = 'us-experiments'; experiments.innerHTML = '<summary>Experimental features</summary><p class="settings-hint">Personal opt-ins for features that may change.</p>';
41
- experiments.appendChild(document.getElementById('us-capsules-enabled').closest('.settings-card'));ai.appendChild(experiments);experiments.id = 'experimental-features';section('us-experimental').remove();chat.remove();
38
+ var experiments = document.createElement('details'); experiments.className = 'us-experiments'; experiments.innerHTML = '<summary>Capsules</summary><p class="settings-hint">Personal opt-ins for features that may change.</p>';
39
+ experiments.appendChild(document.getElementById('us-capsules-enabled').closest('.settings-card'));document.getElementById('us-experimental-settings').appendChild(experiments);section('us-experimental').remove();chat.remove();
42
40
  initPersonalProviders();
43
41
  section('us-email').remove();
44
42
  initPersonalIntegrations();
@@ -49,13 +47,19 @@ export function buildUserSettingsLayout() {
49
47
  root.querySelectorAll('.layout-option-icon').forEach(function (el) {el.remove();});
50
48
  }
51
49
  export function loadUserSettingsExtras(page) {
52
- if (page === 'us-integrations') loadPersonalIntegrations();
53
- if (page === 'us-providers') { loadSpeechSettings(); loadPersonalProviders(); } else leavePersonalProviders();
50
+ if (page === 'us-integrations') { loadPersonalIntegrations(); loadGithubIntegration(); }
51
+ else leaveGithubIntegration();
52
+ if (page === 'us-providers' && !store.get('personalAiSettingsMounted')) {
53
+ store.set({ personalAiSettingsMounted: true });
54
+ loadSpeechSettings(); loadPersonalProviders();
55
+ mountDefaultAi(document.getElementById('us-default-ai')); requestDefaultAi();
56
+ var ws = getWs(); if (ws && ws.readyState === 1) ws.send(JSON.stringify({type:'get_claude_allow_list'}));
57
+ } else if (page !== 'us-providers') leavePersonalAiPage();
54
58
  if (page === 'us-agent') mountAgentBehavior(document.getElementById('us-agent-behavior'));
55
- if (page === 'us-ai') { mountDefaultAi(document.getElementById('us-default-ai')); requestDefaultAi(); var ws=getWs();if(ws && ws.readyState===1)ws.send(JSON.stringify({type:'get_claude_allow_list'})); }
56
59
  }
57
60
  export function leaveAgentBehaviorPage() {unmountAgentBehavior();resetAgentBehavior();}
58
- export function finishUserSettingsExtras() {leavePersonalProviders();leaveAgentBehaviorPage();closeSpeechPicker();unmountDefaultAi(document.getElementById('us-default-ai'));}
61
+ function leavePersonalAiPage() {leavePersonalProviders();closeSpeechPicker();unmountDefaultAi(document.getElementById('us-default-ai'));store.set({personalAiSettingsMounted:false});}
62
+ export function finishUserSettingsExtras() {leaveGithubIntegration();leavePersonalAiPage();leaveAgentBehaviorPage();}
59
63
  // Returns the draft prompt for leaving one personal page (or every page when
60
64
  // pageId is omitted); the shell shows it with the shared confirmation.
61
65
  export function personalLeavePrompt(pageId) {
@@ -67,11 +71,10 @@ export function personalLeavePrompt(pageId) {
67
71
  var key = document.getElementById('us-linear-key');
68
72
  var keyDraft = key && key.value && store.get('personalLinearEditing') && !(key.form && key.form.hidden);
69
73
  var dirty = leaving('us-integrations') && (store.get('personalEmailDirty') || store.get('personalLinearDirty') || keyDraft) ||
70
- leaving('us-providers') && store.get('userVoiceDirty') ||
74
+ leaving('us-providers') && (store.get('userVoiceDirty') || store.get('defaultAiDraftDirty') || store.get('userAllowDirty')) ||
71
75
  leaving('us-notifications') && store.get('notificationDraft') ||
72
- leaving('us-appearance') && (store.get('appearanceDraft') || store.get('userTerminalDirty')) ||
73
- leaving('us-account') && (store.get('userProfileDirty') || pinDirty) ||
74
- leaving('us-ai') && (store.get('defaultAiDraftDirty') || store.get('userAllowDirty'));
76
+ leaving('us-appearance') && (store.get('appearanceDraft') || store.get('appearanceColorEditor') || store.get('userTerminalDirty')) ||
77
+ leaving('us-account') && (store.get('userProfileDirty') || pinDirty);
75
78
  if (!dirty) return null;
76
79
  return { message: 'Your unsaved changes will remain in this tab. Leave this editor and keep the draft?', action: 'Keep draft and leave' };
77
80
  }
@@ -1,9 +1,12 @@
1
+ import { createWorkspaceBackgroundGallery } from './workspace-background-gallery.js';
1
2
  import { store } from './store.js';
2
3
  import { getThemes, retryAppearanceSave, getAppearanceChoices, selectAppearanceMode, selectAppearanceTheme, onThemeChange, importLegacyAppearance } from './theme.js';
3
4
  import { enhanceSettingsSelects } from './settings-select.js';
4
5
 
5
6
  export function initPersonalThemes(root) {
6
7
  root.innerHTML = '<h3>Theme</h3><div class="us-preference-row"><label class="settings-label" for="us-theme-mode">Appearance</label><select id="us-theme-mode" class="settings-select"><option value="system">Follow system</option><option value="light">Light</option><option value="dark">Dark</option></select></div><p class="settings-hint">Choose a palette for each appearance. Switching light and dark keeps both choices.</p><div class="us-theme-pair"></div><p id="us-appearance-status" class="settings-status" role="status"></p>';
8
+ var syncBackground = createWorkspaceBackgroundGallery(root);
9
+ root.appendChild(root.querySelector('#us-appearance-status'));
7
10
  var mode = root.querySelector('select');
8
11
  mode.onchange = function () { selectAppearanceMode(mode.value); };
9
12
  ['light', 'dark'].forEach(function (variant) {
@@ -13,6 +16,7 @@ export function initPersonalThemes(root) {
13
16
  root.querySelector('.us-theme-pair').appendChild(group);
14
17
  });
15
18
  function sync() {
19
+ syncBackground();
16
20
  var themes = getThemes(); var choices = getAppearanceChoices(); mode.value = choices.mode; mode.dispatchEvent(new Event('settings-sync'));
17
21
  ['light', 'dark'].forEach(function (variant) {
18
22
  var select = root.querySelector('#us-theme-' + variant); select.replaceChildren();
@@ -31,8 +35,8 @@ export function initPersonalThemes(root) {
31
35
  var retry = document.createElement('button'); retry.type = 'button'; retry.className = 'settings-btn-sm'; retry.textContent = 'Retry save'; retry.hidden = !store.get('appearanceSaveError'); retry.onclick = retryAppearanceSave; root.appendChild(retry);
32
36
  var legacy = document.createElement('button'); legacy.type = 'button'; legacy.className = 'settings-btn-sm'; legacy.textContent = "Import this browser's previous theme"; legacy.onclick = importLegacyAppearance; root.appendChild(legacy);
33
37
  store.subscribe(function (state, previous) {
34
- legacy.hidden = !state.legacyAppearanceAvailable; retry.hidden = !state.appearanceSaveError;
35
- if (state.appearanceDraft !== previous.appearanceDraft || state.appearancePreference !== previous.appearancePreference) sync();
38
+ legacy.hidden = !state.legacyAppearanceAvailable; retry.hidden = !state.appearanceSaveError || !!state.appearanceColorEditor;
39
+ if (state.appearanceDraft !== previous.appearanceDraft || state.appearancePreference !== previous.appearancePreference || state.appearanceColorEditor !== previous.appearanceColorEditor || state.appearanceColorPreview !== previous.appearanceColorPreview || state.appearancePendingSaves !== previous.appearancePendingSaves) sync();
36
40
  });
37
41
  legacy.hidden = !store.get('legacyAppearanceAvailable'); onThemeChange(sync); sync(); enhanceSettingsSelects(root);
38
42
  }
@@ -1,3 +1,4 @@
1
+ import { integrationEditorBusy } from './personal-integration-editor.js';
1
2
  import { initPersonalAppearance } from './user-settings-appearance.js';
2
3
  import { initAccountSessions, resetAccountSessions } from './user-settings-sessions.js';
3
4
  import { initPersonalVoice } from './user-settings-voice.js';
@@ -38,7 +39,7 @@ export function initUserSettings() {
38
39
  enter: function () { populateAccount(); },
39
40
  activate: function (page) { switchSection(page); },
40
41
  leave: function () { currentPage = null; resetAccountSessions(); finishUserSettingsExtras(); },
41
- busy: function () { return store.get('speechPopup') ? 'Finish or close the open voice settings editor first.' : ''; },
42
+ busy: function () { if (integrationEditorBusy()) return 'Wait for the current connection operation to finish.'; return store.get('speechPopup') ? 'Finish or close the open voice settings editor first.' : ''; },
42
43
  leavePrompt: function (kind, pageId) { return personalLeavePrompt(kind === 'page' ? pageId : undefined); }
43
44
  });
44
45
  pinForm = document.getElementById('us-pin-form');
@@ -0,0 +1,92 @@
1
+ import { store } from './store.js';
2
+ import { workspaceBackgrounds, selectAppearanceBackground } from './appearance-preferences.js';
3
+ import { createCustomColorEditor, openCustomColor, cancelCustomColor } from './workspace-custom-colors.js';
4
+ import { registerTooltip } from './tooltip.js';
5
+
6
+ function chevron(right) { return '<svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="' + (right ? 'm8 5 5 5-5 5' : 'm12 5-5 5 5 5') + '" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>'; }
7
+ export function createWorkspaceBackgroundGallery(root) {
8
+ var gallery = document.createElement('div');
9
+ gallery.id = 'us-workspace-background'; gallery.className = 'us-background-gallery';
10
+ gallery.setAttribute('role', 'radiogroup'); gallery.setAttribute('aria-label', 'Workspace background');
11
+ gallery.setAttribute('aria-describedby', 'us-background-help');
12
+ gallery.innerHTML = '<h3>Workspace background</h3><p id="us-background-help" class="settings-hint">Choose a color or picture. Both follow your light or dark appearance.</p><div class="us-background-palette"><button type="button" class="us-palette-arrow" data-step="-1" aria-label="Previous colors" hidden>' + chevron(false) + '</button><div class="us-background-colors"></div><button type="button" class="us-palette-arrow" data-step="1" aria-label="Next colors" hidden>' + chevron(true) + '</button><button type="button" id="us-add-color" class="us-palette-add" aria-label="Add custom color" aria-haspopup="dialog" aria-expanded="false" aria-controls="us-custom-color-editor"><svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M10 4v12M4 10h12" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg></button></div><h4>Pictures</h4><div class="us-background-pictures"></div>';
13
+ var strip = gallery.querySelector('.us-background-colors');
14
+ var add = gallery.querySelector('#us-add-color'); registerTooltip(add, 'Add custom color');
15
+ add.dataset.tipSuppressWhenOpen = '#us-custom-color-editor';
16
+ function addChoice(color, container) {
17
+ var button = document.createElement('button'); button.type = 'button'; button.className = 'us-background-choice';
18
+ button.dataset.backgroundChoice = color ? color.id : 'workshop';
19
+ button.setAttribute('role', 'radio'); button.setAttribute('aria-label', color ? color.name : 'Workshop');
20
+ var preview = document.createElement('span'); preview.className = 'us-background-sample'; preview.setAttribute('aria-hidden', 'true');
21
+ if (color) { preview.style.setProperty('--swatch-light', color.light); preview.style.setProperty('--swatch-dark', color.dark); registerTooltip(button, color.name); }
22
+ else { preview.classList.add('us-background-workshop'); }
23
+ button.appendChild(preview);
24
+ if (!color) { var label = document.createElement('span'); label.textContent = 'Workshop'; button.appendChild(label); }
25
+ container.appendChild(button);
26
+ button.onclick = function () { selectAppearanceBackground(color ? 'plain' : 'workshop', color ? color.id : undefined); };
27
+ }
28
+ function overflow() {
29
+ var overflowing = strip.scrollWidth > strip.clientWidth + 1;
30
+ gallery.querySelectorAll('[data-step]').forEach(function (button) {
31
+ button.hidden = !overflowing;
32
+ button.disabled = button.dataset.step === '-1' ? strip.scrollLeft < 1 : strip.scrollLeft + strip.clientWidth >= strip.scrollWidth - 1;
33
+ });
34
+ }
35
+ gallery.querySelectorAll('[data-step]').forEach(function (button) {
36
+ registerTooltip(button, button.getAttribute('aria-label'));
37
+ button.onclick = function () { strip.scrollBy({ left: Number(button.dataset.step) * strip.clientWidth * 0.8 }); };
38
+ });
39
+ strip.onscroll = overflow;
40
+ function reveal(button) {
41
+ if (!button || !strip.contains(button) || !strip.clientWidth) return;
42
+ var box = button.getBoundingClientRect(), bounds = strip.getBoundingClientRect();
43
+ if (box.left < bounds.left) strip.scrollLeft -= bounds.left - box.left;
44
+ if (box.right > bounds.right) strip.scrollLeft += box.right - bounds.right;
45
+ }
46
+ function layout() {
47
+ overflow();
48
+ var visible = strip.clientWidth > 0;
49
+ if (visible && (strip.dataset.visible !== 'true' || strip.dataset.revealPending === 'true')) {
50
+ reveal(strip.querySelector('[aria-checked="true"]'));
51
+ delete strip.dataset.revealPending;
52
+ }
53
+ strip.dataset.visible = String(visible);
54
+ }
55
+ strip.addEventListener('focusin', function (event) { reveal(event.target); });
56
+ addChoice(null, gallery.querySelector('.us-background-pictures'));
57
+ add.onclick = function () { if (store.get('appearanceColorEditor')) cancelCustomColor(); else openCustomColor(); };
58
+ var syncEditor = createCustomColorEditor(gallery, add);
59
+ var buttons = [], renderedColors = null;
60
+ gallery.onkeydown = function (event) {
61
+ var index = buttons.indexOf(event.target); if (index < 0) return;
62
+ var target;
63
+ if (event.key === 'ArrowRight' || event.key === 'ArrowDown') target = (index + 1) % buttons.length;
64
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') target = (index + buttons.length - 1) % buttons.length;
65
+ if (event.key === 'Home') target = 0;
66
+ if (event.key === 'End') target = buttons.length - 1;
67
+ if (target === undefined) return;
68
+ event.preventDefault(); buttons[target].focus({ preventScroll: strip.contains(buttons[target]) }); buttons[target].click();
69
+ };
70
+ root.appendChild(gallery);
71
+ new ResizeObserver(layout).observe(strip);
72
+ return function () {
73
+ var current = store.get('appearanceDraft') || store.get('appearancePreference') || {};
74
+ var colors = workspaceBackgrounds.colors.concat(current.customBackgroundColors || []);
75
+ if (renderedColors !== JSON.stringify(colors)) {
76
+ var focused = document.activeElement && document.activeElement.dataset.backgroundChoice;
77
+ renderedColors = JSON.stringify(colors); strip.replaceChildren();
78
+ colors.forEach(function (color) { addChoice(color, strip); });
79
+ if (focused) { var restored = Array.from(strip.children).find(function (button) { return button.dataset.backgroundChoice === focused; }); if (restored) restored.focus({ preventScroll: true }); }
80
+ }
81
+ buttons = Array.from(gallery.querySelectorAll('[role="radio"]'));
82
+ var editor = store.get('appearanceColorEditor'); add.disabled = !!store.get('appearancePendingSaves');
83
+ add.setAttribute('aria-expanded', String(!!editor)); syncEditor();
84
+ var selected = current.background === 'workshop' ? 'workshop' : workspaceBackgrounds.getColor(current.backgroundColor, current.customBackgroundColors).id;
85
+ if (strip.dataset.selection !== selected) { strip.dataset.selection = selected; strip.dataset.revealPending = 'true'; }
86
+ buttons.forEach(function (button) {
87
+ var checked = button.dataset.backgroundChoice === selected;
88
+ button.setAttribute('aria-checked', String(checked)); button.tabIndex = checked ? 0 : -1; button.disabled = !!(editor && editor.saving);
89
+ });
90
+ layout();
91
+ };
92
+ }
@@ -0,0 +1,132 @@
1
+ import { store } from './store.js';
2
+ import { workspaceBackgrounds, queueAppearanceSave, appearanceStatus } from './appearance-preferences.js';
3
+ import { registerTooltip, dismissTooltip } from './tooltip.js';
4
+
5
+ function preference() { return store.get('appearanceDraft') || store.get('appearancePreference') || {}; }
6
+ function clamp(value, max) { return Math.max(0, Math.min(max, value)); }
7
+ function hsv(hex) {
8
+ var rgb = hex.slice(1).match(/../g).map(function (part) { return parseInt(part, 16) / 255; });
9
+ var max = Math.max.apply(null, rgb), min = Math.min.apply(null, rgb), delta = max - min, hue = 0;
10
+ if (delta) {
11
+ if (max === rgb[0]) hue = ((rgb[1] - rgb[2]) / delta) % 6;
12
+ else if (max === rgb[1]) hue = (rgb[2] - rgb[0]) / delta + 2;
13
+ else hue = (rgb[0] - rgb[1]) / delta + 4;
14
+ }
15
+ return { hue: (hue * 60 + 360) % 360, saturation: max ? delta / max * 100 : 0, value: max * 100 };
16
+ }
17
+ function hex(color) {
18
+ var chroma = color.value / 100 * color.saturation / 100;
19
+ var x = chroma * (1 - Math.abs((color.hue / 60) % 2 - 1)), m = color.value / 100 - chroma;
20
+ var rgb = color.hue < 60 ? [chroma,x,0] : color.hue < 120 ? [x,chroma,0] : color.hue < 180 ? [0,chroma,x] : color.hue < 240 ? [0,x,chroma] : color.hue < 300 ? [x,0,chroma] : [chroma,0,x];
21
+ return '#' + rgb.map(function (value) { return Math.round((value + m) * 255).toString(16).padStart(2, '0'); }).join('').toUpperCase();
22
+ }
23
+ export function openCustomColor() {
24
+ if (store.get('appearancePendingSaves') || store.get('appearanceColorEditor')) return;
25
+ var current = preference(), source = workspaceBackgrounds.getColor(current.backgroundColor, current.customBackgroundColors);
26
+ var variant = document.documentElement.classList.contains('light-theme') ? 'light' : 'dark';
27
+ var draft = Object.assign({ id: 'custom-' + crypto.randomUUID(), light: source.light.toUpperCase(), dark: source.dark.toUpperCase(), variant: variant, saving: false, error: '', previousStatus: store.get('appearanceSaveStatus') || '', previousError: !!store.get('appearanceSaveError') }, hsv(source[variant]));
28
+ store.set({ appearanceColorEditor: draft }); dismissTooltip();
29
+ document.getElementById((current.customBackgroundColors || []).length >= workspaceBackgrounds.maxCustom ? 'us-color-cancel' : 'us-color-area').focus();
30
+ }
31
+ export function cancelCustomColor(restoreFocus) {
32
+ var editor = store.get('appearanceColorEditor'); if (!editor || editor.saving) return;
33
+ store.set({ appearanceColorEditor: null, appearanceColorPreview: null });
34
+ appearanceStatus(editor.previousStatus, editor.previousError);
35
+ if (restoreFocus !== false) document.getElementById('us-add-color').focus();
36
+ }
37
+ function updateEditor(change, visual) {
38
+ var editor = store.get('appearanceColorEditor'); if (!editor || editor.saving) return;
39
+ var next = Object.assign({}, editor, change, { error: '' });
40
+ if (visual) next[next.variant] = hex(next);
41
+ var custom = workspaceBackgrounds.normalizeCustom([{ id: next.id, name: next.light + ' / ' + next.dark, light: next.light, dark: next.dark }]);
42
+ store.set({ appearanceColorEditor: next, appearanceColorPreview: custom ? custom[0] : store.get('appearanceColorPreview') });
43
+ }
44
+ async function saveEditor() {
45
+ var editor = store.get('appearanceColorEditor'); if (!editor || editor.saving) return;
46
+ var custom = workspaceBackgrounds.normalizeCustom([{ id: editor.id, name: editor.light + ' / ' + editor.dark, light: editor.light, dark: editor.dark }]);
47
+ if (!custom) { store.set({ appearanceColorEditor: Object.assign({}, editor, { error: 'Use six-digit HEX colors, such as #E9CBD4.' }) }); document.getElementById('us-color-hex').focus(); return; }
48
+ var current = preference(), colors = (current.customBackgroundColors || []).concat(custom);
49
+ if (!workspaceBackgrounds.normalizeCustom(colors)) return;
50
+ var values = Object.assign({}, current, { customBackgroundColors: colors, background: 'plain', backgroundColor: editor.id });
51
+ store.set({ appearanceColorEditor: Object.assign({}, editor, { saving: true, error: '' }) });
52
+ var saved = await queueAppearanceSave(values), active = store.get('appearanceColorEditor');
53
+ if (!active || active.id !== editor.id) return;
54
+ if (saved) {
55
+ store.set({ appearanceColorEditor: null, appearanceColorPreview: null });
56
+ var button = Array.from(document.querySelectorAll('[data-background-choice]')).find(function (node) { return node.dataset.backgroundChoice === editor.id; });
57
+ if (button) button.focus({ preventScroll: true });
58
+ } else {
59
+ if (store.get('appearanceDraft') === values) {
60
+ var acknowledged = store.get('appearancePreference') || {};
61
+ var unchanged = ['theme','mode','lightTheme','darkTheme','background','backgroundColor','customBackgroundColors'].every(function (key) { return JSON.stringify(current[key] || null) === JSON.stringify(acknowledged[key] || null); });
62
+ store.set({ appearanceDraft: unchanged ? null : current });
63
+ }
64
+ store.set({ appearanceColorEditor: Object.assign({}, active, { saving: false, error: 'Could not save. Your preview remains. Try Add again.' }) });
65
+ document.getElementById('us-color-save').focus();
66
+ }
67
+ }
68
+ export function createCustomColorEditor(host, anchor) {
69
+ var form = document.createElement('form'); form.id = 'us-custom-color-editor'; form.className = 'us-color-popover'; form.hidden = true;
70
+ form.setAttribute('role', 'dialog'); form.setAttribute('aria-label', 'Add color');
71
+ form.innerHTML = '<div class="us-color-top"><div class="us-color-modes" role="group" aria-label="Color appearance"><button type="button" data-variant="light" aria-label="Light color"><svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><circle cx="10" cy="10" r="3"/><path d="M10 1v2m0 14v2M1 10h2m14 0h2M4 4l1.5 1.5m9 9L16 16M4 16l1.5-1.5m9-9L16 4"/></svg></button><button type="button" data-variant="dark" aria-label="Dark color"><svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M16.7 12A7 7 0 0 1 8 3.3 7 7 0 1 0 16.7 12Z"/></svg></button></div><span class="us-color-preview" aria-hidden="true"></span></div><div class="us-color-controls"><div id="us-color-area" class="us-color-area" role="slider" tabindex="0" aria-label="Saturation and brightness" aria-valuemin="0" aria-valuemax="100" aria-describedby="us-color-help"><span></span></div><span id="us-color-help" class="sr-only">Left and right adjust saturation. Up and down adjust brightness.</span><input id="us-color-hue" type="range" min="0" max="359" step="1" aria-label="Hue"><label for="us-color-hex" class="sr-only">HEX color</label><input id="us-color-hex" class="us-text-input" maxlength="7" spellcheck="false" autocomplete="off" aria-describedby="us-color-error"></div><p id="us-color-error" class="settings-status" role="status"></p><div class="settings-form-actions"><button id="us-color-save" class="us-btn" type="submit">Add</button><button id="us-color-cancel" class="settings-btn-sm" type="button">Cancel</button></div>';
72
+ var area = form.querySelector('#us-color-area'), hue = form.querySelector('#us-color-hue'), input = form.querySelector('#us-color-hex');
73
+ function position() {
74
+ if (form.hidden) return;
75
+ var box = anchor.getBoundingClientRect();
76
+ var left = clamp(box.right - form.offsetWidth, window.innerWidth - form.offsetWidth - 8);
77
+ var top = box.bottom + 6;
78
+ if (top + form.offsetHeight > window.innerHeight - 8) top = box.top - form.offsetHeight - 6;
79
+ form.style.left = Math.max(8, left) + 'px'; form.style.top = Math.max(8, top) + 'px';
80
+ }
81
+ form.querySelectorAll('[data-variant]').forEach(function (button) {
82
+ registerTooltip(button, button.getAttribute('aria-label'));
83
+ button.onclick = function () { var editor = store.get('appearanceColorEditor'); if (editor && !editor.saving) updateEditor(Object.assign({ variant: button.dataset.variant }, hsv(workspaceBackgrounds.normalizeHex(editor[button.dataset.variant]) || '#808080'))); };
84
+ });
85
+ input.oninput = function () {
86
+ var editor = store.get('appearanceColorEditor'), change = {}; if (!editor) return;
87
+ change[editor.variant] = input.value; var valid = workspaceBackgrounds.normalizeHex(input.value);
88
+ if (valid) Object.assign(change, hsv(valid)); updateEditor(change);
89
+ };
90
+ hue.oninput = function () { updateEditor({ hue: Number(hue.value) }, true); };
91
+ function point(event) {
92
+ var editor = store.get('appearanceColorEditor'); if (!editor || editor.saving) return;
93
+ var box = area.getBoundingClientRect();
94
+ updateEditor({ saturation: clamp((event.clientX - box.left) / box.width * 100, 100), value: clamp(100 - (event.clientY - box.top) / box.height * 100, 100) }, true);
95
+ }
96
+ area.onpointerdown = function (event) { if (event.button !== 0) return; event.preventDefault(); area.focus(); area.setPointerCapture(event.pointerId); point(event); };
97
+ area.onpointermove = function (event) { if (area.hasPointerCapture(event.pointerId)) point(event); };
98
+ area.onkeydown = function (event) {
99
+ var editor = store.get('appearanceColorEditor'); if (!editor) return;
100
+ var change = {}, amount = event.shiftKey ? 10 : 1;
101
+ if (event.key === 'ArrowLeft') change.saturation = clamp(editor.saturation - amount, 100);
102
+ if (event.key === 'ArrowRight') change.saturation = clamp(editor.saturation + amount, 100);
103
+ if (event.key === 'ArrowUp') change.value = clamp(editor.value + amount, 100);
104
+ if (event.key === 'ArrowDown') change.value = clamp(editor.value - amount, 100);
105
+ if (!Object.keys(change).length) return;
106
+ event.preventDefault(); updateEditor(change, true);
107
+ };
108
+ form.querySelector('#us-color-cancel').onclick = function () { cancelCustomColor(); };
109
+ form.onsubmit = function (event) { event.preventDefault(); saveEditor(); };
110
+ form.onkeydown = function (event) { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); cancelCustomColor(); } };
111
+ document.addEventListener('pointerdown', function (event) { if (!form.hidden && !form.contains(event.target) && !anchor.contains(event.target)) cancelCustomColor(false); }, true);
112
+ form.addEventListener('focusout', function () { setTimeout(function () { if (!form.hidden && !form.contains(document.activeElement) && document.activeElement !== anchor) cancelCustomColor(false); }, 0); });
113
+ window.addEventListener('resize', position); document.addEventListener('scroll', position, true);
114
+ host.appendChild(form);
115
+ return function () {
116
+ var editor = store.get('appearanceColorEditor'); form.hidden = !editor; if (!editor) return;
117
+ var limit = (preference().customBackgroundColors || []).length >= workspaceBackgrounds.maxCustom && !editor.saving;
118
+ form.querySelector('.us-color-controls').hidden = limit; form.querySelector('.us-color-top').hidden = limit;
119
+ form.querySelectorAll('[data-variant]').forEach(function (button) { button.setAttribute('aria-pressed', String(editor.variant === button.dataset.variant)); });
120
+ input.value = editor[editor.variant]; input.setAttribute('aria-label', (editor.variant === 'light' ? 'Light' : 'Dark') + ' HEX'); input.setAttribute('aria-invalid', String(!workspaceBackgrounds.normalizeHex(input.value)));
121
+ form.querySelector('.us-color-preview').style.backgroundColor = workspaceBackgrounds.normalizeHex(input.value) || 'transparent';
122
+ area.style.setProperty('--picker-hue', String(editor.hue));
123
+ area.querySelector('span').style.left = editor.saturation + '%'; area.querySelector('span').style.top = (100 - editor.value) + '%';
124
+ area.setAttribute('aria-valuenow', String(Math.round(editor.saturation)));
125
+ area.setAttribute('aria-valuetext', 'Saturation ' + Math.round(editor.saturation) + '%, brightness ' + Math.round(editor.value) + '%');
126
+ hue.value = Math.round(editor.hue); hue.setAttribute('aria-valuetext', hue.value + ' degrees');
127
+ form.querySelector('#us-color-error').textContent = limit ? '12 custom colors saved.' : editor.error || (editor.saving ? 'Saving…' : '');
128
+ form.querySelectorAll('input,button').forEach(function (control) { control.disabled = editor.saving; });
129
+ form.querySelector('#us-color-save').hidden = limit;
130
+ position();
131
+ };
132
+ }
@@ -2,6 +2,7 @@
2
2
  import { store } from './store.js';
3
3
  import { sendYou, openYou } from './you.js';
4
4
  import { relativeTime } from './project-logs-render.js';
5
+ import { registerTooltip } from './tooltip.js';
5
6
 
6
7
  function state() { return store.get('youActivity') || {}; }
7
8
  function put(values) { store.set({ youActivity: Object.assign({}, state(), values) }); }
@@ -24,7 +25,7 @@ function announce(message) {
24
25
  put({ active: true });
25
26
  el('you-activity').classList.add('announcing');
26
27
  el('you-activity-toggle').setAttribute('aria-label', message);
27
- el('you-activity-toggle').title = message;
28
+ registerTooltip(el('you-activity-toggle'), message);
28
29
  var characters = Array.from(message);
29
30
  var index = 0;
30
31
  var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
@@ -94,7 +95,8 @@ export function handleYouActivity(msg) {
94
95
  }
95
96
  export function initYouActivity() {
96
97
  var host = document.createElement('div'); host.id = 'you-activity';
97
- host.innerHTML = '<button id="you-activity-toggle" type="button" aria-expanded="false" aria-controls="you-activity-history"><span id="you-activity-text" aria-hidden="true"></span></button>' +
98
+ host.innerHTML = '<button id="you-activity-toggle" type="button" aria-expanded="false" aria-controls="you-activity-history">' +
99
+ '<span id="you-activity-text" aria-hidden="true"></span><svg class="you-activity-chevron" viewBox="0 0 16 16" aria-hidden="true"><path d="m4 6 4 4 4-4"/></svg></button>' +
98
100
  '<section id="you-activity-history" class="hidden" aria-label="Clay’s memory activity"><header><strong>Clay’s memory</strong><button id="you-activity-close" type="button" aria-label="Close memory activity">Close</button></header>' +
99
101
  '<div id="you-activity-list"></div><button id="you-activity-more" class="hidden" type="button">Load earlier activity</button><button id="you-activity-open" type="button">Open Me</button></section>';
100
102
  document.querySelector('.top-bar-left-pills').prepend(host);