Skip to content

Commit e996b44

Browse files
author
xlx1212
committed
feat(loopx): make each requirement row a single state button
- replace the separate status text + install button with one action button that fills the remaining row width, so the three rows line up and the large blank gap on the right is gone - available requirements now render the same button shape with a check icon and the availability label instead of a plain text status - unavailable requirements install on click and flip to a spinner state (the pending flag is set and re-rendered synchronously, plus a pressed style) so the click gets immediate feedback - tag the button with data-state (install / installing / available / status) and style each state, reusing the existing lx-spin keyframes
1 parent 9928df9 commit e996b44

2 files changed

Lines changed: 112 additions & 29 deletions

File tree

  • src/crates/contracts/product-domains/src/miniapp/builtin/assets/bitfun-loopx

‎src/crates/contracts/product-domains/src/miniapp/builtin/assets/bitfun-loopx/style.css‎

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -418,28 +418,71 @@ button:disabled { cursor: not-allowed; opacity: .48; }
418418
gap: 8px;
419419
}
420420
.environment-fact__title strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
421-
.environment-fact__title span { color: var(--lx-text-muted); font: 11px/1.2 var(--lx-mono); }
422-
.environment-fact__status-actions {
423-
display: grid;
424-
grid-template-columns: auto 128px;
425-
align-items: center;
426-
justify-content: start;
427-
gap: 8px;
428-
}
421+
/* 一行只有一个动作按钮:可用时它就是「可用」按钮,不可用时点击即安装;
422+
按钮铺满标签右侧的剩余宽度,所以三行对齐,右侧不再留大块空白。 */
429423
.environment-fact .environment-fact__action {
430424
width: 100%;
425+
min-width: 0;
431426
display: inline-flex;
432427
align-items: center;
433428
justify-content: center;
434429
gap: 5px;
435430
min-height: 32px;
436431
padding: 4px 10px;
432+
border: 1px solid var(--lx-border);
433+
border-radius: 7px;
434+
background: transparent;
435+
color: var(--lx-text-soft);
436+
font-family: inherit;
437437
font-size: 11px;
438438
font-weight: 600;
439439
white-space: nowrap;
440+
cursor: pointer;
441+
transition: background 120ms ease, border-color 120ms ease, color 120ms ease, transform 90ms ease, filter 90ms ease;
440442
}
441443
.environment-fact__action span { color: inherit; font: inherit; letter-spacing: 0; }
442444
.environment-fact__action svg { width: 14px; height: 14px; flex: 0 0 auto; }
445+
.environment-fact__action[data-state="install"] {
446+
border-color: var(--lx-accent);
447+
background: var(--lx-accent);
448+
color: var(--openbitfun-text-on-accent);
449+
}
450+
.environment-fact__action[data-state="install"]:hover:not(:disabled) {
451+
border-color: var(--lx-accent-hover);
452+
background: var(--lx-accent-hover);
453+
}
454+
/* 点击立刻下沉变暗;宿主开始安装后按钮自己切成 spinner。 */
455+
.environment-fact__action:not(:disabled):active { transform: translateY(1px); filter: brightness(.95); }
456+
.environment-fact__action[data-state="installing"] {
457+
border-color: var(--lx-border-strong);
458+
background: var(--lx-muted-surface);
459+
color: var(--lx-text-soft);
460+
cursor: progress;
461+
}
462+
.environment-fact__action[data-state="available"] {
463+
border-color: color-mix(in srgb, var(--lx-success) 40%, var(--lx-border));
464+
background: color-mix(in srgb, var(--lx-success) 12%, transparent);
465+
color: var(--lx-success);
466+
cursor: default;
467+
}
468+
.environment-fact__action[data-state="status"] {
469+
background: transparent;
470+
color: var(--lx-text-muted);
471+
cursor: default;
472+
}
473+
.environment-fact__action[data-state="install"]:disabled {
474+
border-color: var(--lx-border-strong);
475+
background: var(--lx-muted-surface);
476+
color: var(--lx-text-muted);
477+
opacity: .55;
478+
cursor: not-allowed;
479+
}
480+
.environment-fact__action:disabled { cursor: default; opacity: 1; }
481+
.environment-fact__action:focus-visible {
482+
outline: 2px solid color-mix(in srgb, var(--lx-accent) 45%, transparent);
483+
outline-offset: 1px;
484+
}
485+
.environment-fact__spinner { transform-origin: center; animation: lx-spin .8s linear infinite; }
443486
.environment-fact__version { margin: 3px 0 0; overflow: hidden; color: var(--lx-text-muted); font: 11px/1.2 var(--lx-mono); text-overflow: ellipsis; white-space: nowrap; }
444487
@keyframes lx-spin { to { transform: rotate(360deg); } }
445488

@@ -1577,7 +1620,6 @@ button:disabled { cursor: not-allowed; opacity: .48; }
15771620
.environment-body { grid-template-columns: 1fr; align-items: stretch; }
15781621
.environment-grid { grid-template-columns: 1fr; }
15791622
.environment-fact__title { grid-template-columns: 96px minmax(0, 1fr); gap: 6px; }
1580-
.environment-fact__status-actions { grid-template-columns: auto minmax(110px, 128px); gap: 6px; }
15811623
}
15821624

