@chatcode/cco-better-sidebar 0.18.0-rc.3 → 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 +19 -1
- package/README_EN.md +19 -1
- package/lib/client-editor.js +326 -258
- package/lib/client-mermaid.js +312 -259
- package/lib/client-registry.js +1392 -605
- package/lib/client-terminal.js +328 -260
- package/lib/client.js +1352 -565
- package/lib/index.js +162 -5
- package/lib/types/client/LogoutConfirmDialog.d.ts +5 -0
- package/lib/types/client/PluginMarketSettingsSection.d.ts +7 -0
- package/lib/types/client/PluginMarketView.d.ts +7 -0
- package/lib/types/client/api.d.ts +15 -0
- package/lib/types/client/builtins/tabs.d.ts +6 -3
- package/lib/types/client/locales.d.ts +23 -0
- package/lib/types/client/plugin-market-catalog.d.ts +25 -0
- package/lib/types/client/ui-tweaks/branding.d.ts +1 -1
- package/lib/types/client/ui-tweaks/index.d.ts +1 -1
- package/lib/types/client/ui-tweaks/sidebar-header.d.ts +6 -0
- package/lib/types/client/ui-tweaks/typography.d.ts +9 -0
- package/lib/types/login-page-script.d.ts +2 -0
- package/lib/types/login-route.d.ts +2 -0
- package/lib/types/plugin-market-api.d.ts +5 -0
- package/lib/types/plugin-market-shared.d.ts +37 -0
- package/lib/types/ui-tweaks/config.d.ts +5 -6
- package/package.json +1 -1
- package/src/client/AccountMenu.tsx +12 -2
- package/src/client/LogoutConfirmDialog.tsx +39 -0
- package/src/client/PluginMarketSettingsSection.tsx +55 -0
- package/src/client/PluginMarketView.module.css +35 -0
- package/src/client/PluginMarketView.tsx +154 -0
- package/src/client/api.ts +7 -0
- package/src/client/builtins/tabs.tsx +16 -3
- package/src/client/index.tsx +10 -1
- package/src/client/locales-ja.ts +23 -0
- package/src/client/locales.ts +48 -0
- package/src/client/plugin-market-catalog.ts +90 -0
- package/src/client/sidebar.module.css +81 -19
- package/src/client/ui-tweaks/branding.tsx +25 -16
- package/src/client/ui-tweaks/index.tsx +36 -99
- package/src/client/ui-tweaks/sidebar-header.ts +73 -0
- package/src/client/ui-tweaks/typography.ts +38 -0
- package/src/index.ts +2 -0
- package/src/login-page-script.ts +107 -0
- package/src/login-route.ts +7 -4
- package/src/plugin-market-api.ts +42 -0
- package/src/plugin-market-shared.ts +21 -0
- package/src/ui-tweaks/config.ts +10 -10
package/lib/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import { YAMLMap, YAMLSeq, parse, parseDocument, stringify } from "yaml";
|
|
|
26
26
|
const UI_TWEAKS_SETTINGS_NAMESPACE = "better-sidebar";
|
|
27
27
|
/** Configuration schema with documented defaults. */
|
|
28
28
|
const Config$1 = z$1.object({
|
|
29
|
-
fontSize: z$1.number().min(10).max(32).default(
|
|
29
|
+
fontSize: z$1.number().min(10).max(32).default(14),
|
|
30
30
|
codeFontScale: z$1.number().min(50).max(150).default(81),
|
|
31
31
|
codeFontSize: z$1.number().min(8).max(32),
|
|
32
32
|
lineHeight: z$1.number().min(0).max(64).default(16),
|
|
@@ -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");
|
|
@@ -5882,6 +5993,51 @@ function apply$1(ctx) {
|
|
|
5882
5993
|
ctx.logger.info("[ChatCode CLI UI Tweaks] settings namespace registered and Web routes mounted");
|
|
5883
5994
|
}
|
|
5884
5995
|
//#endregion
|
|
5996
|
+
//#region src/plugin-market-api.ts
|
|
5997
|
+
function buildPluginMarketApi(ctx, cwdOf) {
|
|
5998
|
+
const service = () => {
|
|
5999
|
+
const value = ctx.get("chatcodePluginMarket", false);
|
|
6000
|
+
if (!value) throw new SidebarError("chatcode-unavailable", "插件市场服务未挂载,请更新并启用 ChatCode config 插件。", 503);
|
|
6001
|
+
return value;
|
|
6002
|
+
};
|
|
6003
|
+
const scopeOf = (payload) => {
|
|
6004
|
+
const scope = requireString(payload, "scope");
|
|
6005
|
+
if (scope !== "project" && scope !== "user") throw new SidebarError("bad-request", "scope must be project or user");
|
|
6006
|
+
return scope;
|
|
6007
|
+
};
|
|
6008
|
+
const contextOf = async (payload) => {
|
|
6009
|
+
const sessionId = payload?.sessionId;
|
|
6010
|
+
if (sessionId === void 0 || sessionId === "") return {
|
|
6011
|
+
sessionId: "",
|
|
6012
|
+
cwd: process.cwd()
|
|
6013
|
+
};
|
|
6014
|
+
requireString(payload, "sessionId");
|
|
6015
|
+
return {
|
|
6016
|
+
sessionId,
|
|
6017
|
+
cwd: (await cwdOf(payload)).cwd
|
|
6018
|
+
};
|
|
6019
|
+
};
|
|
6020
|
+
return {
|
|
6021
|
+
"plugins.context": contextOf,
|
|
6022
|
+
"plugins.marketplaces": () => service().marketplaces(),
|
|
6023
|
+
"plugins.catalog": async (payload) => {
|
|
6024
|
+
const market = service();
|
|
6025
|
+
const id = requireString(payload, "marketplaceId");
|
|
6026
|
+
if (payload.includeMetadata !== true) return market.plugins(id);
|
|
6027
|
+
return market.catalog ? market.catalog(id) : {
|
|
6028
|
+
plugins: await market.plugins(id),
|
|
6029
|
+
warning: ""
|
|
6030
|
+
};
|
|
6031
|
+
},
|
|
6032
|
+
"plugins.installed": async (payload) => service().installed((await contextOf(payload)).cwd),
|
|
6033
|
+
"plugins.install": async (payload) => service().install((await contextOf(payload)).cwd, requireString(payload, "pluginId"), scopeOf(payload)),
|
|
6034
|
+
"plugins.uninstall": async (payload) => {
|
|
6035
|
+
await service().uninstall((await contextOf(payload)).cwd, requireString(payload, "pluginId"), scopeOf(payload));
|
|
6036
|
+
return { ok: true };
|
|
6037
|
+
}
|
|
6038
|
+
};
|
|
6039
|
+
}
|
|
6040
|
+
//#endregion
|
|
5885
6041
|
//#region src/index.ts
|
|
5886
6042
|
/**
|
|
5887
6043
|
* dsh-better-sidebar host half: the /sidebar JSON API (explorer listing, file
|
|
@@ -6197,6 +6353,7 @@ function buildApi(ctx, ptyManager, agentPtyRegistry, resolved, terminalShell, ge
|
|
|
6197
6353
|
parent: parentOf(cwd) ?? null
|
|
6198
6354
|
};
|
|
6199
6355
|
},
|
|
6356
|
+
...buildPluginMarketApi(ctx, cwdOf),
|
|
6200
6357
|
"fs.tree": async (payload) => {
|
|
6201
6358
|
const { cwd } = await cwdOf(payload);
|
|
6202
6359
|
return listDirectory(payload.path === void 0 ? cwd : await ensureWorkspacePath(cwd, requireString(payload, "path"), fenceEnabledOf(getSettings)), resolved.listLimit);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Context } from '../context-types.ts';
|
|
2
|
+
import type { PluginMarketCatalog } from './plugin-market-catalog.ts';
|
|
3
|
+
export declare function PluginMarketSettingsSection({ ctx, catalog }: {
|
|
4
|
+
ctx: Context;
|
|
5
|
+
catalog: PluginMarketCatalog;
|
|
6
|
+
}): JSX.Element;
|
|
7
|
+
export declare function registerPluginMarketSettings(ctx: Context, catalog: PluginMarketCatalog): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SessionScope } from './api.ts';
|
|
2
|
+
import type { PluginMarketCatalog } from './plugin-market-catalog.ts';
|
|
3
|
+
export declare function PluginMarketView({ scope, visible, catalog }: {
|
|
4
|
+
scope: SessionScope;
|
|
5
|
+
visible: boolean;
|
|
6
|
+
catalog: PluginMarketCatalog;
|
|
7
|
+
}): JSX.Element;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BrowserProbeResult } from './browser.ts';
|
|
2
|
+
import type { MarketplaceCatalog, MarketplacePlugin, InstalledMarketplacePlugin, PluginMarketScope } from '../plugin-market-shared.ts';
|
|
2
3
|
/** One wire failure. */
|
|
3
4
|
export declare class SidebarApiError extends Error {
|
|
4
5
|
readonly code: string;
|
|
@@ -184,6 +185,20 @@ export interface SessionScope {
|
|
|
184
185
|
}
|
|
185
186
|
/** The sidebar API surface (session scope threaded through every call). */
|
|
186
187
|
export declare const api: {
|
|
188
|
+
pluginMarketContext: (scope: SessionScope, signal?: AbortSignal) => Promise<{
|
|
189
|
+
sessionId: string;
|
|
190
|
+
cwd: string;
|
|
191
|
+
}>;
|
|
192
|
+
pluginMarketplaces: (signal?: AbortSignal) => Promise<MarketplaceCatalog>;
|
|
193
|
+
pluginCatalog: (marketplaceId: string, signal?: AbortSignal) => Promise<MarketplacePlugin[] | {
|
|
194
|
+
plugins: MarketplacePlugin[];
|
|
195
|
+
warning: string;
|
|
196
|
+
}>;
|
|
197
|
+
pluginsInstalled: (scope: SessionScope, signal?: AbortSignal) => Promise<InstalledMarketplacePlugin[]>;
|
|
198
|
+
pluginInstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope) => Promise<InstalledMarketplacePlugin>;
|
|
199
|
+
pluginUninstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope) => Promise<{
|
|
200
|
+
ok: true;
|
|
201
|
+
}>;
|
|
187
202
|
chatcodeStatus: (signal?: AbortSignal) => Promise<ChatCodeAuthState>;
|
|
188
203
|
chatcodeStart: () => Promise<{
|
|
189
204
|
sessionId: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
* (editor / git / terminal / browser / diff) through
|
|
2
|
+
* The 6 built-in tab descriptors: the plugin registers its own pages
|
|
3
|
+
* (editor / git / terminal / browser / diff / plugin-market) through
|
|
4
4
|
* the same {@link BetterSidebarService} external plugins use — eating its
|
|
5
5
|
* own dogfood. The terminal descriptor owns its quota (`TERMINAL_LIMIT`)
|
|
6
6
|
* and mints `terminal:<uuid>` ids through `createTab`; the browser mints
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* (the old standalone explorer merged into it).
|
|
9
9
|
*/
|
|
10
10
|
import type { Context } from '../../context-types.ts';
|
|
11
|
+
import { type PluginMarketCatalog } from '../plugin-market-catalog.ts';
|
|
11
12
|
import type { TabDescriptor } from '../service.ts';
|
|
12
13
|
/** How many UI-owned terminals may be open at once (agent-owned ones are uncapped). */
|
|
13
14
|
export declare const TERMINAL_LIMIT = 3;
|
|
@@ -15,6 +16,8 @@ export declare const TERMINAL_LIMIT = 3;
|
|
|
15
16
|
export interface BuiltinTabOptions {
|
|
16
17
|
/** Returns the display title for newly opened terminal tabs. */
|
|
17
18
|
terminalTitle?: () => string;
|
|
19
|
+
/** Shared app-lifetime catalog; production supplies the activation's store. */
|
|
20
|
+
pluginMarketCatalog?: PluginMarketCatalog;
|
|
18
21
|
}
|
|
19
|
-
/** The
|
|
22
|
+
/** The 6 built-in tab descriptors. */
|
|
20
23
|
export declare function builtinTabs(ctx: Context, options?: BuiltinTabOptions): readonly TabDescriptor[];
|
|
@@ -22,8 +22,31 @@
|
|
|
22
22
|
*/
|
|
23
23
|
/** The zh dictionary (also registered into the ChatCode CLI locale registry under {@link LOCALE_NS}). */
|
|
24
24
|
export declare const zh: {
|
|
25
|
+
pluginMarketTitle: string;
|
|
26
|
+
pluginMarketProjectDirectory: string;
|
|
27
|
+
pluginMarketInstalledTab: string;
|
|
28
|
+
pluginMarketRefresh: string;
|
|
29
|
+
pluginMarketLoading: string;
|
|
30
|
+
pluginMarketRefreshing: string;
|
|
31
|
+
pluginMarketEmpty: string;
|
|
32
|
+
pluginMarketBack: string;
|
|
33
|
+
pluginMarketInstall: string;
|
|
34
|
+
pluginMarketUninstall: string;
|
|
35
|
+
pluginMarketProject: string;
|
|
36
|
+
pluginMarketUser: string;
|
|
37
|
+
pluginMarketInstalled: string;
|
|
38
|
+
pluginMarketUninstalled: string;
|
|
39
|
+
pluginMarketSubtitle: string;
|
|
40
|
+
pluginMarketSearch: string;
|
|
41
|
+
pluginMarketDefault: string;
|
|
42
|
+
pluginMarketBrowse: string;
|
|
43
|
+
pluginMarketNoDescription: string;
|
|
44
|
+
pluginMarketInstalling: string;
|
|
45
|
+
pluginMarketUninstalling: string;
|
|
25
46
|
accountMenuSettings: string;
|
|
26
47
|
accountMenuLogout: string;
|
|
48
|
+
accountMenuLogoutConfirmTitle: string;
|
|
49
|
+
accountMenuLogoutConfirmDescription: string;
|
|
27
50
|
accountMenuLabel: string;
|
|
28
51
|
updateMenu: string;
|
|
29
52
|
updateNav: string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MarketplaceCatalog, MarketplacePlugin } from '../plugin-market-shared.ts';
|
|
2
|
+
interface Entry<T> {
|
|
3
|
+
readonly status: 'loading' | 'ready' | 'error';
|
|
4
|
+
readonly data?: T;
|
|
5
|
+
readonly error?: string;
|
|
6
|
+
readonly warning?: string;
|
|
7
|
+
}
|
|
8
|
+
interface Snapshot {
|
|
9
|
+
readonly markets?: Entry<MarketplaceCatalog>;
|
|
10
|
+
readonly plugins: Readonly<Record<string, Entry<MarketplacePlugin[]>>>;
|
|
11
|
+
readonly refresh?: {
|
|
12
|
+
readonly status: 'loading' | 'ready' | 'error';
|
|
13
|
+
readonly error?: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface PluginMarketCatalog {
|
|
17
|
+
subscribe(listener: () => void): () => void;
|
|
18
|
+
getSnapshot(): Snapshot;
|
|
19
|
+
start(): void;
|
|
20
|
+
ensure(marketplaceId: string): void;
|
|
21
|
+
refresh(marketplaceId?: string): void;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
}
|
|
24
|
+
export declare function createPluginMarketCatalog(): PluginMarketCatalog;
|
|
25
|
+
export {};
|
|
@@ -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;
|
|
@@ -58,7 +58,7 @@ declare const en: {
|
|
|
58
58
|
readonly fontSize: "Message font size";
|
|
59
59
|
readonly fontSizeHint: "Number between 10 and 32; applies to message text, headings, tables and code.";
|
|
60
60
|
readonly codeFontSize: "Code font size";
|
|
61
|
-
readonly codeFontSizeHint: "Absolute code size in px (8–32); 13px is ChatCode CLI's default at a
|
|
61
|
+
readonly codeFontSizeHint: "Absolute code size in px (8–32); 13px is ChatCode CLI's default at a 14px body. Applies to code blocks; inline code follows proportionally.";
|
|
62
62
|
readonly lineHeight: "Line spacing";
|
|
63
63
|
readonly lineHeightHint: "Base vertical spacing in px: gaps between message rows (Think ↔ tool cards) and blocks inside one reply, plus text line-height, paragraph and list margins, all scale from it; 16 is ChatCode CLI's default.";
|
|
64
64
|
readonly tableStyle: "Table style";
|
|
@@ -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;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Font overrides for the conversation, preserving the live theme families. */
|
|
2
|
+
/**
|
|
3
|
+
* Apply the UI Tweaks size and spacing preferences to the host's prose tokens.
|
|
4
|
+
* @param fontSize - Selected body size in pixels.
|
|
5
|
+
* @param lineHeight - Vertical-spacing unit; 16 retains the normal rhythm.
|
|
6
|
+
* @param codeFontSize - Selected code-block size in pixels.
|
|
7
|
+
* @returns Theme-token overrides for a runtime stylesheet.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildFontCss(fontSize: number, lineHeight: number, codeFontSize: number): string;
|
|
@@ -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;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Shared config plugin routes inside the sidebar's existing trust fence. */
|
|
2
|
+
import type { Context } from './context-types.ts';
|
|
3
|
+
export declare function buildPluginMarketApi(ctx: Pick<Context, 'get'>, cwdOf: (payload: unknown) => Promise<{
|
|
4
|
+
cwd: string;
|
|
5
|
+
}>): Record<string, (payload: unknown) => Promise<unknown>>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Credential-free structural contract of the config plugin's host service. */
|
|
2
|
+
export type PluginMarketScope = 'project' | 'user';
|
|
3
|
+
export interface PluginMarketplace {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
official: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface MarketplacePlugin {
|
|
10
|
+
id: string;
|
|
11
|
+
marketplaceId: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
}
|
|
15
|
+
export interface InstalledMarketplacePlugin extends MarketplacePlugin {
|
|
16
|
+
scope: PluginMarketScope;
|
|
17
|
+
projectPath: string;
|
|
18
|
+
installPath: string;
|
|
19
|
+
version: string;
|
|
20
|
+
installedAt: string;
|
|
21
|
+
}
|
|
22
|
+
export interface MarketplaceCatalog {
|
|
23
|
+
marketplaces: PluginMarketplace[];
|
|
24
|
+
warning: string;
|
|
25
|
+
}
|
|
26
|
+
export interface PluginCatalog {
|
|
27
|
+
plugins: MarketplacePlugin[];
|
|
28
|
+
warning: string;
|
|
29
|
+
}
|
|
30
|
+
export interface PluginMarketService {
|
|
31
|
+
marketplaces(): Promise<MarketplaceCatalog>;
|
|
32
|
+
plugins(marketplaceId: string): Promise<MarketplacePlugin[]>;
|
|
33
|
+
catalog?(marketplaceId: string): Promise<PluginCatalog>;
|
|
34
|
+
installed(cwd: string): Promise<InstalledMarketplacePlugin[]>;
|
|
35
|
+
install(cwd: string, pluginId: string, scope: PluginMarketScope): Promise<InstalledMarketplacePlugin>;
|
|
36
|
+
uninstall(cwd: string, pluginId: string, scope: PluginMarketScope): Promise<void>;
|
|
37
|
+
}
|
|
@@ -22,10 +22,9 @@ export interface UITweaksConfig {
|
|
|
22
22
|
*/
|
|
23
23
|
codeFontScale?: number;
|
|
24
24
|
/**
|
|
25
|
-
* Absolute code font size in px (8–32)
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* `codeFontScale` percentage, then to the stock default.
|
|
25
|
+
* Absolute code font size in px (8–32), default 13. Drives code blocks;
|
|
26
|
+
* inline code and small code follow proportionally. When unset, uses a non-default legacy
|
|
27
|
+
* `codeFontScale` percentage, otherwise 13px.
|
|
29
28
|
*/
|
|
30
29
|
codeFontSize?: number;
|
|
31
30
|
/**
|
|
@@ -106,7 +105,7 @@ export interface UITweaksConfig {
|
|
|
106
105
|
}
|
|
107
106
|
export declare const MIN_FONT_SIZE = 10;
|
|
108
107
|
export declare const MAX_FONT_SIZE = 32;
|
|
109
|
-
export declare const DEFAULT_FONT_SIZE =
|
|
108
|
+
export declare const DEFAULT_FONT_SIZE = 14;
|
|
110
109
|
/** 16px matches the stock DSH vertical rhythm (message/block gaps, etc.). */
|
|
111
110
|
export declare const MIN_LINE_HEIGHT = 0;
|
|
112
111
|
export declare const MAX_LINE_HEIGHT = 64;
|
|
@@ -115,7 +114,7 @@ export declare const DEFAULT_LINE_HEIGHT = 16;
|
|
|
115
114
|
export declare const MIN_CODE_FONT_SCALE = 50;
|
|
116
115
|
export declare const MAX_CODE_FONT_SCALE = 150;
|
|
117
116
|
export declare const DEFAULT_CODE_FONT_SCALE = 81;
|
|
118
|
-
/**
|
|
117
|
+
/** Default absolute code-block size, paired with 14px prose. */
|
|
119
118
|
export declare const MIN_CODE_FONT_SIZE = 8;
|
|
120
119
|
export declare const MAX_CODE_FONT_SIZE = 32;
|
|
121
120
|
export declare const DEFAULT_CODE_FONT_SIZE = 13;
|
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
|
+
}
|