@chatcode/cco-better-sidebar 0.18.0-rc.4 → 0.18.0-rc.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.
- package/README.md +14 -0
- package/README_EN.md +14 -0
- package/lib/client-editor.js +271 -260
- package/lib/client-mermaid.js +271 -260
- package/lib/client-registry.js +659 -509
- package/lib/client-terminal.js +271 -260
- package/lib/client.js +653 -503
- package/lib/index.js +115 -4
- package/lib/types/client/LogoutConfirmDialog.d.ts +5 -0
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/ui-tweaks/branding.d.ts +1 -1
- package/lib/types/client/ui-tweaks/sidebar-header.d.ts +6 -0
- package/lib/types/login-page-script.d.ts +2 -0
- package/lib/types/login-route.d.ts +2 -0
- package/package.json +1 -1
- package/src/client/AccountMenu.tsx +12 -2
- package/src/client/LogoutConfirmDialog.tsx +39 -0
- package/src/client/locales-ja.ts +2 -0
- package/src/client/locales.ts +6 -0
- package/src/client/sidebar.module.css +81 -19
- package/src/client/ui-tweaks/branding.tsx +10 -9
- package/src/client/ui-tweaks/sidebar-header.ts +73 -0
- package/src/login-page-script.ts +107 -0
- package/src/login-route.ts +7 -4
package/lib/index.js
CHANGED
|
@@ -865,13 +865,124 @@ function registerBundleRoute(ctx, fence) {
|
|
|
865
865
|
});
|
|
866
866
|
}
|
|
867
867
|
//#endregion
|
|
868
|
+
//#region src/login-page-script.ts
|
|
869
|
+
/** Standalone sign-in recovery; each document starts once and always keeps a usable reopen action. */
|
|
870
|
+
const CHATCODE_LOGIN_PAGE_SCRIPT = String.raw`
|
|
871
|
+
(() => {
|
|
872
|
+
const message = document.getElementById('message');
|
|
873
|
+
const button = document.getElementById('login');
|
|
874
|
+
const fallback = document.getElementById('login-fallback');
|
|
875
|
+
const link = document.getElementById('login-link');
|
|
876
|
+
const current = new URL(location.href);
|
|
877
|
+
const returnTo = (() => {
|
|
878
|
+
const value = current.searchParams.get('return');
|
|
879
|
+
if (!value || !value.startsWith('/') || value.startsWith('//')) return '/';
|
|
880
|
+
const target = new URL(value, location.origin);
|
|
881
|
+
return target.origin === location.origin ? target.pathname + target.search + target.hash : '/';
|
|
882
|
+
})();
|
|
883
|
+
let busy = false, checking = false, attempted = false, recheck = false, attempt, popup;
|
|
884
|
+
const call = async name => {
|
|
885
|
+
const response = await fetch('/sidebar/api/' + name, {
|
|
886
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}',
|
|
887
|
+
});
|
|
888
|
+
const data = await response.json();
|
|
889
|
+
if (!response.ok || !data.ok) throw new Error('操作失败');
|
|
890
|
+
return data.value;
|
|
891
|
+
};
|
|
892
|
+
const showMessage = (text, error = false) => {
|
|
893
|
+
message.textContent = text;
|
|
894
|
+
message.classList.toggle('error', error);
|
|
895
|
+
};
|
|
896
|
+
async function login() {
|
|
897
|
+
if (busy) return;
|
|
898
|
+
attempted = true;
|
|
899
|
+
busy = true;
|
|
900
|
+
button.disabled = true;
|
|
901
|
+
button.textContent = '正在打开登录页…';
|
|
902
|
+
let tab = null;
|
|
903
|
+
try { tab = window.open('', '_blank'); if (tab) tab.opener = null; } catch {}
|
|
904
|
+
try {
|
|
905
|
+
if (!attempt) {
|
|
906
|
+
const started = await call('chatcode.start');
|
|
907
|
+
const url = new URL(started.url);
|
|
908
|
+
if (!['http:', 'https:'].includes(url.protocol) || typeof started.sessionId !== 'string') throw new Error('登录地址无效');
|
|
909
|
+
attempt = { sessionId: started.sessionId, url: url.href };
|
|
910
|
+
}
|
|
911
|
+
link.href = attempt.url;
|
|
912
|
+
link.textContent = attempt.url;
|
|
913
|
+
fallback.hidden = false;
|
|
914
|
+
if (tab && !tab.closed) {
|
|
915
|
+
try { tab.location.href = attempt.url; popup = tab; } catch { try { tab.close(); } catch {} }
|
|
916
|
+
}
|
|
917
|
+
showMessage('请在登录页完成登录;若网页已关闭,可随时重新打开。');
|
|
918
|
+
} catch {
|
|
919
|
+
try { if (tab) tab.close(); } catch {}
|
|
920
|
+
showMessage('登录页打开失败,请重试。', true);
|
|
921
|
+
} finally {
|
|
922
|
+
busy = false;
|
|
923
|
+
button.disabled = false;
|
|
924
|
+
button.textContent = attempt ? '重新打开登录页' : '去登录';
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async function status() {
|
|
928
|
+
if (checking || busy) return;
|
|
929
|
+
checking = true;
|
|
930
|
+
const observedAttempt = attempt, observedStarted = attempted;
|
|
931
|
+
try {
|
|
932
|
+
const state = await call('chatcode.status');
|
|
933
|
+
if (busy || attempt !== observedAttempt || attempted !== observedStarted) return;
|
|
934
|
+
if (state.available && state.validation === 'valid' && !state.expired) {
|
|
935
|
+
try { if (popup) popup.close(); window.focus(); } catch {}
|
|
936
|
+
location.replace(returnTo);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (!state.available) {
|
|
940
|
+
recheck = true;
|
|
941
|
+
button.textContent = '重新检测';
|
|
942
|
+
showMessage('登录服务暂不可用,请稍后重试。', true);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (state.configured && state.validation === 'unavailable') {
|
|
946
|
+
recheck = true;
|
|
947
|
+
button.textContent = '重新检测';
|
|
948
|
+
showMessage('暂时无法验证已有账号,请检查网络后重新检测。', true);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
recheck = false;
|
|
952
|
+
button.textContent = attempt ? '重新打开登录页' : '去登录';
|
|
953
|
+
if (attempt && (state.login?.sessionId !== attempt.sessionId || state.login.state !== 'pending')) {
|
|
954
|
+
attempt = undefined;
|
|
955
|
+
fallback.hidden = true;
|
|
956
|
+
link.removeAttribute('href');
|
|
957
|
+
link.textContent = '';
|
|
958
|
+
button.textContent = '去登录';
|
|
959
|
+
showMessage('登录请求已失效,请重新登录。');
|
|
960
|
+
}
|
|
961
|
+
if (!attempted) await login();
|
|
962
|
+
} catch {
|
|
963
|
+
recheck = true;
|
|
964
|
+
button.textContent = '重新检测';
|
|
965
|
+
showMessage('无法读取登录状态,请稍后重试。', true);
|
|
966
|
+
} finally {
|
|
967
|
+
checking = false;
|
|
968
|
+
if (!busy) button.disabled = false;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
button.addEventListener('click', () => { void (recheck ? status() : login()); });
|
|
972
|
+
void status();
|
|
973
|
+
setInterval(() => { void status(); }, 2000);
|
|
974
|
+
})();
|
|
975
|
+
`;
|
|
976
|
+
//#endregion
|
|
868
977
|
//#region src/login-route.ts
|
|
869
978
|
const assetPath = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "logo.svg");
|
|
870
|
-
|
|
979
|
+
/** Render the standalone login gate with one automatic attempt and persistent manual recovery. */
|
|
980
|
+
function renderChatcodeLoginPage(logo) {
|
|
871
981
|
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>请登录 ChatCode</title><style>
|
|
872
982
|
*{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh;display:grid;place-items:center;font-family:Inter,"Microsoft YaHei",system-ui,sans-serif;color:#172b4d;background:radial-gradient(circle at 18% 18%,#e5edff 0,transparent 30rem),linear-gradient(135deg,#edf2fb,#dfe8f7)}.card{width:min(490px,calc(100vw - 40px));padding:34px 44px 36px;border:1px solid #dce4ef;border-radius:18px;background:#fff;box-shadow:0 20px 48px rgba(36,62,103,.18),0 2px 8px rgba(36,62,103,.06)}.brand{display:flex;align-items:center;justify-content:center;gap:12px;padding-bottom:27px;border-bottom:1px solid #edf0f5}.brand img{width:34px;height:34px}.brand strong{font-size:22px;letter-spacing:-.02em}.content{padding-top:40px;text-align:center}.content h1{margin:0;font-size:28px;line-height:1.3;font-weight:650}.content p{min-height:24px;margin:12px 0 30px;color:#71809a;font-size:14px;line-height:1.7}.action{width:100%;min-height:46px;border:0;border-radius:8px;background:#9B0B09;color:#fff;font:600 15px/1 Inter,"Microsoft YaHei",sans-serif;cursor:pointer;box-shadow:0 8px 18px rgba(155,11,9,.25);transition:transform .18s ease,box-shadow .18s ease,background .18s ease}.action:hover:not(:disabled){transform:translateY(-2px);background:#b20d0a;box-shadow:0 12px 24px rgba(155,11,9,.38)}.action:focus-visible{outline:3px solid #f0b7b5;outline-offset:3px}.action:disabled{cursor:default;opacity:.7}.error{color:#b42318}@media(max-width:480px){.card{padding:30px 26px}.content{padding-top:32px}}
|
|
873
|
-
|
|
874
|
-
|
|
983
|
+
.login-fallback{margin:18px 0 0;color:#71809a;font-size:12px;line-height:1.7;overflow-wrap:anywhere}.login-fallback a{display:block;margin-top:4px;color:#315da8;text-underline-offset:3px}
|
|
984
|
+
</style></head><body><main class="card"><div class="brand"><img src="data:image/svg+xml;base64,${logo}" alt=""><strong>ChatCode</strong></div><section class="content"><h1>请登录 ChatCode</h1><p id="message" aria-live="polite">正在检查登录状态…</p><button id="login" class="action" type="button" disabled>正在检查登录状态…</button><div id="login-fallback" class="login-fallback" hidden>若登录网页已关闭或未弹出,可再次访问:<a id="login-link" target="_blank" rel="noopener noreferrer"></a></div></section></main><script>
|
|
985
|
+
${CHATCODE_LOGIN_PAGE_SCRIPT}
|
|
875
986
|
<\/script></body></html>`;
|
|
876
987
|
}
|
|
877
988
|
function registerLoginRoute(ctx, fence) {
|
|
@@ -896,7 +1007,7 @@ function registerLoginRoute(ctx, fence) {
|
|
|
896
1007
|
"cache-control": "no-store",
|
|
897
1008
|
"content-security-policy": "default-src 'self'; img-src 'self' data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'"
|
|
898
1009
|
});
|
|
899
|
-
res.end(req.method === "HEAD" ? void 0 :
|
|
1010
|
+
res.end(req.method === "HEAD" ? void 0 : renderChatcodeLoginPage(logo));
|
|
900
1011
|
} catch {
|
|
901
1012
|
res.writeHead(500);
|
|
902
1013
|
res.end("login page unavailable");
|
|
@@ -45,6 +45,8 @@ export declare const zh: {
|
|
|
45
45
|
pluginMarketUninstalling: string;
|
|
46
46
|
accountMenuSettings: string;
|
|
47
47
|
accountMenuLogout: string;
|
|
48
|
+
accountMenuLogoutConfirmTitle: string;
|
|
49
|
+
accountMenuLogoutConfirmDescription: string;
|
|
48
50
|
accountMenuLabel: string;
|
|
49
51
|
updateMenu: string;
|
|
50
52
|
updateNav: string;
|
|
@@ -40,6 +40,6 @@ export declare function SidebarProjectLogo({ size }: {
|
|
|
40
40
|
}): ReactNode;
|
|
41
41
|
/** Expanded-sidebar wordmark supplied through the host's dedicated slot. */
|
|
42
42
|
export declare function SidebarProjectName(): ReactNode;
|
|
43
|
-
export declare const BRANDING_CSS = "\n/* ChatCode CLI branding is deliberately limited to primary actions. Every\n other surface and interaction state (including workspace/sidebar rows,\n menus, the composer, text and shadows) remains owned by the host theme. */\nbody:not([data-ds-dark-theme]) .dut-unicom-primary-action{background:linear-gradient(135deg,#9B0B09,#b71916) !important;border-color:#9B0B09 !important;color:#fff !important;box-shadow:0 7px 18px rgba(155,11,9,.24) !important}\nbody:not([data-ds-dark-theme]) .dut-unicom-primary-action:hover{background:linear-gradient(135deg,#b71916,#cf302c) !important;border-color:#b71916 !important;color:#fff !important;box-shadow:0 10px 24px rgba(155,11,9,.30) !important}\nbody:not([data-ds-dark-theme]) .dut-unicom-primary-action:active{transform:translateY(1px);box-shadow:0 4px 11px rgba(155,11,9,.22) !important}\n/* In dark mode, alter only the resting background of primary buttons. Once\n hovered, the host owns the complete interaction treatment again. */\nbody[data-ds-dark-theme] .dut-unicom-primary-action:not(:hover){background:#9B0B09 !important}\n.dut-project-logo{display:block;object-fit:contain}\n.dut-left-logo{width:
|
|
43
|
+
export declare const BRANDING_CSS = "\n/* ChatCode CLI branding is deliberately limited to primary actions. Every\n other surface and interaction state (including workspace/sidebar rows,\n menus, the composer, text and shadows) remains owned by the host theme. */\nbody:not([data-ds-dark-theme]) .dut-unicom-primary-action{background:linear-gradient(135deg,#9B0B09,#b71916) !important;border-color:#9B0B09 !important;color:#fff !important;box-shadow:0 7px 18px rgba(155,11,9,.24) !important}\nbody:not([data-ds-dark-theme]) .dut-unicom-primary-action:hover{background:linear-gradient(135deg,#b71916,#cf302c) !important;border-color:#b71916 !important;color:#fff !important;box-shadow:0 10px 24px rgba(155,11,9,.30) !important}\nbody:not([data-ds-dark-theme]) .dut-unicom-primary-action:active{transform:translateY(1px);box-shadow:0 4px 11px rgba(155,11,9,.22) !important}\n/* In dark mode, alter only the resting background of primary buttons. Once\n hovered, the host owns the complete interaction treatment again. */\nbody[data-ds-dark-theme] .dut-unicom-primary-action:not(:hover){background:#9B0B09 !important}\n.dut-project-logo{display:block;object-fit:contain}\n.dut-left-logo{width:24px !important;height:24px !important}\n.dut-brand-title{font:var(--dsw-font-s-strong-14) !important}\n.dut-sidebar-logo{width:24px !important;height:24px !important}\n.dut-sidebar-brand-name{font:var(--dsw-font-s-strong-14) !important;letter-spacing:-.01em !important;white-space:nowrap}\n\n.dut-sidebar-header{height:44px !important;padding:0 4px !important;margin-bottom:8px !important;gap:8px !important}\n.dut-sidebar-brand{min-width:0 !important;background:transparent !important;box-shadow:none !important}\n.dut-sidebar-identity{height:24px !important;gap:8px !important}\n.dut-sidebar-wordmark{font:var(--dsw-font-s-strong-14) !important;letter-spacing:-.01em !important;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.dut-sidebar-toggle{width:28px !important;height:28px !important;border-radius:8px !important;color:var(--dsw-alias-label-secondary) !important}\n.dut-sidebar-toggle:hover{background:var(--dsw-alias-interactive-bg-hover) !important}\n/* Reuse the original ChatCode primary-action red, scoped to this marked CTA. */\n.dut-sidebar-new-session{height:40px !important;justify-content:flex-start !important;gap:8px !important;margin:0 0 10px !important;padding:8px 10px !important;border:1px solid #9B0B09 !important;border-radius:10px !important;background:linear-gradient(135deg,#9B0B09,#b71916) !important;color:#fff !important;font:var(--dsw-font-s-strong-14) !important;font-weight:600 !important;box-shadow:none !important;transform:none !important}\n.dut-sidebar-new-session:hover{background:linear-gradient(135deg,#b71916,#cf302c) !important}\n.dut-sidebar-new-session:active{background:#9B0B09 !important}\n.dut-sidebar-new-session:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}\n.dut-sidebar-new-session > span:first-child{flex:1;min-width:0;overflow:hidden;mask-image:none !important}\n.dut-sidebar-new-session > span:first-child > span{width:auto !important;justify-content:flex-start !important;gap:8px !important;min-width:0}\n.dut-sidebar-new-session > span:first-child > span > svg{width:16px;height:16px;flex:none;color:inherit}\n.dut-sidebar-new-session > span:first-child > span > span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.dut-sidebar-new-session > span + span{opacity:1 !important;flex:none;color:color-mix(in srgb,currentColor 75%,transparent);font:var(--dsw-font-xxs-12)}\n/* Temporary navigation-only hide. Remove this block to restore Plugins;\n registrations, settings, installed plugins and other panel entries stay intact. */\nnav:is([aria-label=\"\u5168\u5C40\u9762\u677F\"],[aria-label=\"Global panels\"]) > button:is([aria-label=\"\u63D2\u4EF6\"],[aria-label=\"Plugins\"]),\nnav:is([aria-label=\"\u5168\u5C40\u9762\u677F\"],[aria-label=\"Global panels\"]):has(> button:is([aria-label=\"\u63D2\u4EF6\"],[aria-label=\"Plugins\"]):only-child){display:none !important}\n\n.dut-brand-version{display:none !important}\n.dut-hero-preview{display:none !important}\n.dut-hero-logo{width:60px !important;height:60px !important}\n.dut-hero-title{font-size:30px !important;line-height:40px !important;font-weight:600 !important;letter-spacing:.06em !important;white-space:nowrap}\n[class*=\"headline\"]:has(.dut-hero-logo){grid-template-columns:60px auto !important;font-size:30px !important;line-height:40px !important}\n.dut-settings-logo{width:19px;height:19px;object-fit:contain}\n";
|
|
44
44
|
/** Install the brand theme and retain it across host navigation/rerenders. */
|
|
45
45
|
export declare function installBranding(): () => void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Compact sidebar chrome; the host retains every button, shortcut and drag surface. */
|
|
2
|
+
export declare const SIDEBAR_HEADER_CSS = "\n.dut-sidebar-header{height:44px !important;padding:0 4px !important;margin-bottom:8px !important;gap:8px !important}\n.dut-sidebar-brand{min-width:0 !important;background:transparent !important;box-shadow:none !important}\n.dut-sidebar-identity{height:24px !important;gap:8px !important}\n.dut-sidebar-wordmark{font:var(--dsw-font-s-strong-14) !important;letter-spacing:-.01em !important;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.dut-sidebar-toggle{width:28px !important;height:28px !important;border-radius:8px !important;color:var(--dsw-alias-label-secondary) !important}\n.dut-sidebar-toggle:hover{background:var(--dsw-alias-interactive-bg-hover) !important}\n/* Reuse the original ChatCode primary-action red, scoped to this marked CTA. */\n.dut-sidebar-new-session{height:40px !important;justify-content:flex-start !important;gap:8px !important;margin:0 0 10px !important;padding:8px 10px !important;border:1px solid #9B0B09 !important;border-radius:10px !important;background:linear-gradient(135deg,#9B0B09,#b71916) !important;color:#fff !important;font:var(--dsw-font-s-strong-14) !important;font-weight:600 !important;box-shadow:none !important;transform:none !important}\n.dut-sidebar-new-session:hover{background:linear-gradient(135deg,#b71916,#cf302c) !important}\n.dut-sidebar-new-session:active{background:#9B0B09 !important}\n.dut-sidebar-new-session:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}\n.dut-sidebar-new-session > span:first-child{flex:1;min-width:0;overflow:hidden;mask-image:none !important}\n.dut-sidebar-new-session > span:first-child > span{width:auto !important;justify-content:flex-start !important;gap:8px !important;min-width:0}\n.dut-sidebar-new-session > span:first-child > span > svg{width:16px;height:16px;flex:none;color:inherit}\n.dut-sidebar-new-session > span:first-child > span > span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.dut-sidebar-new-session > span + span{opacity:1 !important;flex:none;color:color-mix(in srgb,currentColor 75%,transparent);font:var(--dsw-font-xxs-12)}\n/* Temporary navigation-only hide. Remove this block to restore Plugins;\n registrations, settings, installed plugins and other panel entries stay intact. */\nnav:is([aria-label=\"\u5168\u5C40\u9762\u677F\"],[aria-label=\"Global panels\"]) > button:is([aria-label=\"\u63D2\u4EF6\"],[aria-label=\"Plugins\"]),\nnav:is([aria-label=\"\u5168\u5C40\u9762\u677F\"],[aria-label=\"Global panels\"]):has(> button:is([aria-label=\"\u63D2\u4EF6\"],[aria-label=\"Plugins\"]):only-child){display:none !important}\n";
|
|
3
|
+
/** Mark only the expanded brand row and its adjacent native New Session button. */
|
|
4
|
+
export declare function syncSidebarHeader(): void;
|
|
5
|
+
/** Restore native header styling when the branding effect is disposed. */
|
|
6
|
+
export declare function clearSidebarHeader(): void;
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import type { Context, SidebarHttpRequest } from './context-types.ts';
|
|
2
|
+
/** Render the standalone login gate with one automatic attempt and persistent manual recovery. */
|
|
3
|
+
export declare function renderChatcodeLoginPage(logo: string): string;
|
|
2
4
|
export declare function registerLoginRoute(ctx: Context, fence: (req: SidebarHttpRequest) => boolean): () => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatcode/cco-better-sidebar",
|
|
3
|
-
"version": "0.18.0-rc.
|
|
3
|
+
"version": "0.18.0-rc.5",
|
|
4
4
|
"description": "ChatCode CLI Web plugin: a VS Code-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes a service for other plugins to register sidebar tabs and file viewers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -11,6 +11,7 @@ import { useEffect, useRef, useState } from 'react'
|
|
|
11
11
|
import { api } from './api.ts'
|
|
12
12
|
import { useChatcodeAccount } from './useChatcodeAccount.ts'
|
|
13
13
|
import { redirectToChatcodeLogin } from './chatcode-login-route.ts'
|
|
14
|
+
import { LogoutConfirmDialog } from './LogoutConfirmDialog.tsx'
|
|
14
15
|
import { t } from './locales.ts'
|
|
15
16
|
import css from './sidebar.module.css'
|
|
16
17
|
|
|
@@ -75,6 +76,7 @@ function logout(): void {
|
|
|
75
76
|
export function AccountMenu(): JSX.Element {
|
|
76
77
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
77
78
|
const [open, setOpen] = useState(false)
|
|
79
|
+
const [logoutTarget, setLogoutTarget] = useState<'chatcode' | 'browser'>()
|
|
78
80
|
const [userName, setUserName] = useState('User')
|
|
79
81
|
const { chatcode, busy, error, loginUrl, startLogin, logout: logoutChatcode } = useChatcodeAccount()
|
|
80
82
|
|
|
@@ -157,11 +159,11 @@ export function AccountMenu(): JSX.Element {
|
|
|
157
159
|
{canStartChatcodeLogin && <button type="button" role="menuitem" className={css.accountItem} disabled={busy} onClick={startLogin}>
|
|
158
160
|
<LogoutIcon />{busy ? t('accountMenuChatcodeStarting') : t('accountMenuChatcodeLogin')}
|
|
159
161
|
</button>}
|
|
160
|
-
{hasChatcodeAccount && <button type="button" role="menuitem" className={css.accountItem} disabled={busy} onClick={
|
|
162
|
+
{hasChatcodeAccount && <button type="button" role="menuitem" className={css.accountItem} disabled={busy} onClick={() => { setLogoutTarget('chatcode') }}>
|
|
161
163
|
<LogoutIcon />{t('accountMenuLogout')}
|
|
162
164
|
</button>}
|
|
163
165
|
</>}
|
|
164
|
-
{chatcode?.available === false && <button type="button" role="menuitem" className={css.accountItem} onClick={
|
|
166
|
+
{chatcode?.available === false && <button type="button" role="menuitem" className={css.accountItem} onClick={() => { setLogoutTarget('browser') }}>
|
|
165
167
|
<LogoutIcon />{t('accountMenuLogout')}
|
|
166
168
|
</button>}
|
|
167
169
|
</div>
|
|
@@ -178,6 +180,14 @@ export function AccountMenu(): JSX.Element {
|
|
|
178
180
|
<span className={css.accountName}>{chatcodeName}</span>
|
|
179
181
|
<span className={css.accountPresence} style={{ opacity: signedIn ? 1 : 0.25 }} aria-hidden="true" />
|
|
180
182
|
</button>
|
|
183
|
+
{logoutTarget !== undefined && <LogoutConfirmDialog
|
|
184
|
+
onCancel={() => { setLogoutTarget(undefined) }}
|
|
185
|
+
onConfirm={() => {
|
|
186
|
+
setLogoutTarget(undefined)
|
|
187
|
+
if (logoutTarget === 'chatcode') logoutChatcode()
|
|
188
|
+
else logout()
|
|
189
|
+
}}
|
|
190
|
+
/>}
|
|
181
191
|
</div>
|
|
182
192
|
)
|
|
183
193
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect, useId, useRef } from 'react'
|
|
2
|
+
import { t } from './locales.ts'
|
|
3
|
+
import css from './sidebar.module.css'
|
|
4
|
+
|
|
5
|
+
/** Native modal supplies an inert background and default Cancel focus until a choice is made. */
|
|
6
|
+
export function LogoutConfirmDialog({ onCancel, onConfirm }: {
|
|
7
|
+
onCancel: () => void
|
|
8
|
+
onConfirm: () => void
|
|
9
|
+
}): JSX.Element {
|
|
10
|
+
const ref = useRef<HTMLDialogElement>(null)
|
|
11
|
+
const titleId = useId()
|
|
12
|
+
const descriptionId = useId()
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const dialog = ref.current
|
|
16
|
+
dialog?.showModal()
|
|
17
|
+
return () => { if (dialog?.open) dialog.close() }
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<dialog
|
|
22
|
+
ref={ref}
|
|
23
|
+
className={css.accountLogoutDialog}
|
|
24
|
+
role="alertdialog"
|
|
25
|
+
aria-modal="true"
|
|
26
|
+
aria-labelledby={titleId}
|
|
27
|
+
aria-describedby={descriptionId}
|
|
28
|
+
onCancel={event => { event.preventDefault(); onCancel() }}
|
|
29
|
+
onKeyDown={event => { event.stopPropagation() }}
|
|
30
|
+
>
|
|
31
|
+
<h2 id={titleId} className={css.accountLogoutTitle}>{t('accountMenuLogoutConfirmTitle')}</h2>
|
|
32
|
+
<p id={descriptionId} className={css.accountLogoutDescription}>{t('accountMenuLogoutConfirmDescription')}</p>
|
|
33
|
+
<div className={css.accountLogoutActions}>
|
|
34
|
+
<button type="button" autoFocus onClick={onCancel}>{t('cancel')}</button>
|
|
35
|
+
<button type="button" className={css.accountLogoutConfirm} onClick={onConfirm}>{t('accountMenuLogout')}</button>
|
|
36
|
+
</div>
|
|
37
|
+
</dialog>
|
|
38
|
+
)
|
|
39
|
+
}
|
package/src/client/locales-ja.ts
CHANGED
|
@@ -39,6 +39,8 @@ export const ja: Record<string, string> = {
|
|
|
39
39
|
pluginMarketUninstalling: 'アンインストール中…',
|
|
40
40
|
accountMenuSettings: '設定',
|
|
41
41
|
accountMenuLogout: 'ログアウト',
|
|
42
|
+
accountMenuLogoutConfirmTitle: 'ログアウトしますか?',
|
|
43
|
+
accountMenuLogoutConfirmDescription: 'ChatCode CLI からログアウトしますか?続行するには再度ログインする必要があります。',
|
|
42
44
|
accountMenuLabel: 'アカウントメニュー',
|
|
43
45
|
updateMenu: '更新を確認 / 今すぐ更新',
|
|
44
46
|
updateNav: '更新',
|
package/src/client/locales.ts
CHANGED
|
@@ -46,6 +46,8 @@ export const zh = {
|
|
|
46
46
|
pluginMarketUninstalling: '卸载中…',
|
|
47
47
|
accountMenuSettings: '设置',
|
|
48
48
|
accountMenuLogout: '退出登录',
|
|
49
|
+
accountMenuLogoutConfirmTitle: '退出登录?',
|
|
50
|
+
accountMenuLogoutConfirmDescription: '确定要退出 ChatCode CLI 吗?退出后需要重新登录才能继续使用。',
|
|
49
51
|
accountMenuLabel: '账户菜单',
|
|
50
52
|
updateMenu: '检查更新 / 一键更新',
|
|
51
53
|
updateNav: '更新',
|
|
@@ -538,6 +540,8 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
538
540
|
pluginMarketUninstalling: 'Uninstalling…',
|
|
539
541
|
accountMenuSettings: 'Settings',
|
|
540
542
|
accountMenuLogout: 'Log out',
|
|
543
|
+
accountMenuLogoutConfirmTitle: 'Log out?',
|
|
544
|
+
accountMenuLogoutConfirmDescription: 'Are you sure you want to log out of ChatCode CLI? You will need to sign in again to continue.',
|
|
541
545
|
accountMenuLabel: 'Account menu',
|
|
542
546
|
updateMenu: 'Check for updates / Update now',
|
|
543
547
|
updateNav: 'Update',
|
|
@@ -1037,6 +1041,8 @@ import { zhMO as zhMODict } from './locales-zh-MO.ts'
|
|
|
1037
1041
|
// English fallback for the new account/model surfaces until third-language translations are supplied.
|
|
1038
1042
|
// Keep this list explicit so the dictionary parity check still catches other missing keys.
|
|
1039
1043
|
const accountAndModelFallback = {
|
|
1044
|
+
accountMenuLogoutConfirmTitle: en.accountMenuLogoutConfirmTitle,
|
|
1045
|
+
accountMenuLogoutConfirmDescription: en.accountMenuLogoutConfirmDescription,
|
|
1040
1046
|
chatcodeGateChecking: en.chatcodeGateChecking,
|
|
1041
1047
|
chatcodeGateUnverified: en.chatcodeGateUnverified,
|
|
1042
1048
|
chatcodeGatePending: en.chatcodeGatePending,
|
|
@@ -442,13 +442,11 @@
|
|
|
442
442
|
box-sizing: border-box;
|
|
443
443
|
gap: 8px;
|
|
444
444
|
width: 100%;
|
|
445
|
-
min-height:
|
|
446
|
-
padding:
|
|
445
|
+
min-height: 44px;
|
|
446
|
+
padding: 8px 12px max(8px, env(safe-area-inset-bottom));
|
|
447
447
|
border-radius: 0;
|
|
448
448
|
background: var(--dsw-alias-bg-layer-1);
|
|
449
|
-
font
|
|
450
|
-
font-size: var(--dsw-font-size-body-sm);
|
|
451
|
-
line-height: var(--dsw-font-line-height-body-sm);
|
|
449
|
+
font: var(--dsw-font-xs-13);
|
|
452
450
|
transition: background var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
|
453
451
|
}
|
|
454
452
|
|
|
@@ -466,18 +464,21 @@
|
|
|
466
464
|
.accountAvatar {
|
|
467
465
|
position: relative;
|
|
468
466
|
display: grid;
|
|
469
|
-
|
|
470
|
-
|
|
467
|
+
box-sizing: border-box;
|
|
468
|
+
width: 24px;
|
|
469
|
+
height: 24px;
|
|
471
470
|
flex: none;
|
|
472
471
|
place-items: center;
|
|
473
472
|
border-radius: 50%;
|
|
474
473
|
background: var(--dsw-alias-bg-layer-3);
|
|
475
474
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
476
475
|
color: var(--dsw-alias-fg-accent);
|
|
476
|
+
font: var(--dsw-font-xxs-12);
|
|
477
477
|
font-weight: 600;
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
.accountName {
|
|
481
|
+
min-width: 0;
|
|
481
482
|
overflow: hidden;
|
|
482
483
|
text-overflow: ellipsis;
|
|
483
484
|
white-space: nowrap;
|
|
@@ -490,10 +491,10 @@
|
|
|
490
491
|
left: 8px;
|
|
491
492
|
display: grid;
|
|
492
493
|
box-sizing: border-box;
|
|
493
|
-
width: min(
|
|
494
|
-
padding:
|
|
494
|
+
width: min(224px, calc(100% - 16px));
|
|
495
|
+
padding: 8px 6px 6px;
|
|
495
496
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
496
|
-
border-radius:
|
|
497
|
+
border-radius: 10px;
|
|
497
498
|
background: var(--dsw-alias-bg-layer-2);
|
|
498
499
|
z-index: 1;
|
|
499
500
|
}
|
|
@@ -502,16 +503,15 @@
|
|
|
502
503
|
display: flex;
|
|
503
504
|
align-items: center;
|
|
504
505
|
gap: 8px;
|
|
505
|
-
min-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
font
|
|
509
|
-
line-height: var(--dsw-font-line-height-body-sm);
|
|
506
|
+
min-width: 0;
|
|
507
|
+
min-height: 28px;
|
|
508
|
+
padding: 0 6px 6px;
|
|
509
|
+
font: var(--dsw-font-xs-13);
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
.accountMenuDivider {
|
|
513
513
|
height: 1px;
|
|
514
|
-
margin: 0 4px
|
|
514
|
+
margin: 0 4px 4px;
|
|
515
515
|
background: var(--dsw-alias-border-l2);
|
|
516
516
|
}
|
|
517
517
|
|
|
@@ -519,12 +519,12 @@
|
|
|
519
519
|
display: flex;
|
|
520
520
|
align-items: center;
|
|
521
521
|
gap: 8px;
|
|
522
|
-
min-height:
|
|
523
|
-
padding: 0
|
|
522
|
+
min-height: 30px;
|
|
523
|
+
padding: 0 8px;
|
|
524
524
|
border-radius: 7px;
|
|
525
525
|
background: transparent;
|
|
526
526
|
text-align: left;
|
|
527
|
-
font: var(--dsw-font-
|
|
527
|
+
font: var(--dsw-font-xs-13);
|
|
528
528
|
}
|
|
529
529
|
|
|
530
530
|
.accountItem:hover {
|
|
@@ -546,6 +546,68 @@
|
|
|
546
546
|
border-color: var(--dsw-alias-bg-layer-1);
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
+
/* The native top layer is viewport-centered even though the account rail is positioned. */
|
|
550
|
+
.accountLogoutDialog {
|
|
551
|
+
box-sizing: border-box;
|
|
552
|
+
width: min(400px, calc(100vw - 32px));
|
|
553
|
+
max-width: calc(100vw - 32px);
|
|
554
|
+
max-height: calc(100dvh - 32px);
|
|
555
|
+
margin: auto;
|
|
556
|
+
padding: 24px;
|
|
557
|
+
overflow: auto;
|
|
558
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
559
|
+
border-radius: 16px;
|
|
560
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
561
|
+
color: var(--dsw-alias-fg-base);
|
|
562
|
+
font: var(--dsw-font-s-14);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.accountLogoutDialog::backdrop {
|
|
566
|
+
background: var(--dsw-alias-bg-mask-1);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.accountLogoutTitle {
|
|
570
|
+
margin: 0;
|
|
571
|
+
font: var(--dsw-font-base-strong-16);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.accountLogoutDescription {
|
|
575
|
+
margin: 12px 0 24px;
|
|
576
|
+
color: var(--dsw-alias-label-secondary);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.accountLogoutActions {
|
|
580
|
+
display: flex;
|
|
581
|
+
flex-wrap: wrap;
|
|
582
|
+
justify-content: flex-end;
|
|
583
|
+
gap: 8px;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.accountLogoutActions button {
|
|
587
|
+
min-height: 34px;
|
|
588
|
+
padding: 6px 14px;
|
|
589
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
590
|
+
border-radius: 8px;
|
|
591
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
592
|
+
color: var(--dsw-alias-fg-base);
|
|
593
|
+
font: var(--dsw-font-xs-13);
|
|
594
|
+
cursor: pointer;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.accountLogoutActions button:hover {
|
|
598
|
+
background: var(--dsw-alias-bg-hover);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.accountLogoutActions button:focus-visible {
|
|
602
|
+
outline: 2px solid var(--dsw-alias-border-accent);
|
|
603
|
+
outline-offset: 2px;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.accountLogoutActions .accountLogoutConfirm {
|
|
607
|
+
border-color: var(--dsw-alias-state-error-primary);
|
|
608
|
+
color: var(--dsw-alias-state-error-primary);
|
|
609
|
+
}
|
|
610
|
+
|
|
549
611
|
/* The compatibility action cluster used when the host exposes no native
|
|
550
612
|
right-sidebar affordance. It stays pinned to the viewport corner, sitting
|
|
551
613
|
flush in the 34px tab strip (28px buttons at top: 3) whose right end it
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ReactNode } from 'react'
|
|
11
|
+
import { clearSidebarHeader, SIDEBAR_HEADER_CSS, syncSidebarHeader } from './sidebar-header.ts'
|
|
11
12
|
|
|
12
13
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
13
14
|
interface SlotMap {
|
|
@@ -89,14 +90,11 @@ body:not([data-ds-dark-theme]) .dut-unicom-primary-action:active{transform:trans
|
|
|
89
90
|
hovered, the host owns the complete interaction treatment again. */
|
|
90
91
|
body[data-ds-dark-theme] .dut-unicom-primary-action:not(:hover){background:#9B0B09 !important}
|
|
91
92
|
.dut-project-logo{display:block;object-fit:contain}
|
|
92
|
-
.dut-left-logo{width:
|
|
93
|
-
.dut-brand-title{font-
|
|
94
|
-
.dut-sidebar-logo{width:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
44px knot is not cropped by that host-owned overflow boundary. */
|
|
98
|
-
[class*="brandIdentity"]:has(.dut-sidebar-logo){height:44px !important}
|
|
99
|
-
.dut-sidebar-brand-name{font-size:18px !important;line-height:22px !important;font-weight:600 !important;letter-spacing:0 !important;white-space:nowrap}
|
|
93
|
+
.dut-left-logo{width:24px !important;height:24px !important}
|
|
94
|
+
.dut-brand-title{font:var(--dsw-font-s-strong-14) !important}
|
|
95
|
+
.dut-sidebar-logo{width:24px !important;height:24px !important}
|
|
96
|
+
.dut-sidebar-brand-name{font:var(--dsw-font-s-strong-14) !important;letter-spacing:-.01em !important;white-space:nowrap}
|
|
97
|
+
${SIDEBAR_HEADER_CSS}
|
|
100
98
|
.dut-brand-version{display:none !important}
|
|
101
99
|
.dut-hero-preview{display:none !important}
|
|
102
100
|
.dut-hero-logo{width:60px !important;height:60px !important}
|
|
@@ -110,7 +108,8 @@ function hasPrimaryLabel(button: HTMLButtonElement): boolean {
|
|
|
110
108
|
// descriptions, where words like "install" do not identify a primary action.
|
|
111
109
|
if (button.closest('[data-chatcode-plugin-market], [data-plugin-module]')) return false
|
|
112
110
|
const label = `${button.textContent ?? ''} ${button.getAttribute('aria-label') ?? ''} ${button.title}`.trim().toLowerCase()
|
|
113
|
-
|
|
111
|
+
if (button.classList.contains('dut-sidebar-brand') || button.classList.contains('dut-sidebar-new-session')) return false
|
|
112
|
+
return /(?:^|\s)(send|install)(?:\s|$)|发送|(?:^|\s)安装(?:\s|$)/u.test(label)
|
|
114
113
|
}
|
|
115
114
|
|
|
116
115
|
/** Replace one host SVG while retaining its generated layout class names. */
|
|
@@ -241,6 +240,7 @@ function syncBranding(): void {
|
|
|
241
240
|
if (svg instanceof SVGElement) replaceHostLogo(svg)
|
|
242
241
|
})
|
|
243
242
|
|
|
243
|
+
syncSidebarHeader()
|
|
244
244
|
document.querySelectorAll<HTMLButtonElement>('button').forEach((button) => {
|
|
245
245
|
button.classList.toggle('dut-unicom-primary-action', hasPrimaryLabel(button))
|
|
246
246
|
})
|
|
@@ -285,6 +285,7 @@ export function installBranding(): () => void {
|
|
|
285
285
|
return () => {
|
|
286
286
|
observer.disconnect()
|
|
287
287
|
if (frame !== undefined) window.cancelAnimationFrame(frame)
|
|
288
|
+
clearSidebarHeader()
|
|
288
289
|
favicon.remove()
|
|
289
290
|
style.remove()
|
|
290
291
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Compact sidebar chrome; the host retains every button, shortcut and drag surface. */
|
|
2
|
+
export const SIDEBAR_HEADER_CSS = `
|
|
3
|
+
.dut-sidebar-header{height:44px !important;padding:0 4px !important;margin-bottom:8px !important;gap:8px !important}
|
|
4
|
+
.dut-sidebar-brand{min-width:0 !important;background:transparent !important;box-shadow:none !important}
|
|
5
|
+
.dut-sidebar-identity{height:24px !important;gap:8px !important}
|
|
6
|
+
.dut-sidebar-wordmark{font:var(--dsw-font-s-strong-14) !important;letter-spacing:-.01em !important;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
7
|
+
.dut-sidebar-toggle{width:28px !important;height:28px !important;border-radius:8px !important;color:var(--dsw-alias-label-secondary) !important}
|
|
8
|
+
.dut-sidebar-toggle:hover{background:var(--dsw-alias-interactive-bg-hover) !important}
|
|
9
|
+
/* Reuse the original ChatCode primary-action red, scoped to this marked CTA. */
|
|
10
|
+
.dut-sidebar-new-session{height:40px !important;justify-content:flex-start !important;gap:8px !important;margin:0 0 10px !important;padding:8px 10px !important;border:1px solid #9B0B09 !important;border-radius:10px !important;background:linear-gradient(135deg,#9B0B09,#b71916) !important;color:#fff !important;font:var(--dsw-font-s-strong-14) !important;font-weight:600 !important;box-shadow:none !important;transform:none !important}
|
|
11
|
+
.dut-sidebar-new-session:hover{background:linear-gradient(135deg,#b71916,#cf302c) !important}
|
|
12
|
+
.dut-sidebar-new-session:active{background:#9B0B09 !important}
|
|
13
|
+
.dut-sidebar-new-session:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
14
|
+
.dut-sidebar-new-session > span:first-child{flex:1;min-width:0;overflow:hidden;mask-image:none !important}
|
|
15
|
+
.dut-sidebar-new-session > span:first-child > span{width:auto !important;justify-content:flex-start !important;gap:8px !important;min-width:0}
|
|
16
|
+
.dut-sidebar-new-session > span:first-child > span > svg{width:16px;height:16px;flex:none;color:inherit}
|
|
17
|
+
.dut-sidebar-new-session > span:first-child > span > span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
18
|
+
.dut-sidebar-new-session > span + span{opacity:1 !important;flex:none;color:color-mix(in srgb,currentColor 75%,transparent);font:var(--dsw-font-xxs-12)}
|
|
19
|
+
/* Temporary navigation-only hide. Remove this block to restore Plugins;
|
|
20
|
+
registrations, settings, installed plugins and other panel entries stay intact. */
|
|
21
|
+
nav:is([aria-label="全局面板"],[aria-label="Global panels"]) > button:is([aria-label="插件"],[aria-label="Plugins"]),
|
|
22
|
+
nav:is([aria-label="全局面板"],[aria-label="Global panels"]):has(> button:is([aria-label="插件"],[aria-label="Plugins"]):only-child){display:none !important}
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
const HEADER_CLASSES = [
|
|
26
|
+
'dut-sidebar-header', 'dut-sidebar-brand', 'dut-sidebar-identity',
|
|
27
|
+
'dut-sidebar-wordmark', 'dut-sidebar-toggle', 'dut-sidebar-new-session',
|
|
28
|
+
] as const
|
|
29
|
+
type HeaderClass = typeof HEADER_CLASSES[number]
|
|
30
|
+
|
|
31
|
+
/** Mark only the expanded brand row and its adjacent native New Session button. */
|
|
32
|
+
export function syncSidebarHeader(): void {
|
|
33
|
+
const desired = new Map<HTMLElement, Set<HeaderClass>>()
|
|
34
|
+
const mark = (element: Element | null | undefined, className: HeaderClass): void => {
|
|
35
|
+
if (!(element instanceof HTMLElement)) return
|
|
36
|
+
const classes = desired.get(element) ?? new Set<HeaderClass>()
|
|
37
|
+
classes.add(className)
|
|
38
|
+
desired.set(element, classes)
|
|
39
|
+
}
|
|
40
|
+
for (const name of document.querySelectorAll<HTMLElement>('.dut-sidebar-brand-name,.dut-brand-title')) {
|
|
41
|
+
const row = name.closest<HTMLElement>('[data-window-drag]')
|
|
42
|
+
const logo = row?.querySelector('.dut-sidebar-logo,.dut-left-logo')
|
|
43
|
+
if (!row || !logo) continue
|
|
44
|
+
const brand = [...row.children].find(child => child.contains(name) && child.contains(logo))
|
|
45
|
+
if (!brand) continue
|
|
46
|
+
mark(row, 'dut-sidebar-header')
|
|
47
|
+
mark(brand, 'dut-sidebar-brand')
|
|
48
|
+
mark(logo.closest('[class*="brandIdentity"]'), 'dut-sidebar-identity')
|
|
49
|
+
mark(name, 'dut-sidebar-wordmark')
|
|
50
|
+
for (const button of row.querySelectorAll('button')) {
|
|
51
|
+
if (!brand.contains(button) && button !== brand) mark(button, 'dut-sidebar-toggle')
|
|
52
|
+
}
|
|
53
|
+
// The host renders this native action directly after the drag row. Do
|
|
54
|
+
// not infer a new action from arbitrary text in another panel or dialog.
|
|
55
|
+
const next = row.nextElementSibling
|
|
56
|
+
if (next instanceof HTMLButtonElement) mark(next, 'dut-sidebar-new-session')
|
|
57
|
+
}
|
|
58
|
+
// A collapsed rail no longer has a wordmark. Remove expanded-only styling
|
|
59
|
+
// without changing the host's rail geometry or its logo/expand interaction.
|
|
60
|
+
for (const className of HEADER_CLASSES) {
|
|
61
|
+
for (const element of document.querySelectorAll<HTMLElement>(`.${className}`)) {
|
|
62
|
+
if (!desired.get(element)?.has(className)) element.classList.remove(className)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const [element, classes] of desired) element.classList.add(...classes)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Restore native header styling when the branding effect is disposed. */
|
|
69
|
+
export function clearSidebarHeader(): void {
|
|
70
|
+
for (const className of HEADER_CLASSES) {
|
|
71
|
+
for (const element of document.querySelectorAll(`.${className}`)) element.classList.remove(className)
|
|
72
|
+
}
|
|
73
|
+
}
|