15831625
/* 宽屏三列(任务 | 详情 | 日志);只在右侧工作区真正变窄时上下堆叠。 */
@@ -1599,7 +1641,6 @@ button:disabled { cursor: not-allowed; opacity: .48; }
15991641
@media (max-width: 680px) {
16001642
.environment-grid { grid-template-columns: 1fr; }
16011643
.environment-fact__title { grid-template-columns: 96px minmax(0, 1fr); gap: 6px; }
1602-
.environment-fact__status-actions { grid-template-columns: auto minmax(110px, 128px); gap: 6px; }
16031644
.workbench { grid-template-columns: 1fr; grid-template-rows: minmax(92px, 30dvh) minmax(0, 1fr); }
16041645
.workbench.tasks-collapsed { grid-template-columns: 1fr; grid-template-rows: 44px minmax(0, 1fr); }
16051646
.rail-splitter { display: none; }

‎src/crates/contracts/product-domains/src/miniapp/builtin/assets/bitfun-loopx/ui.js‎

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2716,25 +2716,50 @@ function renderExecutionSupport() {
27162716
view.resumeAllLoopx.title = suspended ? text('resumeAllHint') : text('resumeAll');
27172717
}
27182718

2719-
function environmentInstallIcon() {
2719+
function environmentIcon(paths) {
27202720
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
27212721
svg.setAttribute('viewBox', '0 0 24 24');
2722-
svg.setAttribute('width', '15');
2723-
svg.setAttribute('height', '15');
27242722
svg.setAttribute('fill', 'none');
27252723
svg.setAttribute('stroke', 'currentColor');
27262724
svg.setAttribute('stroke-width', '1.8');
27272725
svg.setAttribute('stroke-linecap', 'round');
27282726
svg.setAttribute('stroke-linejoin', 'round');
27292727
svg.setAttribute('aria-hidden', 'true');
2730-
for (const pathData of ['M12 3v12', 'm7 10 5 5 5-5', 'M5 21h14a2 2 0 0 0 2-2v-4', 'M3 15v4a2 2 0 0 0 2 2']) {
2728+
for (const pathData of paths) {
27312729
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
27322730
path.setAttribute('d', pathData);
27332731
svg.append(path);
27342732
}
27352733
return svg;
27362734
}
27372735

2736+
function environmentInstallIcon() {
2737+
return environmentIcon([
2738+
'M12 3v12',
2739+
'm7 10 5 5 5-5',
2740+
'M5 21h14a2 2 0 0 0 2-2v-4',
2741+
'M3 15v4a2 2 0 0 0 2 2',
2742+
]);
2743+
}
2744+
2745+
function environmentCheckIcon() {
2746+
return environmentIcon(['M20 6 9 17l-5-5']);
2747+
}
2748+
2749+
function environmentSpinnerIcon() {
2750+
const svg = environmentIcon([]);
2751+
const track = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
2752+
track.setAttribute('cx', '12');
2753+
track.setAttribute('cy', '12');
2754+
track.setAttribute('r', '8');
2755+
track.setAttribute('opacity', '0.28');
2756+
const arc = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2757+
arc.setAttribute('d', 'M20 12a8 8 0 0 0-8-8');
2758+
svg.append(track, arc);
2759+
svg.classList.add('environment-fact__spinner');
2760+
return svg;
2761+
}
2762+
27382763
function environmentFact(name, label, fact, action) {
27392764
const element = document.createElement('article');
27402765
const status = fact && fact.status ? fact.status : 'unknown';
@@ -2746,28 +2771,45 @@ function environmentFact(name, label, fact, action) {
27462771
title.className = 'environment-fact__title';
27472772
const strong = document.createElement('strong');
27482773
strong.textContent = label;
2749-
const statusActions = document.createElement('div');
2750-
statusActions.className = 'environment-fact__status-actions';
2751-
const value = document.createElement('span');
2752-
value.textContent = statusLabel(status);
2753-
statusActions.append(value);
2754-
2755-
if (action) {
2756-
const button = document.createElement('button');
2757-
button.type = 'button';
2758-
button.className = 'primary-button environment-fact__action';
2774+
2775+
// One button carries the whole row state: install -> installing -> available.
2776+
// A click flips the local pending flag synchronously, so the spinner shows
2777+
// before the host round-trip finishes.
2778+
const button = document.createElement('button');
2779+
button.type = 'button';
2780+
button.className = 'environment-fact__action';
2781+
let buttonState;
2782+
let buttonLabel;
2783+
if (action && action.pending) {
2784+
buttonState = 'installing';
2785+
buttonLabel = text('installing');
2786+
button.disabled = true;
2787+
button.append(environmentSpinnerIcon());
2788+
} else if (action) {
2789+
buttonState = 'install';
2790+
buttonLabel = action.label;
27592791
button.disabled = Boolean(action.disabled);
27602792
button.append(environmentInstallIcon());
2761-
const buttonLabel = document.createElement('span');
2762-
buttonLabel.textContent = action.pending ? text('installing') : action.label;
2763-
button.append(buttonLabel);
27642793
button.addEventListener('click', () => action.onClick());
27652794
if (action.onPointerDown) {
27662795
button.addEventListener('pointerdown', action.onPointerDown);
27672796
}
2768-
statusActions.append(button);
2797+
} else if (status === 'available') {
2798+
buttonState = 'available';
2799+
buttonLabel = statusLabel(status);
2800+
button.disabled = true;
2801+
button.append(environmentCheckIcon());
2802+
} else {
2803+
buttonState = 'status';
2804+
buttonLabel = statusLabel(status);
2805+
button.disabled = true;
27692806
}
2770-
title.append(strong, statusActions);
2807+
button.dataset.state = buttonState;
2808+
const buttonLabelElement = document.createElement('span');
2809+
buttonLabelElement.textContent = buttonLabel;
2810+
button.append(buttonLabelElement);
2811+
2812+
title.append(strong, button);
27712813
element.append(title);
27722814

27732815
// Keep the row compact: only the short version stays visible; the long host

0 commit comments

Comments
 (0)