/* ============================================================ layout.css — 命令栏头部 / 主布局 / 卡片 / 表单 / 按钮 / 侧滑抽屉 ============================================================ */ /* ================= HEADER — 命令栏 ================= */ .header-bar { position: relative; z-index: 20; /* 高于下方卡片,保证 pill tooltip 不被遮挡 */ display: flex; align-items: center; gap: 16px; padding: 16px 22px; margin-bottom: 26px; background: radial-gradient(620px 240px at 88% -40%, rgba(47,216,193,.12), transparent 60%), linear-gradient(160deg, var(--bg-panel) 0%, var(--bg-card) 70%); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); flex-wrap: wrap; } /* 顶部三色霓虹描边:ESP32 青绿 → BLE 蓝 → GD32 琥珀,一条细线点出子系统谱系 */ .header-bar::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, var(--esp32) 0%, var(--ble) 38%, var(--gd32) 78%, transparent 100%); opacity: .85; border-radius: var(--radius-lg) var(--radius-lg) 0 0; } .header-brand { display: flex; align-items: center; gap: 12px; padding-right: 20px; border-right: 1px solid var(--border); white-space: nowrap; } .brand-logo { width: 40px; height: 40px; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: 11px; background: linear-gradient(145deg, rgba(47,216,193,.16), rgba(95,168,255,.10)); border: 1px solid rgba(47,216,193,.3); box-shadow: 0 0 18px rgba(47,216,193,.18), inset 0 0 12px rgba(47,216,193,.08); color: var(--esp32); } .brand-logo svg { width: 24px; height: 24px; display: block; } .brand-text { display: flex; flex-direction: column; line-height: 1.15; } .brand-text strong { font-family: var(--font-display); font-size: 17px; font-weight: 700; letter-spacing: .02em; background: linear-gradient(92deg, var(--text) 30%, var(--esp32-hi) 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; } .brand-sub { font-family: var(--font-mono); font-size: 10px; letter-spacing: .22em; text-transform: uppercase; color: var(--text-label); margin-top: 2px; } /* 兼容旧结构:独立 dot-mark(无 brand-logo 时)仍显示为一颗呼吸 LED */ .header-brand .dot-mark { width: 10px; height: 10px; border-radius: 50%; background: var(--esp32); box-shadow: 0 0 10px var(--esp32-dim); flex-shrink: 0; } .header-brand strong { font-size: 16px; font-weight: 700; } .header-status { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .header-sub { display: flex; gap: 10px; margin-left: auto; flex-wrap: wrap; } /* ---- 状态胶囊 ---- */ .pill { display: inline-flex; align-items: center; gap: 7px; padding: 6px 13px; border-radius: 999px; font-size: 12px; font-weight: 500; font-family: var(--font-mono); background: var(--bg-elevated); border: 1px solid var(--border); color: var(--text-dim); white-space: nowrap; transition: border-color .2s, background .2s, transform .2s; } .pill .indicator { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--text-label); } .pill.online .indicator { background: var(--green); box-shadow: 0 0 8px rgba(61,220,142,.6); } .pill.offline .indicator { background: var(--red); box-shadow: 0 0 8px rgba(255,107,107,.5); } .pill.processing .indicator, .pill.live .indicator { background: var(--amber); animation: blink 1s infinite; } .pill.idle .indicator { background: var(--text-dim); } .pill.online { color: var(--can-hi); border-color: rgba(61,220,142,.35); } .pill.offline { color: #ffb3b3; border-color: rgba(255,107,107,.3); } .pill.processing, .pill.live { color: var(--gd32-hi); border-color: rgba(252,191,63,.35); } @keyframes blink { 50% { opacity: .3; } } .pill-val { font-weight: 700; color: var(--text); font-family: var(--font-mono); } /* ---- 顶栏三链路胶囊(WiFi / ECAT / BLE) ---- */ .link-pill { gap: 6px; padding: 5px 10px 5px 8px; } .link-pill-tag { font-size: 10px; font-weight: 700; letter-spacing: .04em; padding: 1px 5px; border-radius: 4px; color: var(--esp32); background: var(--esp32-dim); } .link-pill-tag.tag-ecat { color: var(--mem-hi); background: var(--mem-dim); } .link-pill-tag.tag-ble { color: var(--ble); background: rgba(95,168,255,.12); } .link-pill #pillWifiTxt, .link-pill #pillEcatTxt, .link-pill #pillBleTxt, .link-pill #canNetTxt { font-size: 11px; overflow: hidden; text-overflow: ellipsis; } /* WiFi 要带 IP+信号;ECAT/BLE 连接后显示设备名 */ .link-pill #pillWifiTxt, .link-pill #canNetTxt { max-width: 22em; } .link-pill #pillEcatTxt { max-width: 14em; } .link-pill #pillBleTxt { max-width: 18em; } /* ================= 主布局 ================= */ .main-layout { display: grid; grid-template-columns: 400px 1fr; gap: var(--gap); align-items: start; } /* ================= 卡片 ================= */ .card { position: relative; background: linear-gradient(170deg, var(--bg-card) 0%, rgba(15,23,38,.92) 100%); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow-card); } .card + .card { margin-top: var(--gap); } /* 子系统色带:卡片顶部 2px 渐变,由修饰类决定颜色 */ .card::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, var(--card-accent, var(--esp32)), transparent 85%); opacity: .7; pointer-events: none; } .card--esp32 { --card-accent: var(--esp32); --card-accent-dim: var(--esp32-dim); } .card--gd32 { --card-accent: var(--gd32); --card-accent-dim: var(--gd32-dim); } .card--ble { --card-accent: var(--ble); --card-accent-dim: var(--ble-dim); } .card--can { --card-accent: var(--can); --card-accent-dim: var(--can-dim); } .card--motor { --card-accent: var(--motor); --card-accent-dim: var(--motor-dim); } .card--mem { --card-accent: var(--mem); --card-accent-dim: var(--mem-dim); } .card--status { --card-accent: var(--green); --card-accent-dim: var(--can-dim); } .card-head { display: flex; align-items: center; gap: 10px; padding: 14px 18px; border-bottom: 1px solid var(--border-subtle); font-family: var(--font-display); font-size: 13px; font-weight: 600; color: var(--text); letter-spacing: .05em; cursor: pointer; user-select: none; } /* 图标芯片:圆角方块底色取自卡片子系统色 */ .card-head .ico { width: 28px; height: 28px; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 15px; border-radius: 8px; background: var(--card-accent-dim, var(--esp32-dim)); border: 1px solid color-mix(in srgb, var(--card-accent, var(--esp32)) 30%, transparent); } .card-head-btn { margin-left: auto; padding: 4px 10px; position: relative; z-index: 2; } .card-body { padding: 17px 18px; } .card.collapsed .card-body { display: none; } .card-head .collapse-arrow { margin-left: auto; font-size: 10px; color: var(--text-label); transition: transform .2s; } .card.collapsed .collapse-arrow { transform: rotate(-90deg); } /* ================= 表单 ================= */ .form-row { margin-bottom: 14px; } .form-row:last-child { margin-bottom: 0; } .form-label { display: block; font-family: var(--font-display); font-size: 11px; color: var(--text-label); text-transform: uppercase; letter-spacing: .12em; margin-bottom: 7px; } .input, select, textarea { width: 100%; padding: 10px 13px; font-size: 13px; font-family: var(--font-mono); background: var(--bg-input); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius-sm); outline: none; transition: border-color .18s, box-shadow .18s, background .18s; } .input::placeholder { color: var(--text-label); } .input:focus, select:focus, textarea:focus { border-color: var(--esp32); background: color-mix(in srgb, var(--esp32) 5%, var(--bg-input)); box-shadow: 0 0 0 3px var(--esp32-dim); } select { cursor: pointer; } select option { background: var(--bg-panel); color: var(--text); } .input-group { display: flex; gap: 8px; } .input-group .input { flex: 1; } /* ================= 按钮 ================= */ /* 共享 .btn chrome(OTA/CAN 等页)。电机页主题按钮在 motor.css,勿互相复制。 */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 7px; padding: 9px 17px; border: none; border-radius: var(--radius-sm); font-family: var(--font-display); font-size: 13px; font-weight: 600; letter-spacing: .03em; cursor: pointer; white-space: nowrap; transition: transform .16s, box-shadow .2s, background .2s, border-color .2s, color .2s, opacity .2s; } .btn:hover:not(:disabled) { transform: translateY(-1px); } .btn:active:not(:disabled) { transform: translateY(0) scale(.97); } .btn:disabled { opacity: .38; cursor: not-allowed; transform: none; } /* 主操作 = ESP32 青绿 */ .btn-primary { background: linear-gradient(135deg, var(--esp32) 0%, #14b8a6 100%); color: #04231e; box-shadow: 0 4px 16px rgba(47,216,193,.28), inset 0 1px 0 rgba(255,255,255,.25); } .btn-primary:hover:not(:disabled) { box-shadow: 0 6px 22px rgba(47,216,193,.4), inset 0 1px 0 rgba(255,255,255,.3); } .btn-success { background: linear-gradient(135deg, var(--green) 0%, #16a374 100%); color: #03231a; box-shadow: 0 4px 16px rgba(61,220,142,.25), inset 0 1px 0 rgba(255,255,255,.22); } .btn-danger { background: linear-gradient(135deg, var(--red) 0%, #dc4c4c 100%); color: #fff; box-shadow: 0 4px 16px rgba(255,107,107,.25), inset 0 1px 0 rgba(255,255,255,.2); } .btn-outline { background: rgba(255,255,255,.02); color: var(--text-dim); border: 1px solid var(--border-strong); } .btn-outline:hover:not(:disabled) { background: var(--bg-hover); color: var(--text); border-color: #3d5a84; } .btn-ghost { background: transparent; color: var(--text-dim); } .btn-ghost:hover:not(:disabled) { background: var(--bg-hover); color: var(--text); } .btn-block { width: 100%; justify-content: center; } .btn-sm { padding: 6px 12px; font-size: 12px; } /* ---- 多色子系统页签(关键)---- console-mode.js 用 btn-primary / btn-outline 表达"选中/未选中", 这里按按钮 id 把"选中态"染成各自子系统色: WiFi=青绿 · BLE=蓝 · ESP32=青绿 · GD32=琥珀。 */ #modeBleBtn.btn-primary { background: linear-gradient(135deg, var(--ble) 0%, #3b82f6 100%); color: #04142b; box-shadow: 0 4px 16px rgba(95,168,255,.3), inset 0 1px 0 rgba(255,255,255,.25); } #modeEcatBtn.btn-primary { background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%); color: #0f0a1f; box-shadow: 0 4px 16px rgba(139,92,246,.3), inset 0 1px 0 rgba(255,255,255,.25); } #modeWifiBtn.btn-outline { border-color: rgba(47,216,193,.45); color: var(--esp32); } #modeBleBtn.btn-outline { border-color: rgba(95,168,255,.45); color: var(--ble); } #modeEcatBtn.btn-outline { border-color: rgba(139,92,246,.45); color: #a78bfa; } #modeWifiBtn.btn-outline:hover:not(:disabled) { border-color: var(--esp32); background: rgba(47,216,193,.08); color: var(--esp32); } #modeBleBtn.btn-outline:hover:not(:disabled) { border-color: var(--ble); background: rgba(95,168,255,.08); color: var(--ble); } #modeEcatBtn.btn-outline:hover:not(:disabled) { border-color: #8b5cf6; background: rgba(139,92,246,.08); color: #a78bfa; } /* 升级目标:左右两个等宽扁按钮(GD32 琥珀 · ESP32 青绿) */ .target-tab-row { display: flex; gap: 8px; margin-bottom: 14px; } .target-tab-btn { flex: 1 1 0; min-width: 0; min-height: 40px; padding: 10px 14px; font-size: 14px; font-weight: 700; letter-spacing: .04em; flex-direction: row; gap: 8px; border-radius: var(--radius-sm); } .target-tab-btn .target-tab-ico { font-size: 15px; line-height: 1; opacity: .9; } .target-tab-btn .target-tab-txt { line-height: 1.1; } .target-tab-btn.btn-outline { border-width: 1.5px; background: rgba(255,255,255,.03); } #targetGd32Tab.btn-primary { background: linear-gradient(135deg, var(--gd32) 0%, #f59e0b 100%); color: #2b1d02; box-shadow: 0 4px 16px rgba(252,191,63,.3), inset 0 1px 0 rgba(255,255,255,.3); } #targetEsp32Tab.btn-primary { background: linear-gradient(135deg, var(--esp32) 0%, #14b8a6 100%); color: #04231e; box-shadow: 0 4px 16px rgba(47,216,193,.28), inset 0 1px 0 rgba(255,255,255,.25); } /* 未选中时用各自子系统的淡描边,避免两个灰按钮看不出身份 */ #targetGd32Tab.btn-outline { border-color: rgba(252,191,63,.45); color: var(--gd32); } #targetEsp32Tab.btn-outline { border-color: rgba(47,216,193,.45); color: var(--esp32); } #targetGd32Tab.btn-outline:hover:not(:disabled) { border-color: var(--gd32); background: rgba(252,191,63,.08); color: var(--gd32); } #targetEsp32Tab.btn-outline:hover:not(:disabled) { border-color: var(--esp32); background: rgba(47,216,193,.08); color: var(--esp32); } #ppEcatOta.active { color: #2b1d02; background: linear-gradient(135deg, var(--gd32), #f59e0b); box-shadow: 0 2px 12px rgba(252,191,63,.35); } /* EtherCAT 连接说明 + 探测:与卡片内其他控件同一套字重/字距 */ .ecat-mode-note { margin: 0 0 12px; padding: 10px 12px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle); background: rgba(252,191,63,.06); font-size: 12px; line-height: 1.7; color: var(--text-dim); } .ecat-mode-note b { color: var(--gd32); font-weight: 600; } #bootEcatProbeBtn { margin-bottom: 10px; border-color: rgba(252,191,63,.4); color: var(--gd32); background: rgba(252,191,63,.05); } #bootEcatProbeBtn:hover:not(:disabled) { border-color: var(--gd32); background: rgba(252,191,63,.12); color: var(--gd32-hi); } /* ---- APP ↔ BOOT 跳转控制台 ---- */ .jump-role-panel { display: grid; grid-template-columns: 1fr; gap: 10px; margin-bottom: 12px; } @media (min-width: 420px) { .jump-role-panel { grid-template-columns: 140px 1fr; align-items: stretch; } } .jump-role-badge { display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 2px; min-height: 92px; padding: 12px 10px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg-elevated); text-align: center; transition: border-color .2s, background .2s, box-shadow .2s; cursor: pointer; user-select: none; } .jump-role-badge:hover { border-color: var(--gd32); box-shadow: 0 0 0 3px rgba(252,191,63,.12); } .jump-role-badge:active { transform: scale(0.98); } .jump-role-badge .jump-role-label { font-family: var(--font-display); font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--text-label); } .jump-role-badge strong { font-family: var(--font-display); font-size: 18px; font-weight: 700; letter-spacing: .04em; color: var(--text); } .jump-role-badge .jump-role-sw { font-family: var(--font-mono); font-size: 11px; color: var(--text-dim); } .jump-role-badge[data-role="app"] { border-color: rgba(47,216,193,.55); background: rgba(47,216,193,.1); box-shadow: 0 0 0 3px rgba(47,216,193,.08); } .jump-role-badge[data-role="app"] strong { color: var(--esp32); } .jump-role-badge[data-role="boot"] { border-color: rgba(252,191,63,.6); background: rgba(252,191,63,.12); box-shadow: 0 0 0 3px rgba(252,191,63,.1); } .jump-role-badge[data-role="boot"] strong { color: var(--gd32); } .jump-role-badge[data-role="unknown"] strong { color: var(--text-dim); font-size: 15px; } .jump-role-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; font-size: 12px; } .jump-role-meta > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; } .jump-role-meta span { color: var(--text-label); font-family: var(--font-display); font-size: 10px; letter-spacing: .08em; text-transform: uppercase; } .jump-role-meta b { font-family: var(--font-mono); font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .jump-action-row { display: grid; grid-template-columns: 88px 1fr; gap: 8px; margin-top: 4px; } .jump-btn-primary { min-height: 44px; background: linear-gradient(135deg, var(--gd32) 0%, #f59e0b 100%); color: #2b1d02 !important; font-weight: 700; box-shadow: 0 4px 14px rgba(252,191,63,.28); } .jump-btn-primary:hover:not(:disabled) { filter: brightness(1.05); color: #2b1d02 !important; } .jump-btn-secondary { min-height: 44px; } .jump-btn-test { margin-top: 8px; min-height: 40px; border-color: rgba(252,191,63,.35); color: var(--gd32); background: transparent; } .jump-btn-test:hover:not(:disabled) { background: rgba(252,191,63,.1); border-color: var(--gd32); } .jump-btn-test:disabled { opacity: .45; cursor: not-allowed; } .jump-history { margin-top: 12px; border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); background: var(--bg-card); overflow: hidden; } .jump-history-head { padding: 8px 12px; border-bottom: 1px solid var(--border-subtle); font-family: var(--font-display); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--text-label); } .jump-history-list { max-height: 148px; overflow-y: auto; padding: 6px 0; } .jump-history-empty { padding: 14px 12px; font-size: 12px; color: var(--text-dim); text-align: center; } .jump-history-item { display: flex; align-items: flex-start; gap: 8px; padding: 6px 12px; font-size: 12px; line-height: 1.45; font-family: var(--font-mono); } .jump-history-item + .jump-history-item { border-top: 1px dashed var(--border-subtle); } .jump-history-item .jh-tag { flex-shrink: 0; min-width: 42px; text-align: center; padding: 1px 6px; border-radius: 999px; font-size: 10px; font-weight: 700; background: var(--bg-input); color: var(--text-dim); } .jump-history-item.ok .jh-tag { background: rgba(61,220,142,.18); color: var(--can-hi); } .jump-history-item.err .jh-tag { background: rgba(255,107,107,.16); color: var(--red); } .jump-history-item.info .jh-tag { background: rgba(47,216,193,.14); color: var(--esp32); } .jump-history-item .jh-msg { flex: 1; color: var(--text); word-break: break-all; } .jump-history-item .jh-time { flex-shrink: 0; color: var(--text-dim); font-size: 10px; } /* GD32 OTA 大按钮 = 琥珀(区别于 ESP32 青绿) */ #uploadGd32Btn { background: linear-gradient(135deg, var(--gd32) 0%, #f59e0b 100%); color: #2b1d02; box-shadow: 0 6px 20px rgba(252,191,63,.3), inset 0 1px 0 rgba(255,255,255,.3); } #bootEcatRunBtn { background: linear-gradient(135deg, var(--gd32) 0%, #f59e0b 100%); color: #2b1d02; box-shadow: 0 6px 20px rgba(252,191,63,.3), inset 0 1px 0 rgba(255,255,255,.3); } #bootEcatRunBtn:disabled { opacity: .38; } #uploadEsp32Btn { box-shadow: 0 6px 20px rgba(47,216,193,.32), inset 0 1px 0 rgba(255,255,255,.28); } /* ================= 连接状态条 / 提示 ================= */ .conn-indicator { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-radius: var(--radius-sm); background: var(--bg-elevated); border: 1px solid var(--border); font-size: 13px; font-weight: 500; } .conn-indicator .light { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; background: var(--text-label); } .conn-indicator.ok .light { background: var(--green); box-shadow: 0 0 8px rgba(61,220,142,.6); } .conn-indicator.err .light { background: var(--red); box-shadow: 0 0 8px rgba(255,107,107,.5); } .conn-indicator.warn .light { background: var(--amber); box-shadow: 0 0 8px rgba(252,191,63,.45); } .helper-note { margin-top: 10px; padding: 10px 13px; border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--bg-elevated); font-size: 12px; line-height: 1.65; color: var(--text-dim); } .helper-note.ok { border-color: rgba(61,220,142,.3); background: rgba(61,220,142,.07); color: #a9f0c6; } .helper-note.warn { border-color: rgba(252,191,63,.3); background: rgba(252,191,63,.07); color: #fbe3ae; } .helper-note.err { border-color: rgba(255,107,107,.3); background: rgba(255,107,107,.07); color: #ffc4c4; } .helper-note code { color: var(--esp32-hi); font-family: var(--font-mono); } /* ================= 右侧滑出抽屉:堆与任务栈 ================= */ .sys-drawer { --sys-drawer-panel-w: min(390px, calc(100vw - 48px)); --sys-drawer-peek: 38px; position: fixed; top: 76px; right: 0; z-index: 80; width: calc(var(--sys-drawer-panel-w) + var(--sys-drawer-peek)); height: calc(100vh - 92px); max-height: calc(100vh - 92px); display: flex; flex-direction: row; align-items: stretch; transform: translateX(var(--sys-drawer-panel-w)); transition: transform .3s cubic-bezier(.22, 1, .36, 1); filter: drop-shadow(-8px 0 24px rgba(2,5,12,.5)); } .sys-drawer:hover, .sys-drawer:focus-within { transform: translateX(0); } .sys-drawer-rail { flex: 0 0 var(--sys-drawer-peek); width: var(--sys-drawer-peek); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 14px 4px; background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-card) 100%); border: 1px solid var(--border); border-right: none; border-radius: var(--radius) 0 0 var(--radius); cursor: default; } .sys-drawer-rail-ico { font-size: 16px; line-height: 1; } .sys-drawer-rail-text { writing-mode: vertical-rl; text-orientation: mixed; font-family: var(--font-display); font-size: 11px; font-weight: 600; letter-spacing: .18em; color: var(--text-dim); } .sys-drawer:hover .sys-drawer-rail-text, .sys-drawer:focus-within .sys-drawer-rail-text { color: var(--mem-hi); } .sys-drawer-panel { flex: 1 1 var(--sys-drawer-panel-w); width: var(--sys-drawer-panel-w); min-width: 0; height: 100%; margin: 0 !important; display: flex; flex-direction: column; border-radius: 0; overflow: hidden; background: var(--bg-card); border: 1px solid var(--border); border-left: none; border-right: none; } .sys-drawer-head { cursor: default; flex-shrink: 0; } .sys-drawer-head .card-head-btn { margin-left: auto; } .sys-drawer-body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }