style: improve web-server panel of list service card in config server modal

This commit is contained in:
KodekaTeam committed 2026-06-04 01:48:32 +07:00
1 parent 4f634530da
commit af92445c6c
4 files changed
+327 -100

No files matched your search

+217 -3
View File
@@ -407,9 +407,223 @@ body {
font-size: 11px;
font-weight: 600;
line-height: 1.25;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
}
.server-config-component-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
gap: 16px;
}
.server-config-component-card {
min-width: 0;
min-height: 100%;
padding: 16px;
display: flex;
flex-direction: column;
gap: 14px;
}
.server-config-component-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
align-items: start;
}
.server-config-component-copy {
min-width: 0;
}
.server-config-component-description {
margin-top: 6px;
font-size: 12px;
color: var(--text-muted);
line-height: 1.6;
}
.server-config-component-divider {
margin: 0;
border: 0;
border-top: 1px solid var(--border);
}
.server-config-badge-list {
display: flex;
align-items: flex-start;
gap: 8px;
flex-wrap: wrap;
min-width: 0;
}
.server-config-readiness-notices {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
gap: 8px;
}
.server-config-readiness-notice {
min-width: 0;
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid rgba(245, 158, 11, 0.24);
background: rgba(245, 158, 11, 0.08);
color: #b45309;
font-size: 12px;
line-height: 1.5;
}
.server-config-readiness-notice svg {
flex-shrink: 0;
margin-top: 2px;
color: #f59e0b;
}
.server-config-readiness-notice span {
min-width: 0;
}
.server-config-component-issue-summary {
width: 100%;
min-width: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 9px 11px;
border-radius: 8px;
color: #ef4444;
background: rgba(248, 113, 113, 0.15);
border: 1px solid rgba(248, 113, 113, 0.3);
cursor: pointer;
font-size: 12px;
font-weight: 600;
text-align: left;
}
.server-config-component-issue-summary:hover {
background: rgba(248, 113, 113, 0.2);
border-color: rgba(248, 113, 113, 0.42);
}
.server-config-component-issue-copy {
min-width: 0;
display: inline-flex;
align-items: center;
gap: 7px;
}
.server-config-component-issue-copy span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.server-config-component-issue-action {
flex-shrink: 0;
color: #dc2626;
font-size: 11px;
}
.server-config-issue-overlay {
position: fixed;
inset: 0;
z-index: 1003;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(3, 7, 18, 0.58);
}
.server-config-issue-dialog {
width: min(520px, 100%);
max-height: min(70vh, 560px);
padding: 18px;
display: grid;
gap: 14px;
overflow: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
}
.server-config-issue-dialog-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.server-config-issue-close {
width: 30px;
height: 30px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
border: 1px solid rgba(148, 163, 184, 0.18);
background: rgba(15, 23, 42, 0.16);
color: var(--text-muted);
cursor: pointer;
}
.server-config-issue-close:hover {
color: var(--text-primary);
border-color: var(--border-hover);
}
.server-config-issue-list {
display: grid;
gap: 8px;
}
.server-config-issue-note {
padding: 12px;
border-radius: 8px;
border: 1px solid rgba(248, 113, 113, 0.3);
background: rgba(248, 113, 113, 0.14);
color: var(--text-danger);
font-size: 12px;
line-height: 1.55;
}
.server-config-component-actions {
margin-top: auto;
padding-top: 14px;
border-top: 1px solid var(--border);
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.server-config-component-actions .btn {
min-width: 94px;
white-space: nowrap;
}
@media (max-width: 520px) {
.server-config-component-card {
padding: 14px;
}
.server-config-component-header {
grid-template-columns: 1fr;
}
.server-config-component-header > .ui-badge {
justify-self: start;
}
.server-config-component-actions .btn {
flex: 1 1 140px;
}
}
/* Progress bars */
@@ -1,6 +1,7 @@
"use client";
import { type ReactNode, useCallback, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import {
AlertTriangle,
Eye,
@@ -638,19 +639,20 @@ export default function ServerConfigModal({
gap: 18,
}}
>
{pendingConfirm ? (
<div
style={{
position: "fixed",
inset: 0,
background: "rgba(3,7,18,0.58)",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 20,
zIndex: 1002,
}}
>
{pendingConfirm && typeof document !== "undefined"
? createPortal(
<div
style={{
position: "fixed",
inset: 0,
background: "rgba(3,7,18,0.58)",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 20,
zIndex: 1002,
}}
>
<div
className="card"
style={{
@@ -895,8 +897,10 @@ export default function ServerConfigModal({
</button>
</div>
</div>
</div>
) : null}
</div>,
document.body,
)
: null}
<div
style={{
@@ -73,6 +73,7 @@ export function UserBadge({
return (
<span
className="ui-badge"
title={label}
style={{
color: style.color,
background: style.background,
@@ -1,7 +1,15 @@
"use client";
import { useState } from "react";
import { Loader2, Plus, RefreshCw, Trash2 } from "lucide-react";
import { createPortal } from "react-dom";
import {
AlertTriangle,
Loader2,
Plus,
RefreshCw,
Trash2,
X,
} from "lucide-react";
import type {
WebStackComponentStatus,
ServerConfigSnapshot,
@@ -39,6 +47,10 @@ export default function ServerConfigWebServerPanel({
const [activeGroup, setActiveGroup] = useState<
"infrastructure" | "runtime-tools"
>("infrastructure");
const [activeIssueDetail, setActiveIssueDetail] = useState<{
label: string;
notes: string[];
} | null>(null);
const infrastructureComponents = snapshot.webServer.components.filter(
(component) =>
@@ -55,31 +67,13 @@ export default function ServerConfigWebServerPanel({
: runtimeToolComponents;
const renderComponentCard = (component: WebStackComponentStatus) => (
<div
key={component.key}
className="card"
style={{ padding: 16, display: "grid", gap: 14 }}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
gap: 12,
}}
>
<div>
<div key={component.key} className="card server-config-component-card">
<div className="server-config-component-header">
<div className="server-config-component-copy">
<strong style={{ color: "var(--text-primary)", fontSize: 14 }}>
{component.label}
</strong>
<p
style={{
marginTop: 6,
fontSize: 12,
color: "var(--text-muted)",
lineHeight: 1.6,
}}
>
<p className="server-config-component-description">
{component.description}
</p>
</div>
@@ -88,11 +82,9 @@ export default function ServerConfigWebServerPanel({
tone={component.installed ? "success" : "warning"}
/>
</div>
<hr style={{ borderColor: "var(--border)" }} />
<hr className="server-config-component-divider" />
<div
style={{ display: "flex", gap: 8, maxHeight: "20px", flexWrap: "wrap" }}
>
<div className="server-config-badge-list">
<UserBadge label={component.category} tone="neutral" />
{component.version ? (
<UserBadge label={component.version} tone="neutral" />
@@ -108,48 +100,36 @@ export default function ServerConfigWebServerPanel({
) : null}
</div>
<div
style={{ display: "flex", gap: 8, maxHeight: "20px", flexWrap: "wrap" }}
>
<div className="server-config-badge-list">
{component.recommendedFor.map((target) => (
<UserBadge key={target} label={target} tone="neutral" />
))}
</div>
{component.notes.length > 0 ? (
<div
style={{
display: "grid",
gap: 6,
lineHeight: 1.4,
padding: 12,
borderRadius: 12,
background: "rgba(248, 113, 113, 0.15)",
border: "1px solid rgba(248, 113, 113, 0.3)",
}}
<button
type="button"
className="server-config-component-issue-summary"
onClick={() =>
setActiveIssueDetail({
label: component.label,
notes: component.notes,
})
}
>
{component.notes.map((note) => (
<div
key={note}
style={{ color: "var(--text-danger)", fontSize: 12 }}
>
{note}
</div>
))}
</div>
<span className="server-config-component-issue-copy">
<AlertTriangle size={13} />
<span>
{component.notes.length === 1
? "1 issue detected"
: `${component.notes.length} issues detected`}
</span>
</span>
<span className="server-config-component-issue-action">Details</span>
</button>
) : null}
<hr style={{ borderColor: "var(--border)" }} />
<div
style={{
display: "flex",
gap: 6,
maxHeight: "20px",
padding: "0 0 24px 0",
flexWrap: "wrap",
}}
>
<div className="server-config-component-actions">
{component.availableActions.length > 0 ? (
component.availableActions.map((action) => {
const actionStyle = getWebStackActionStyle(action);
@@ -192,6 +172,51 @@ export default function ServerConfigWebServerPanel({
return (
<div style={{ display: "grid", gap: 16 }}>
{activeIssueDetail && typeof document !== "undefined"
? createPortal(
<div className="server-config-issue-overlay">
<div className="card server-config-issue-dialog">
<div className="server-config-issue-dialog-header">
<div>
<strong
style={{ color: "var(--text-primary)", fontSize: 15 }}
>
{activeIssueDetail.label} Issues
</strong>
<p
style={{
marginTop: 6,
color: "var(--text-muted)",
fontSize: 12,
lineHeight: 1.5,
}}
>
Service information returned by the current server
snapshot.
</p>
</div>
<button
type="button"
onClick={() => setActiveIssueDetail(null)}
aria-label="Close issue details"
className="server-config-issue-close"
>
<X size={14} />
</button>
</div>
<div className="server-config-issue-list">
{activeIssueDetail.notes.map((note) => (
<div key={note} className="server-config-issue-note">
{note}
</div>
))}
</div>
</div>
</div>,
document.body,
)
: null}
<div className="card" style={{ padding: 18, display: "grid", gap: 14 }}>
<div
style={{
@@ -303,22 +328,11 @@ export default function ServerConfigWebServerPanel({
/>
</div>
{snapshot.webServer.notes.length > 0 ? (
<div
style={{
display: "grid",
gap: 6,
padding: 12,
borderRadius: 12,
background: "rgba(15,23,42,0.18)",
border: "1px solid rgba(148,163,184,0.14)",
}}
>
<div className="server-config-readiness-notices">
{snapshot.webServer.notes.map((note) => (
<div
key={note}
style={{ color: "var(--text-muted)", fontSize: 12 }}
>
{note}
<div key={note} className="server-config-readiness-notice">
<AlertTriangle size={13} />
<span>{note}</span>
</div>
))}
</div>
@@ -437,13 +451,7 @@ export default function ServerConfigWebServerPanel({
</div>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
gap: 16,
}}
>
<div className="server-config-component-grid">
{visibleComponents.map((component) => renderComponentCard(component))}
</div>
</div>