feat: more UI cleanup
This commit is contained in:
124
priv/ui/app.css
124
priv/ui/app.css
@@ -5,7 +5,7 @@
|
||||
--vscode-panel-background: #1e1e1e;
|
||||
--vscode-titleBar-activeBackground: #252526;
|
||||
--vscode-titleBar-activeForeground: #cccccc;
|
||||
--vscode-statusBar-background: #181818;
|
||||
--vscode-statusBar-background: #007acc;
|
||||
--vscode-statusBar-foreground: #ffffff;
|
||||
--vscode-tab-activeBackground: #1e1e1e;
|
||||
--vscode-tab-inactiveBackground: #2d2d2d;
|
||||
@@ -818,6 +818,128 @@ button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.project-selector {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.project-selector-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
height: 22px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--vscode-statusBar-foreground);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.project-selector-trigger:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.project-selector-trigger:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.project-icon,
|
||||
.dropdown-arrow,
|
||||
.project-check-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.project-name,
|
||||
.project-item-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.project-name {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.project-dropdown {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 100%;
|
||||
min-width: 220px;
|
||||
margin-bottom: 4px;
|
||||
background-color: #252526;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-dropdown-header {
|
||||
padding: 8px 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.project-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.project-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.project-item:hover,
|
||||
.project-item.active {
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.project-item.active .project-check-icon {
|
||||
color: #89d185;
|
||||
}
|
||||
|
||||
.project-dropdown-footer {
|
||||
padding: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.create-project-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
color: inherit;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.create-project-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.status-bar-language-select {
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
218
priv/ui/app.js
218
priv/ui/app.js
@@ -13,6 +13,8 @@ const isMac = typeof navigator !== "undefined" && navigator.platform.toLowerCase
|
||||
const state = {
|
||||
session: hydrateSession(clone(bootstrap.session)),
|
||||
status: clone(bootstrap.status),
|
||||
projects: normalizeProjects(bootstrap.projects),
|
||||
projectMenuOpen: false,
|
||||
taskStatus: normalizeTaskStatus(bootstrap.task_status),
|
||||
outputEntries: [],
|
||||
gitLogEntries: [],
|
||||
@@ -24,6 +26,7 @@ const state = {
|
||||
bindNativeMenuBridge();
|
||||
bindGlobalHotkeys();
|
||||
scheduleTaskPolling();
|
||||
void fetchProjects();
|
||||
render();
|
||||
|
||||
function render() {
|
||||
@@ -393,6 +396,7 @@ function renderStatusBar() {
|
||||
|
||||
root.querySelector(".status-bar").innerHTML = `
|
||||
<div class="status-bar-left">
|
||||
${renderProjectSelector()}
|
||||
<button class="status-bar-item status-bar-task-button" data-command="open-tasks-panel" type="button">
|
||||
<span>${escapeHtml(taskMessage)}</span>
|
||||
${taskOverflow > 0 ? `<span class="status-bar-count">+${taskOverflow}</span>` : ""}
|
||||
@@ -422,6 +426,10 @@ function bindEvents() {
|
||||
root.querySelectorAll("button[data-command]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const command = button.dataset.command;
|
||||
if (command === "create-project") {
|
||||
void createProject();
|
||||
return;
|
||||
}
|
||||
if (command === "open-tasks-panel") {
|
||||
openTasksPanel();
|
||||
render();
|
||||
@@ -435,6 +443,25 @@ function bindEvents() {
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-project-menu-trigger]").forEach((button) => {
|
||||
button.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
toggleProjectMenu();
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-project-id]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
void selectProject(button.dataset.projectId);
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-project-create]").forEach((button) => {
|
||||
button.onclick = () => {
|
||||
void createProject();
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll("[data-command='set-ui-language']").forEach((select) => {
|
||||
select.onchange = (event) => {
|
||||
setUiLanguage(event.target.value);
|
||||
@@ -512,6 +539,8 @@ function bindEvents() {
|
||||
},
|
||||
invert: true,
|
||||
});
|
||||
|
||||
bindProjectMenuDismiss();
|
||||
}
|
||||
|
||||
function bindNativeMenuBridge() {
|
||||
@@ -606,6 +635,101 @@ async function fetchTaskStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProjects() {
|
||||
try {
|
||||
const response = await fetch("/api/projects", {
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.projects = normalizeProjects(await response.json());
|
||||
if (!state.projects.active_project_id && state.projects.projects.length) {
|
||||
state.projects.active_project_id = state.projects.projects[0].id;
|
||||
}
|
||||
render();
|
||||
} catch (_error) {
|
||||
// Keep the shell usable if project loading is temporarily unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
async function createProject() {
|
||||
const name = window.prompt("New project name", "New Project");
|
||||
if (!name || !name.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeProjectMenu();
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/projects", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ name: name.trim() }),
|
||||
});
|
||||
|
||||
const payload = await response.json();
|
||||
|
||||
if (!response.ok || payload.status !== "ok") {
|
||||
appendOutputEntry("Create Project", payload.error?.message || `Command failed with HTTP ${response.status}`);
|
||||
setPanelTab("output");
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchProjects();
|
||||
appendOutputEntry("Create Project", `Activated ${payload.project.name}`);
|
||||
render();
|
||||
} catch (error) {
|
||||
appendOutputEntry("Create Project", error?.message || String(error));
|
||||
setPanelTab("output");
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function selectProject(projectId) {
|
||||
if (!projectId || projectId === state.projects.active_project_id) {
|
||||
closeProjectMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
closeProjectMenu();
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/projects", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ project_id: projectId }),
|
||||
});
|
||||
|
||||
const payload = await response.json();
|
||||
|
||||
if (!response.ok || payload.status !== "ok") {
|
||||
appendOutputEntry("Select Project", payload.error?.message || `Command failed with HTTP ${response.status}`);
|
||||
setPanelTab("output");
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchProjects();
|
||||
appendOutputEntry("Select Project", `Activated ${payload.project.name}`);
|
||||
render();
|
||||
} catch (error) {
|
||||
appendOutputEntry("Select Project", error?.message || String(error));
|
||||
setPanelTab("output");
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function openTasksPanel() {
|
||||
state.session.panel.visible = true;
|
||||
state.session.panel.active_tab = "tasks";
|
||||
@@ -1198,6 +1322,13 @@ function normalizeTaskStatus(taskStatus) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeProjects(projectsPayload) {
|
||||
return {
|
||||
active_project_id: projectsPayload?.active_project_id || null,
|
||||
projects: Array.isArray(projectsPayload?.projects) ? projectsPayload.projects : [],
|
||||
};
|
||||
}
|
||||
|
||||
function panelTabs() {
|
||||
return ["tasks", state.session.panel.active_tab, "output", "git_log"].filter(uniqueValue);
|
||||
}
|
||||
@@ -1227,6 +1358,93 @@ function renderLanguageOptions() {
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderProjectSelector() {
|
||||
const activeProject = currentProject();
|
||||
|
||||
return `
|
||||
<div class="project-selector${state.projectMenuOpen ? " is-open" : ""}">
|
||||
<button class="project-selector-trigger" data-project-menu-trigger type="button" title="Switch project">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" class="project-icon">
|
||||
<path d="M14.5 3H7.71l-.85-.85A.5.5 0 0 0 6.5 2h-5a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-10a.5.5 0 0 0-.5-.5zm-13 1h5.29l.85.85c.1.1.23.15.36.15h6.5v9h-13V4z"></path>
|
||||
</svg>
|
||||
<span class="project-name">${escapeHtml(activeProject?.name || "My Blog")}</span>
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" class="dropdown-arrow">
|
||||
<path d="M4.5 5.5L8 9l3.5-3.5h-7z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
${state.projectMenuOpen ? renderProjectDropdown() : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderProjectDropdown() {
|
||||
return `
|
||||
<div class="project-dropdown">
|
||||
<div class="project-dropdown-header">
|
||||
<span>Projects</span>
|
||||
</div>
|
||||
<div class="project-list">
|
||||
${state.projects.projects.map((project) => renderProjectItem(project)).join("")}
|
||||
</div>
|
||||
<div class="project-dropdown-footer">
|
||||
<button class="create-project-btn" data-project-create type="button">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M14 7v1H8v6H7V8H1V7h6V1h1v6h6z"></path>
|
||||
</svg>
|
||||
New Project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderProjectItem(project) {
|
||||
const active = project.id === state.projects.active_project_id;
|
||||
|
||||
return `
|
||||
<button class="project-item ${active ? "active" : ""}" data-project-id="${escapeHtmlAttribute(project.id)}" type="button">
|
||||
<span class="project-item-name">${escapeHtml(project.name)}</span>
|
||||
${active ? `<span class="project-check-icon">✓</span>` : ""}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function currentProject() {
|
||||
return state.projects.projects.find((project) => project.id === state.projects.active_project_id) || state.projects.projects[0] || null;
|
||||
}
|
||||
|
||||
function toggleProjectMenu() {
|
||||
state.projectMenuOpen = !state.projectMenuOpen;
|
||||
render();
|
||||
}
|
||||
|
||||
function closeProjectMenu() {
|
||||
if (!state.projectMenuOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.projectMenuOpen = false;
|
||||
render();
|
||||
}
|
||||
|
||||
function bindProjectMenuDismiss() {
|
||||
if (window.__BDS_PROJECT_MENU_DISMISS_BOUND__) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.__BDS_PROJECT_MENU_DISMISS_BOUND__ = true;
|
||||
document.addEventListener("mousedown", (event) => {
|
||||
if (!state.projectMenuOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selector = root.querySelector(".project-selector");
|
||||
if (selector && !selector.contains(event.target)) {
|
||||
closeProjectMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setUiLanguage(nextLanguage) {
|
||||
state.uiLanguage = nextLanguage;
|
||||
state.status.right.ui_language = nextLanguage;
|
||||
|
||||
Reference in New Issue
Block a user