mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 08:42:54 +00:00
feat: resize Skill Workshop proposal list
This commit is contained in:
@@ -75,7 +75,7 @@
|
||||
/* ── Triage two-pane card ───────────────────────────────────────────── */
|
||||
.sw-triage {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
grid-template-columns: minmax(260px, var(--sw-queue-width, 360px)) 8px minmax(0, 1fr);
|
||||
gap: 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -89,7 +89,6 @@
|
||||
|
||||
/* ── Queue (left) ───────────────────────────────────────────────────── */
|
||||
.sw-queue {
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
@@ -97,6 +96,20 @@
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.sw-queue-resizer {
|
||||
border-left: 1px solid var(--border);
|
||||
border-right: 1px solid var(--border);
|
||||
cursor: col-resize;
|
||||
min-width: 8px;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.sw-queue-resizer:hover,
|
||||
.sw-queue-resizer:focus-visible {
|
||||
background: color-mix(in srgb, var(--accent) 16%, transparent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.sw-queue__head {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
@@ -525,3 +538,13 @@
|
||||
color: var(--text-strong);
|
||||
text-decoration-color: var(--accent);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.sw-triage {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sw-queue-resizer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,7 +487,11 @@ let clawhubSearchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const UPDATE_BANNER_DISMISS_KEY = "openclaw:control-ui:update-banner-dismissed:v1";
|
||||
const SKILL_WORKSHOP_REVIEWED_KEY = "openclaw:control-ui:skill-workshop-reviewed:v1";
|
||||
const SKILL_WORKSHOP_QUEUE_WIDTH_KEY = "openclaw:control-ui:skill-workshop-queue-width:v1";
|
||||
const MAX_SKILL_WORKSHOP_REVIEWED_KEYS = 500;
|
||||
const DEFAULT_SKILL_WORKSHOP_QUEUE_WIDTH = 360;
|
||||
const MIN_SKILL_WORKSHOP_QUEUE_WIDTH = 280;
|
||||
const MAX_SKILL_WORKSHOP_QUEUE_WIDTH = 560;
|
||||
const CRON_THINKING_SUGGESTIONS = ["off", "minimal", "low", "medium", "high"];
|
||||
const CRON_TIMEZONE_SUGGESTIONS = [
|
||||
"UTC",
|
||||
@@ -570,6 +574,40 @@ function saveSkillWorkshopReviewedKeys(keys: string[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function loadSkillWorkshopQueueWidth(): number {
|
||||
const raw = getSafeLocalStorage()?.getItem(SKILL_WORKSHOP_QUEUE_WIDTH_KEY);
|
||||
if (!raw) {
|
||||
return DEFAULT_SKILL_WORKSHOP_QUEUE_WIDTH;
|
||||
}
|
||||
return clampSkillWorkshopQueueWidth(Number(raw));
|
||||
}
|
||||
|
||||
function clampSkillWorkshopQueueWidth(width: number): number {
|
||||
if (!Number.isFinite(width)) {
|
||||
return DEFAULT_SKILL_WORKSHOP_QUEUE_WIDTH;
|
||||
}
|
||||
return Math.min(
|
||||
MAX_SKILL_WORKSHOP_QUEUE_WIDTH,
|
||||
Math.max(MIN_SKILL_WORKSHOP_QUEUE_WIDTH, Math.round(width)),
|
||||
);
|
||||
}
|
||||
|
||||
function setSkillWorkshopQueueWidth(
|
||||
state: AppViewState,
|
||||
width: number,
|
||||
options?: { persist?: boolean },
|
||||
): void {
|
||||
const next = clampSkillWorkshopQueueWidth(width);
|
||||
state.skillWorkshopQueueWidth = next;
|
||||
if (options?.persist) {
|
||||
try {
|
||||
getSafeLocalStorage()?.setItem(SKILL_WORKSHOP_QUEUE_WIDTH_KEY, String(next));
|
||||
} catch {
|
||||
// Width persistence is a convenience; the current drag state still applies.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function skillWorkshopReviewKey(proposal: SkillWorkshopReviewableProposal): string {
|
||||
return `${proposal.key}:${proposal.version}:${proposal.updatedAt ?? proposal.createdAt}`;
|
||||
}
|
||||
@@ -2956,10 +2994,14 @@ export function renderApp(state: AppViewState) {
|
||||
query: state.skillWorkshopQuery,
|
||||
filePreviewKey: state.skillWorkshopFilePreviewKey,
|
||||
filePreviewQuery: state.skillWorkshopFilePreviewQuery,
|
||||
queueWidth: state.skillWorkshopQueueWidth,
|
||||
counts,
|
||||
onStatusFilterChange: (next) => (state.skillWorkshopStatusFilter = next),
|
||||
onQueryChange: (next) => (state.skillWorkshopQuery = next),
|
||||
onFilePreviewQueryChange: (next) => (state.skillWorkshopFilePreviewQuery = next),
|
||||
onQueueWidthChange: (width) => setSkillWorkshopQueueWidth(state, width),
|
||||
onQueueWidthCommit: (width) =>
|
||||
setSkillWorkshopQueueWidth(state, width, { persist: true }),
|
||||
onSelect: (key) => {
|
||||
const proposal = proposals.find((p) => p.key === key);
|
||||
state.skillWorkshopSelectedKey = key;
|
||||
|
||||
@@ -431,6 +431,7 @@ export type AppViewState = {
|
||||
skillWorkshopFilePreviewKey: string | null;
|
||||
skillWorkshopFilePreviewQuery: string;
|
||||
skillWorkshopReviewedKeys: string[];
|
||||
skillWorkshopQueueWidth: number;
|
||||
healthLoading: boolean;
|
||||
healthResult: HealthSummary | null;
|
||||
healthError: string | null;
|
||||
|
||||
@@ -42,7 +42,11 @@ import {
|
||||
} from "./app-lifecycle.ts";
|
||||
import { initNativeBridge } from "./app-native-bridge.ts";
|
||||
import { createChatSession as createChatSessionInternal } from "./app-render.helpers.ts";
|
||||
import { loadSkillWorkshopReviewedKeys, renderApp } from "./app-render.ts";
|
||||
import {
|
||||
loadSkillWorkshopQueueWidth,
|
||||
loadSkillWorkshopReviewedKeys,
|
||||
renderApp,
|
||||
} from "./app-render.ts";
|
||||
import {
|
||||
exportLogs as exportLogsInternal,
|
||||
handleActivityScroll as handleActivityScrollInternal,
|
||||
@@ -634,6 +638,7 @@ export class OpenClawApp extends LitElement {
|
||||
@state() skillWorkshopFilePreviewKey: string | null = null;
|
||||
@state() skillWorkshopFilePreviewQuery = "";
|
||||
@state() skillWorkshopReviewedKeys = loadSkillWorkshopReviewedKeys();
|
||||
@state() skillWorkshopQueueWidth = loadSkillWorkshopQueueWidth();
|
||||
|
||||
@state() healthLoading = false;
|
||||
@state() healthResult: HealthSummary | null = null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { styleMap } from "lit/directives/style-map.js";
|
||||
import "../components/file-preview-modal.ts";
|
||||
|
||||
export type SkillWorkshopProposalStatus =
|
||||
@@ -40,10 +41,13 @@ export type SkillWorkshopProps = {
|
||||
query: string;
|
||||
filePreviewKey: string | null;
|
||||
filePreviewQuery: string;
|
||||
queueWidth: number;
|
||||
counts: Record<SkillWorkshopStatusFilter, number>;
|
||||
onStatusFilterChange: (status: SkillWorkshopStatusFilter) => void;
|
||||
onQueryChange: (query: string) => void;
|
||||
onFilePreviewQueryChange: (query: string) => void;
|
||||
onQueueWidthChange: (width: number) => void;
|
||||
onQueueWidthCommit: (width: number) => void;
|
||||
onSelect: (key: string) => void;
|
||||
onPrev: () => void;
|
||||
onNext: () => void;
|
||||
@@ -90,8 +94,8 @@ export function renderSkillWorkshop(props: SkillWorkshopProps) {
|
||||
return html`
|
||||
<section class="skill-workshop">
|
||||
${renderLifecycleTabs(props)}
|
||||
<div class="sw-triage">
|
||||
${renderQueue(props, groups, selected)}
|
||||
<div class="sw-triage" style=${styleMap({ "--sw-queue-width": `${props.queueWidth}px` })}>
|
||||
${renderQueue(props, groups, selected)} ${renderQueueResizer(props)}
|
||||
${selected ? renderDetail(props, selected) : renderEmpty()}
|
||||
</div>
|
||||
</section>
|
||||
@@ -113,6 +117,65 @@ export function renderSkillWorkshop(props: SkillWorkshopProps) {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderQueueResizer(props: SkillWorkshopProps) {
|
||||
return html`
|
||||
<div
|
||||
class="sw-queue-resizer"
|
||||
role="separator"
|
||||
aria-label="Resize proposal list"
|
||||
aria-orientation="vertical"
|
||||
tabindex="0"
|
||||
@pointerdown=${(event: PointerEvent) => startQueueResize(event, props)}
|
||||
@keydown=${(event: KeyboardEvent) => resizeQueueWithKeyboard(event, props)}
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
|
||||
function startQueueResize(event: PointerEvent, props: SkillWorkshopProps): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const startX = event.clientX;
|
||||
const startWidth = props.queueWidth;
|
||||
let currentWidth = startWidth;
|
||||
const body = document.body;
|
||||
const previousCursor = body.style.cursor;
|
||||
const previousUserSelect = body.style.userSelect;
|
||||
body.style.cursor = "col-resize";
|
||||
body.style.userSelect = "none";
|
||||
|
||||
const cleanup = () => {
|
||||
window.removeEventListener("pointermove", onMove);
|
||||
window.removeEventListener("pointerup", onUp);
|
||||
window.removeEventListener("pointercancel", onUp);
|
||||
body.style.cursor = previousCursor;
|
||||
body.style.userSelect = previousUserSelect;
|
||||
};
|
||||
|
||||
const onMove = (moveEvent: PointerEvent) => {
|
||||
currentWidth = startWidth + moveEvent.clientX - startX;
|
||||
props.onQueueWidthChange(currentWidth);
|
||||
};
|
||||
|
||||
const onUp = () => {
|
||||
cleanup();
|
||||
props.onQueueWidthCommit(currentWidth);
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", onMove);
|
||||
window.addEventListener("pointerup", onUp);
|
||||
window.addEventListener("pointercancel", onUp);
|
||||
}
|
||||
|
||||
function resizeQueueWithKeyboard(event: KeyboardEvent, props: SkillWorkshopProps): void {
|
||||
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const delta = event.key === "ArrowLeft" ? -24 : 24;
|
||||
props.onQueueWidthCommit(props.queueWidth + delta);
|
||||
}
|
||||
|
||||
function renderLifecycleTabs(props: SkillWorkshopProps) {
|
||||
return html`
|
||||
<div class="sw-lifecycle-tabs">
|
||||
|
||||
Reference in New Issue
Block a user