说一句,就调好了
-请先在指令框输入需要固化的功能组合
正在检查 CAN 适配器与 IAR 固件配置
@@ -554,9 +544,9 @@全部功能模式
+自定义模式
修改自然语言后需要重新生成指令,才能点击完成永久保存。
diff --git a/apps/web-console/app.js b/apps/web-console/app.js index 89a71a2..1362e6e 100755 --- a/apps/web-console/app.js +++ b/apps/web-console/app.js @@ -70,10 +70,10 @@ const state = { physicalStatePoller: null, physicalStateSyncPromise: null, passengerStateSyncPromise: null, + wiredCanProbe: { passenger: false, vehicle: false }, + feedbackSignals: { passenger: false, vehicle: false }, pendingModeProfileId: null, modeRunTimer: null, - modeDraftCommand: "", - lastSuccessfulUtterance: "", modeEditor: null, directCanCommandPending: false, }; @@ -133,6 +133,13 @@ function renderCabinLiveStatus(networkStatus = state.connectionStatus) { setSummarySystem("链路未配置", "waiting"); return; } + if (!transportConnectionOnline(state.controlTransport, selected)) { + indicator.dataset.state = "offline"; + label.textContent = `${transportLabel} · 信号链路断开`; + indicator.title = selected?.state_sync?.last_error || "尚未检测到车辆通信设备"; + setSummarySystem("链路断开", "warning"); + return; + } const snapshot = state.vehicleStateSnapshot; const observedIds = Array.isArray(snapshot?.observedFrameIds) @@ -4323,6 +4330,54 @@ function selectedVehicleTransport() { || null; } +function wiredCanConnectionOnline(runtime = state.runtime?.vehicle_can) { + return runtime?.state_sync?.state === "online" + || state.wiredCanProbe.passenger === true + || state.wiredCanProbe.vehicle === true; +} + +function transportConnectionOnline(id, runtime) { + return id === "wired_can" + ? wiredCanConnectionOnline(runtime) + : runtime?.state_sync?.state === "online"; +} + +function setFeedbackSignal(scope, connected, requestedTransport = state.controlTransport) { + if (requestedTransport !== state.controlTransport) return; + const next = connected === true; + const datasetKey = `${scope}FeedbackState`; + const datasetValue = next ? "online" : "offline"; + if (state.feedbackSignals[scope] === next + && document.body?.dataset[datasetKey] === datasetValue) return; + state.feedbackSignals[scope] = next; + if (document.body) { + document.body.dataset[datasetKey] = datasetValue; + } + window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-feedback-state", { + detail: { scope, connected: next, control_transport: requestedTransport }, + })); +} + +function updateWiredCanProbe(scope, connected, requestedTransport) { + if (requestedTransport !== "wired_can" || requestedTransport !== state.controlTransport) return; + const next = connected === true; + if (state.wiredCanProbe[scope] === next) return; + state.wiredCanProbe[scope] = next; + renderControlTransport(); +} + +function resetTransportConnectionEvidence() { + state.wiredCanProbe.passenger = false; + state.wiredCanProbe.vehicle = false; + ["passenger", "vehicle"].forEach((scope) => { + state.feedbackSignals[scope] = false; + if (document.body) document.body.dataset[`${scope}FeedbackState`] = "offline"; + }); + window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-feedback-state", { + detail: { scope: "all", connected: false, control_transport: state.controlTransport }, + })); +} + function isHardwareExecutionReady() { return Boolean( state.runtime?.hardware?.configured || selectedVehicleTransport()?.configured, @@ -4370,9 +4425,14 @@ function renderControlTransport() { // shown as offline, but it must never disable or silently change the // operator's wired/wireless selection. button.disabled = false; - button.dataset.connectionState = ["online", "ready"].includes(runtime?.state_sync?.state) + const connectionState = transportConnectionOnline(id, runtime) ? "online" : "offline"; + button.dataset.connectionState = connectionState; + button.setAttribute( + "aria-label", + `${id === "wireless_mqtt" ? "无线 CAN" : "有线 CAN"},信号链路${connectionState === "online" ? "正常" : "断开"}`, + ); button.title = runtime?.state_configured === true ? runtime?.state_sync?.last_error || "" : `${id === "wireless_mqtt" ? "无线 MQTT" : "有线 CAN"} 当前未配置(仍可手动选择)`; @@ -4388,14 +4448,10 @@ function renderControlTransport() { return; } if (state.controlTransport === "wireless_mqtt") { - const connection = selected?.state_sync?.state === "online" ? "已连接" : "未连接"; + const connection = transportConnectionOnline("wireless_mqtt", selected) ? "已连接" : "未连接"; note.textContent = `MQTT ${connection} · 状态 ${selected?.condition_topic || "n7/n7-headunit/can/state"} · 控制 ${selected?.control_topic || "n7/n7-headunit/can/command"}`; } else { - const connection = selected?.state_sync?.state === "online" - ? "已连接" - : selected?.state_sync?.state === "ready" - ? "已配置" - : "未连接"; + const connection = transportConnectionOnline("wired_can", selected) ? "已连接" : "未连接"; const dualChannel = selected?.target_policy === "dual_channel_wired_can0_passenger_can1_vehicle"; note.textContent = dualChannel @@ -4414,6 +4470,7 @@ function selectControlTransport(transport) { sessionStorage.setItem("cognitive-agent.conversation-id", state.conversationId); state.seatFeedbackSources.passenger = "unavailable"; state.seatFeedbackSources.driver = "unavailable"; + resetTransportConnectionEvidence(); resetVehicleStatusDisplay(); } state.vehicleStateSnapshot = null; @@ -4510,7 +4567,7 @@ function selectExecutionMode(mode, { remember = true } = {}) { : simulatedHardware ? "运行故障仿真" : "生成并执行"; } if (remember && mode === "hardware" && !hardwareExecutionReady) { - showToast(`已切换到真实台架;当前不可执行:${hardwareExecutionIssue()}`); + showToast(`已切换到真实下发;当前不可执行:${hardwareExecutionIssue()}`); } updateSeatFeedbackModeLabel(); refreshIcons(); @@ -4811,6 +4868,43 @@ function createModeGalleryCard(mode) { return shell; } +function createModeGalleryAddCard() { + const shell = document.createElement("article"); + shell.className = "mode-gallery-card-shell mode-gallery-add-card-shell"; + const button = document.createElement("button"); + button.className = "mode-gallery-card mode-gallery-add-card"; + button.type = "button"; + button.dataset.modeCreate = "true"; + + const iconBox = document.createElement("span"); + iconBox.className = "mode-gallery-card-icon"; + const icon = document.createElement("i"); + icon.dataset.lucide = "bookmark-plus"; + icon.dataset.icon = "inline-start"; + iconBox.append(icon); + + const copy = document.createElement("span"); + copy.className = "mode-gallery-card-copy"; + const heading = document.createElement("span"); + heading.className = "mode-gallery-card-heading"; + const name = document.createElement("strong"); + name.textContent = "新增模式"; + const stateLabel = document.createElement("small"); + stateLabel.className = "mode-gallery-state"; + stateLabel.textContent = "点击创建"; + heading.append(name, stateLabel); + const description = document.createElement("span"); + description.className = "mode-gallery-card-description"; + description.textContent = "用自然语言描述需要联动的车辆功能"; + const meta = document.createElement("span"); + meta.className = "mode-gallery-card-meta"; + meta.textContent = "解析 · 校验 · 生成控制指令"; + copy.append(heading, description, meta); + button.append(iconBox, copy); + shell.append(button); + return shell; +} + function populateModeGallery() { const container = $("#modeGalleryGroups"); if (!container) return; @@ -4837,14 +4931,8 @@ function populateModeGallery() { const grid = document.createElement("div"); grid.className = "mode-gallery-grid"; - if (group.modes.length) { - group.modes.forEach((mode) => grid.append(createModeGalleryCard(mode))); - } else { - const empty = document.createElement("div"); - empty.className = "mode-gallery-empty"; - empty.innerHTML = '还没有自定义模式可在主界面根据当前指令创建'; - grid.append(empty); - } + if (group.id === "personal") grid.append(createModeGalleryAddCard()); + group.modes.forEach((mode) => grid.append(createModeGalleryCard(mode))); section.append(grid); container.append(section); }); @@ -4854,38 +4942,24 @@ function populateModeGallery() { refreshIcons(); } -function updateModeRailNavigation() { - const chips = $(".mode-chips"); - const previous = $("#modeRailPrev"); - const next = $("#modeRailNext"); - if (!chips || !previous || !next) return; - const maxScroll = Math.max(0, chips.scrollWidth - chips.clientWidth); - previous.disabled = maxScroll <= 2 || chips.scrollLeft <= 2; - next.disabled = maxScroll <= 2 || chips.scrollLeft >= maxScroll - 2; +function setModeRailExpanded(expanded) { + const toggle = $("#modeRailToggle"); + const content = $("#modeRailContent"); + if (!toggle || !content) return; + const open = expanded === true; + toggle.setAttribute("aria-expanded", String(open)); + content.hidden = !open; } -function scrollModeRail(direction) { - const chips = $(".mode-chips"); - if (!chips) return; - const buttons = $$("button[data-mode-profile-id]", chips); - const viewport = chips.getBoundingClientRect(); - const target = direction > 0 - ? buttons.find((button) => button.getBoundingClientRect().right > viewport.right + 2) - : [...buttons].reverse().find((button) => button.getBoundingClientRect().left < viewport.left - 2); - if (target) { - const targetLeft = direction > 0 - ? target.offsetLeft - 6 - : target.offsetLeft + target.offsetWidth - chips.clientWidth + 6; - chips.scrollTo({ left: Math.max(0, targetLeft), behavior: "smooth" }); - } else { - chips.scrollBy({ left: direction * Math.max(220, chips.clientWidth * 0.72), behavior: "smooth" }); - } +function setModeRailSelection(mode) { + const selection = $("#modeRailSelection"); + if (selection && mode) selection.textContent = String(mode.name || mode.id || "常用模式"); } function openModeGallery() { const backdrop = $("#modeGalleryBackdrop"); if (!backdrop) return; - closeSaveModeComposer(); + setModeRailExpanded(false); backdrop.hidden = false; document.body.classList.add("mode-gallery-open"); $("#modeGalleryToggle").setAttribute("aria-expanded", "true"); @@ -5318,12 +5392,16 @@ function openModeEditor(modeProfileId) { : null; state.modeEditor = { modeId: mode.id, + isNew: false, sourceRequestId: String(flow?.source_request_id || ""), executedInstruction: flow ? instruction : "", flow, + restoreSelector: `[data-mode-edit="${CSS.escape(mode.id)}"]`, }; + $("#modeEditorTitle").textContent = "编辑功能模式"; $("#modeEditorName").value = mode.name || ""; $("#modeEditorInstruction").value = instruction; + $("#modeEditorComplete").innerHTML = '完成'; $("#modeEditorStatus").textContent = flow ? "已载入永久保存的自然语言和控制指令流,可修改后重新生成。" : "修改自然语言后需要重新生成指令,才能点击完成永久保存。"; @@ -5338,18 +5416,44 @@ function openModeEditor(modeProfileId) { refreshIcons(); } +function openModeEditorForCreate({ restoreSelector = "#saveModeToggle" } = {}) { + const backdrop = $("#modeEditorBackdrop"); + if (!backdrop) return; + const instruction = $("#utterance")?.value.trim() || ""; + state.modeEditor = { + modeId: null, + isNew: true, + sourceRequestId: "", + executedInstruction: "", + flow: null, + restoreSelector, + }; + $("#modeEditorTitle").textContent = "新增功能模式"; + $("#modeEditorName").value = ""; + $("#modeEditorInstruction").value = instruction; + $("#modeEditorStatus").textContent = "输入模式名称和自然语言,生成控制指令后即可添加模式卡片。"; + $("#modeEditorComplete").innerHTML = '添加模式'; + setModeEditorFlowExpanded(false); + backdrop.hidden = false; + document.body.classList.add("mode-editor-open"); + renderModeEditorFlow(); + window.requestAnimationFrame(() => { + backdrop.classList.add("open"); + (instruction ? $("#modeEditorName") : $("#modeEditorInstruction")).focus(); + }); + refreshIcons(); +} + function closeModeEditor({ restoreFocus = true } = {}) { const backdrop = $("#modeEditorBackdrop"); - const modeId = state.modeEditor?.modeId; + const restoreSelector = state.modeEditor?.restoreSelector; if (!backdrop || backdrop.hidden) return; backdrop.classList.remove("open"); backdrop.hidden = true; setModeEditorFlowExpanded(false); document.body.classList.remove("mode-editor-open"); state.modeEditor = null; - if (restoreFocus && modeId) { - $(`[data-mode-edit="${CSS.escape(modeId)}"]`)?.focus(); - } + if (restoreFocus && restoreSelector) $(restoreSelector)?.focus(); } function invalidateModeEditorFlow() { @@ -5373,7 +5477,7 @@ async function executeModeEditorInstruction() { const execute = $("#modeEditorExecute"); execute.disabled = true; execute.classList.add("loading"); - execute.innerHTML = '生成中…'; + execute.innerHTML = '生成中…'; $("#modeEditorComplete").disabled = true; $("#modeEditorStatus").textContent = "正在复用主页解析流程生成控制指令;不会执行设备…"; refreshIcons(); @@ -5403,7 +5507,7 @@ async function executeModeEditorInstruction() { } finally { execute.disabled = false; execute.classList.remove("loading"); - execute.innerHTML = '生成指令'; + execute.innerHTML = '生成指令'; refreshIcons(); } } @@ -5417,6 +5521,12 @@ async function completeModeEditor() { $("#modeEditorName").focus(); return; } + if (editor.isNew && !name.endsWith("模式") && name.length > 62) { + showToast("模式名称添加“模式”后不能超过 64 个字符"); + $("#modeEditorName").focus(); + return; + } + const modeName = editor.isNew && !name.endsWith("模式") ? `${name}模式` : name; if ( !editor.sourceRequestId || editor.executedInstruction !== instruction @@ -5428,14 +5538,17 @@ async function completeModeEditor() { const complete = $("#modeEditorComplete"); complete.disabled = true; complete.classList.add("loading"); - complete.innerHTML = '保存中…'; + complete.innerHTML = '保存中…'; refreshIcons(); try { - const response = await apiRequest(`/api/mode-profiles/${encodeURIComponent(editor.modeId)}`, { + const endpoint = editor.isNew + ? "/api/mode-profiles" + : `/api/mode-profiles/${encodeURIComponent(editor.modeId)}`; + const response = await apiRequest(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - name, + name: modeName, instruction, source_request_id: editor.sourceRequestId, vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id, @@ -5449,14 +5562,16 @@ async function completeModeEditor() { if (!response.ok) throw new Error(payload.message || `HTTP ${response.status}`); await loadRuntime(); closeModeEditor({ restoreFocus: false }); - showToast(`${payload.mode_profile?.name || name}已永久保存到模式卡片`); + showToast(`${payload.mode_profile?.name || modeName}已永久保存到模式卡片`); } catch (error) { $("#modeEditorStatus").textContent = error.message || "模式保存失败。"; showToast(error.message || "模式保存失败"); renderModeEditorFlow(); } finally { complete.classList.remove("loading"); - complete.innerHTML = '完成'; + complete.innerHTML = editor.isNew + ? '添加模式' + : '完成'; refreshIcons(); } } @@ -5573,10 +5688,8 @@ function populateModeChips() { group.append(divider); } dynamicButtons.forEach((button) => group.append(button)); - if (userButtons.length) group.scrollLeft = 0; populateModeGallery(); refreshIcons(); - window.requestAnimationFrame(updateModeRailNavigation); } async function deleteUserModeProfile(modeProfileId) { @@ -5724,6 +5837,8 @@ function applyModeCommand(command, modeProfileId = null) { return; } const mode = modeProfiles().find((item) => item.id === modeProfileId); + setModeRailSelection(mode); + setModeRailExpanded(false); if (mode?.command_flow?.source_request_id) { void executeSavedModeProfile(mode); return; @@ -5735,99 +5850,6 @@ function applyModeCommand(command, modeProfileId = null) { $("#requestForm").requestSubmit(); } -function updateModeDraftPreview() { - const command = $("#utterance").value.trim(); - state.modeDraftCommand = command; - const source = $("#saveModeSource"); - source.textContent = command || "请先在指令框输入需要固化的功能组合"; - source.classList.toggle("empty", !command); -} - -function toggleSaveModeComposer() { - const composer = $("#saveModeComposer"); - composer.hidden = !composer.hidden; - if (!composer.hidden) { - updateModeDraftPreview(); - $("#saveModeName").focus(); - } -} - -function closeSaveModeComposer() { - $("#saveModeComposer").hidden = true; - state.modeDraftCommand = ""; -} - -function requestCommandAndWait(utterance) { - return new Promise((resolve) => { - const button = $("#runButton"); - if (!button || button.disabled) { - resolve({ result: null, error: { message: "上一条指令仍在执行" } }); - return; - } - const timeoutMs = requestTimeoutForExecutionMode(state.executionMode) + 5000; - const onComplete = (event) => { - if (event.detail?.utterance !== utterance) return; - window.clearTimeout(timer); - window.removeEventListener("cognitive-agent:request-complete", onComplete); - resolve(event.detail); - }; - const timer = window.setTimeout(() => { - window.removeEventListener("cognitive-agent:request-complete", onComplete); - resolve({ result: null, error: { message: "指令执行等待超时" } }); - }, timeoutMs); - window.addEventListener("cognitive-agent:request-complete", onComplete); - $("#utterance").value = utterance; - $("#requestForm").requestSubmit(); - }); -} - -async function saveCurrentModeProfile() { - const input = $("#saveModeName"); - const rawName = input.value.trim(); - if (!rawName) { - showToast("请输入模式名称"); - return; - } - const sourceCommand = state.modeDraftCommand || $("#utterance").value.trim(); - if (!sourceCommand) { - showToast("请先输入需要创建为模式的用户指令"); - $("#utterance").focus(); - return; - } - const modeName = rawName.endsWith("模式") ? rawName : `${rawName}模式`; - const confirm = $("#saveModeConfirm"); - confirm.disabled = true; - confirm.classList.add("loading"); - confirm.textContent = "创建中…"; - try { - if (state.lastSuccessfulUtterance !== sourceCommand) { - showToast("正在执行当前指令,成功后将创建功能模式"); - const execution = await requestCommandAndWait(sourceCommand); - if (!execution.result || !["succeeded", "partially_succeeded"].includes(execution.result.status)) { - throw new Error(execution.error?.message || "当前指令未成功执行,未创建功能模式"); - } - } - const saveCommand = `这个模式不错,固化为${modeName}`; - const saved = await requestCommandAndWait(saveCommand); - if (!saved.result?.mode_profile || saved.result.status !== "succeeded") { - throw new Error(saved.error?.message || "功能模式创建失败"); - } - await loadRuntime(); - const createdModeId = saved.result.mode_profile.id; - setModeExecutionState(createdModeId, "succeeded"); - closeSaveModeComposer(); - input.value = ""; - $("#utterance").value = sourceCommand; - showToast(`${saved.result.mode_profile.name}已创建并添加到功能模式栏`); - } catch (error) { - showToast(error.message || "功能模式创建失败"); - } finally { - confirm.disabled = false; - confirm.classList.remove("loading"); - confirm.textContent = "创建并添加"; - } -} - function applyRuntimeTargetPolicy(runtime) { const selectedTransport = selectedVehicleTransport(); const wirelessDualChannel = state.controlTransport === "wireless_mqtt" @@ -5903,18 +5925,14 @@ async function loadRuntime() { syncReasoningControls(); $("#matrixCount").textContent = `${state.runtime.matrix.messages} / ${state.runtime.matrix.signals}`; const hardwareButton = $("#hardwareMode"); - // “真实台架”是操作员手动选择的执行目标。硬件暂不可用时仍允许 + // “真实下发”是操作员手动选择的执行目标。硬件暂不可用时仍允许 // 选择该目标并展示诊断信息;真正的控制提交继续由后端安全门拦截。 hardwareButton.disabled = false; hardwareButton.dataset.executionState = hardwareExecutionReady ? "available" : "blocked"; hardwareButton.title = hardwareExecutionReady - ? "真实台架已通过后端执行配置校验" - : `可手动选择真实台架;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`; + ? "真实下发已通过后端执行配置校验" + : `可手动选择真实下发;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`; const simulation = state.runtime.simulated_hardware || {}; - $("#simulationMode").disabled = !simulation.enabled; - $("#simulationMode").title = simulation.enabled - ? "开发专用模拟台架,不连接物理 CAN" - : "通过安卓联调脚本或环境变量启用模拟台架"; const profileText = (hardware.firmware_profiles || []).join(" / ") || "未选择 profile"; const flashText = hardware.auto_flash_enabled ? "自动烧录已启用" : "自动烧录未启用"; const concurrencyText = hardware.concurrency_policy === "single_flight_fail_fast" @@ -5933,7 +5951,7 @@ async function loadRuntime() { ? `${mqttDualChannel ? "MQTT CAN0 副驾座椅 · MQTT CAN1 其余车辆" : `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s`} · ${vehicleRouteText}${mqttDualChannel ? " · 远端自动烧录禁用" : ` · ${profileText} · ${flashText}`}${accessText}${concurrencyText}` : seatHardwareReady ? `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText} · 车端 CAN 未启用,其他领域保持虚拟执行${accessText}${concurrencyText}` - : `可手动选择真实台架;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`; + : `可手动选择真实下发;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`; $("#hardwareAliasNote").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady ? `${mqttSelected ? "MQTT" : "CH1"} → 车身 CAN / 主驾座椅 · CH0 与副驾 T2G 路径禁止` : linkedHardwareReady @@ -5967,7 +5985,11 @@ async function synchronizePassengerSeatState() { const stateReady = mqttSelected ? selectedVehicleTransport()?.state_configured : state.runtime?.hardware?.configured; - if (!stateReady) return false; + if (!stateReady) { + updateWiredCanProbe("passenger", false, requestedTransport); + setFeedbackSignal("passenger", false, requestedTransport); + return false; + } const query = new URLSearchParams({ conversation_id: state.conversationId, vehicle_id: state.vehicleId || state.runtime.default_vehicle_id || "", @@ -5985,6 +6007,8 @@ async function synchronizePassengerSeatState() { if (!response.ok) throw new Error(`HTTP ${response.status}`); const payload = await response.json(); if (requestedTransport !== state.controlTransport) return false; + updateWiredCanProbe("passenger", true, requestedTransport); + setFeedbackSignal("passenger", true, requestedTransport); renderSeatState({ passenger: payload.actuator_state?.passenger || {} }); state.seatFeedbackSources.passenger = payload.complete ? payload.source === "wireless_mqtt_ch0" ? "wireless_mqtt_ch0" : "physical_can" @@ -5993,6 +6017,8 @@ async function synchronizePassengerSeatState() { updateSeatFeedbackModeLabel(); return true; } catch (_error) { + updateWiredCanProbe("passenger", false, requestedTransport); + setFeedbackSignal("passenger", false, requestedTransport); if (!["physical_can", "wireless_mqtt_ch0"].includes(state.seatFeedbackSources.passenger)) { state.seatFeedbackSources.passenger = "unavailable"; updateSeatFeedbackModeLabel(); @@ -6002,8 +6028,12 @@ async function synchronizePassengerSeatState() { } async function synchronizeDriverSeatState() { - if (!selectedVehicleTransport()?.state_configured) return false; const requestedTransport = state.controlTransport; + if (!selectedVehicleTransport()?.state_configured) { + updateWiredCanProbe("vehicle", false, requestedTransport); + setFeedbackSignal("vehicle", false, requestedTransport); + return false; + } const query = new URLSearchParams({ conversation_id: state.conversationId, vehicle_id: state.vehicleId || state.runtime.default_vehicle_id || "", @@ -6019,6 +6049,12 @@ async function synchronizeDriverSeatState() { if (!response.ok) throw new Error(`HTTP ${response.status}`); const payload = await response.json(); if (requestedTransport !== state.controlTransport) return false; + updateWiredCanProbe("vehicle", true, requestedTransport); + setFeedbackSignal( + "vehicle", + Array.isArray(payload.observed_frame_ids) && payload.observed_frame_ids.length > 0, + requestedTransport, + ); applyAuthoritativeVehicleState(payload); state.seatFeedbackSources.driver = payload.complete ? payload.source === "wireless_mqtt_ch1" ? "wireless_mqtt_ch1" : "physical_can_ch1" @@ -6027,6 +6063,8 @@ async function synchronizeDriverSeatState() { updateSeatFeedbackModeLabel(); return true; } catch (error) { + updateWiredCanProbe("vehicle", false, requestedTransport); + setFeedbackSignal("vehicle", false, requestedTransport); state.vehicleStateSnapshot = { ...(state.vehicleStateSnapshot || {}), error: error.message || String(error), @@ -7816,7 +7854,6 @@ async function submitRequest(event) { await synchronizePhysicalSeatState(); } const completed = ["succeeded", "partially_succeeded"].includes(payload.status); - if (completed) state.lastSuccessfulUtterance = utterance; if (submittedModeProfileId) { setModeExecutionState(submittedModeProfileId, completed ? "succeeded" : "failed"); } @@ -8005,13 +8042,21 @@ document.addEventListener("DOMContentLoaded", async () => { button.dataset.modeProfileId || null, ); }); - $(".mode-chips").addEventListener( - "scroll", - () => window.requestAnimationFrame(updateModeRailNavigation), - { passive: true }, - ); - $("#modeRailPrev").addEventListener("click", () => scrollModeRail(-1)); - $("#modeRailNext").addEventListener("click", () => scrollModeRail(1)); + $("#modeRailToggle").addEventListener("click", () => { + setModeRailExpanded($("#modeRailToggle").getAttribute("aria-expanded") !== "true"); + }); + document.addEventListener("pointerdown", (event) => { + const rail = $(".mode-rail"); + const target = event.target instanceof Element ? event.target : null; + if ( + rail + && target + && !rail.contains(target) + && $("#modeRailToggle").getAttribute("aria-expanded") === "true" + ) { + setModeRailExpanded(false); + } + }); $("#modeGalleryToggle").addEventListener("click", openModeGallery); $("#modeGalleryClose").addEventListener("click", () => closeModeGallery()); $("#modeGalleryBackdrop").addEventListener("click", (event) => { @@ -8019,6 +8064,12 @@ document.addEventListener("DOMContentLoaded", async () => { }); $("#modeGalleryGroups").addEventListener("click", (event) => { const target = event.target instanceof Element ? event.target : null; + const createButton = target?.closest("button[data-mode-create]"); + if (createButton) { + event.preventDefault(); + openModeEditorForCreate({ restoreSelector: "[data-mode-create]" }); + return; + } const editButton = target?.closest("button[data-mode-edit]"); if (editButton) { event.preventDefault(); @@ -8059,21 +8110,13 @@ document.addEventListener("DOMContentLoaded", async () => { } else if (event.key === "Escape" && !$("#modeGalleryBackdrop").hidden) { event.preventDefault(); closeModeGallery(); - } - }); - window.addEventListener("resize", () => window.requestAnimationFrame(updateModeRailNavigation)); - $("#utterance").addEventListener("input", () => { - if (!$("#saveModeComposer").hidden) updateModeDraftPreview(); - }); - $("#saveModeToggle").addEventListener("click", toggleSaveModeComposer); - $("#saveModeCancel").addEventListener("click", closeSaveModeComposer); - $("#saveModeConfirm").addEventListener("click", saveCurrentModeProfile); - $("#saveModeName").addEventListener("keydown", (event) => { - if (event.key === "Enter" && !event.isComposing) { + } else if (event.key === "Escape" && $("#modeRailToggle").getAttribute("aria-expanded") === "true") { event.preventDefault(); - saveCurrentModeProfile(); + setModeRailExpanded(false); + $("#modeRailToggle").focus(); } }); + $("#saveModeToggle").addEventListener("click", () => openModeEditorForCreate()); $$(".segment[data-provider]").forEach((button) => button.addEventListener("click", () => selectProvider(button.dataset.provider))); $$('[data-reasoning-effort]').forEach((button) => button.addEventListener("click", () => selectReasoningEffort(button.dataset.reasoningEffort))); $$('[data-execution-mode]').forEach((button) => button.addEventListener("click", () => selectExecutionMode(button.dataset.executionMode))); diff --git a/apps/web-console/console-v2.css b/apps/web-console/console-v2.css index b5e43f8..3c198dc 100644 --- a/apps/web-console/console-v2.css +++ b/apps/web-console/console-v2.css @@ -2,24 +2,24 @@ Only the active remote-control layout lives here. */ .console-v2 { - --v2-accent: #0e7773; - --v2-accent-strong: #095d5a; - --v2-accent-soft: #d8f4f1; - --v2-sidebar: #d8f4f2; - --v2-sidebar-deep: #c7eeeb; - --v2-ink: #102427; - --v2-muted: #6f8082; - --v2-line: rgba(18, 85, 83, 0.14); - --v2-surface: #ffffff; - --v2-canvas: #f4f7f6; - --v2-shadow: 0 18px 50px rgba(35, 71, 74, 0.1); + --v2-accent: var(--primary); + --v2-accent-strong: color-mix(in oklch, var(--primary) 80%, var(--foreground)); + --v2-accent-soft: color-mix(in oklch, var(--primary) 10%, var(--background)); + --v2-sidebar: var(--sidebar); + --v2-sidebar-deep: var(--sidebar-accent); + --v2-ink: var(--foreground); + --v2-muted: var(--muted-foreground); + --v2-line: var(--border); + --v2-surface: var(--card); + --v2-canvas: var(--background); + --v2-shadow: 0 18px 50px oklch(0.145 0.008 326 / 10%); --v2-module-title-block: clamp(12px, 2vh, 22px); --v2-module-title-inline: clamp(20px, 2.3vw, 34px); min-width: 1180px; min-height: 620px; overflow: hidden; color: var(--v2-ink); - background: #f4f7f6; + background: var(--v2-canvas); font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; } @@ -31,10 +31,10 @@ place-content: center; justify-items: center; gap: 10px; - color: #16413f; + color: var(--foreground); background: - radial-gradient(circle at 50% 42%, rgba(80, 190, 179, 0.18), transparent 18rem), - #f4f7f6; + radial-gradient(circle at 50% 42%, color-mix(in oklch, var(--primary) 18%, transparent), transparent 18rem), + var(--background); opacity: 1; visibility: visible; transition: opacity 160ms ease, visibility 160ms ease; @@ -53,7 +53,7 @@ } .console-v2 .v2-boot-screen small { - color: #748886; + color: var(--muted-foreground); font-size: 11px; font-weight: 700; } @@ -68,14 +68,14 @@ margin-bottom: 4px; border-radius: 15px; background: linear-gradient(145deg, var(--v2-accent), var(--v2-accent-strong)); - box-shadow: 0 12px 28px rgba(14, 119, 115, 0.2); + box-shadow: 0 12px 28px color-mix(in oklch, var(--primary) 20%, transparent); } .console-v2 .v2-boot-mark i { width: 4px; height: 4px; border-radius: 999px; - background: #fff; + background: var(--primary-foreground); animation: v2-boot-pulse 900ms ease-in-out infinite alternate; } @@ -150,8 +150,8 @@ overflow: hidden; color: var(--v2-ink); background: - radial-gradient(circle at 14% 3%, rgba(255, 255, 255, 0.82), transparent 24%), - linear-gradient(158deg, var(--v2-sidebar) 0%, #e8f8f6 60%, var(--v2-sidebar-deep) 130%); + radial-gradient(circle at 14% 3%, color-mix(in oklch, var(--card) 82%, transparent), transparent 24%), + linear-gradient(158deg, var(--v2-sidebar) 0%, var(--sidebar-accent) 60%, var(--v2-sidebar-deep) 130%); border-right: 1px solid var(--v2-line); } @@ -2375,32 +2375,6 @@ line-height: 1.55; } -.console-v2.v2-interactive-sync .v2-sync-link-stack { - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 5px; -} - -.console-v2.v2-interactive-sync .v2-sync-link { - min-height: 47px; - display: grid; - grid-template-columns: 1fr 7px; - gap: 4px; - padding: 7px; - border-radius: 11px; -} - -.console-v2.v2-interactive-sync .v2-sync-link > svg { - display: none; -} - -.console-v2.v2-interactive-sync .v2-sync-link strong { - font-size: 9px; -} - -.console-v2.v2-interactive-sync .v2-sync-link span { - font-size: 7px; -} - .console-v2 .v2-sync-log { min-height: 0; display: grid; @@ -3037,12 +3011,12 @@ /* Unified cockpit sync foundation ---------------------------------------- */ .console-v2.v2-passive-display { - --v2-accent: #0b807a; - --v2-accent-strong: #075d5a; - --v2-accent-soft: #d9f5f1; - --v2-sidebar: #e6f6f3; - --v2-sidebar-deep: #ccebe7; - background: #eef3f2; + --v2-accent: var(--primary); + --v2-accent-strong: color-mix(in oklch, var(--primary) 80%, var(--foreground)); + --v2-accent-soft: color-mix(in oklch, var(--primary) 10%, var(--background)); + --v2-sidebar: var(--sidebar); + --v2-sidebar-deep: var(--sidebar-accent); + background: var(--background); } .console-v2.v2-passive-display .workspace { @@ -3058,8 +3032,8 @@ .console-v2.v2-passive-display .v2-sidebar { padding: 26px 20px 18px; background: - radial-gradient(circle at 20% 2%, rgba(255, 255, 255, 0.96), transparent 25%), - linear-gradient(160deg, #e6f7f4 0%, #d5efec 64%, #bfe4df 145%); + radial-gradient(circle at 20% 2%, color-mix(in oklch, var(--card) 96%, transparent), transparent 25%), + linear-gradient(160deg, var(--sidebar) 0%, var(--sidebar-accent) 64%, color-mix(in oklch, var(--sidebar-primary) 10%, var(--sidebar)) 145%); } .console-v2.v2-passive-display .v2-sidebar-toggle, @@ -3101,67 +3075,6 @@ line-height: 1.75; } -.console-v2 .v2-sync-link-stack { - width: 100%; - min-width: 0; - display: grid; - gap: 9px; -} - -.console-v2 .v2-sync-link { - min-width: 0; - min-height: 58px; - display: grid; - grid-template-columns: 34px minmax(0, 1fr) 8px; - align-items: center; - gap: 10px; - padding: 10px 11px; - background: rgba(255, 255, 255, 0.72); - border: 1px solid rgba(13, 108, 104, 0.1); - border-radius: 14px; - box-shadow: 0 9px 24px rgba(32, 94, 91, 0.06); -} - -.console-v2 .v2-sync-link > svg { - width: 18px; - color: #397a77; -} - -.console-v2 .v2-sync-link div { - min-width: 0; - display: grid; - gap: 2px; -} - -.console-v2 .v2-sync-link strong { - color: #173839; - font-size: 12px; -} - -.console-v2 .v2-sync-link span { - overflow: hidden; - color: #819190; - font-size: 9px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.console-v2 .v2-sync-link > b { - width: 7px; - height: 7px; - background: #b8c5c4; - border-radius: 50%; -} - -.console-v2 .v2-sync-link[data-state="online"] > b { - background: #18a783; - box-shadow: 0 0 0 5px rgba(24, 167, 131, 0.1); -} - -.console-v2 .v2-sync-link[data-state="offline"] > b { - background: #e1786f; -} - .console-v2 .v2-sync-scope { padding: 15px; background: linear-gradient(135deg, rgba(4, 89, 87, 0.92), rgba(17, 125, 117, 0.9)); @@ -6037,78 +5950,78 @@ body.device-power-drawer-open { /* Cohesive light cockpit visual refresh --------------------------------- */ .console-v2.v2-passive-display.v2-interactive-sync { - --v2-canvas: #edf4f2; - --v2-line: rgba(16, 105, 99, 0.13); - --v2-shadow: 0 16px 40px rgba(38, 80, 77, 0.09); + --v2-canvas: var(--background); + --v2-line: var(--border); + --v2-shadow: 0 16px 40px oklch(0.145 0.008 326 / 9%); background: - radial-gradient(circle at 76% -8%, rgba(93, 202, 187, 0.16), transparent 34%), - linear-gradient(145deg, #eef5f3 0%, #f7faf9 56%, #e9f3f0 100%); + radial-gradient(circle at 76% -8%, color-mix(in oklch, var(--primary) 14%, transparent), transparent 34%), + linear-gradient(145deg, var(--background) 0%, var(--secondary) 56%, var(--muted) 100%); } .console-v2.v2-passive-display.v2-interactive-sync .workspace { background: - radial-gradient(circle at 73% -10%, rgba(93, 202, 187, 0.13), transparent 34%), - linear-gradient(145deg, #edf4f2 0%, #f7faf9 58%, #eaf3f1 100%); + radial-gradient(circle at 73% -10%, color-mix(in oklch, var(--primary) 12%, transparent), transparent 34%), + linear-gradient(145deg, var(--background) 0%, var(--secondary) 58%, var(--muted) 100%); } .console-v2.v2-passive-display.v2-interactive-sync .command-panel.v2-sidebar { background: - radial-gradient(circle at 12% 2%, rgba(255, 255, 255, 0.92), transparent 24%), - linear-gradient(158deg, #e0f4f1 0%, #edf8f6 58%, #d2ebe7 132%); - border-right-color: rgba(15, 111, 104, 0.12); - box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.6); + radial-gradient(circle at 12% 2%, color-mix(in oklch, var(--card) 92%, transparent), transparent 24%), + linear-gradient(158deg, var(--sidebar) 0%, var(--sidebar-accent) 58%, color-mix(in oklch, var(--sidebar-primary) 9%, var(--sidebar)) 132%); + border-right-color: var(--sidebar-border); + box-shadow: inset -1px 0 0 color-mix(in oklch, var(--card) 60%, transparent); } .console-v2.v2-passive-display.v2-interactive-sync .v2-module-switcher { - background: rgba(249, 252, 251, 0.9); - border-bottom: 1px solid rgba(16, 105, 99, 0.1); - box-shadow: 0 5px 20px rgba(39, 82, 79, 0.045); + background: color-mix(in oklch, var(--card) 90%, transparent); + border-bottom: 1px solid var(--border); + box-shadow: 0 5px 20px oklch(0.145 0.008 326 / 5%); backdrop-filter: blur(14px); } .console-v2.v2-passive-display.v2-interactive-sync .v2-natural-panel { background: - radial-gradient(circle at 88% 12%, rgba(126, 217, 204, 0.16), transparent 28%), - linear-gradient(145deg, rgba(255, 255, 255, 0.98), rgba(247, 252, 250, 0.96)); - border: 1px solid rgba(19, 111, 105, 0.11); + radial-gradient(circle at 88% 12%, color-mix(in oklch, var(--primary) 14%, transparent), transparent 28%), + linear-gradient(145deg, color-mix(in oklch, var(--card) 98%, transparent), color-mix(in oklch, var(--secondary) 96%, transparent)); + border: 1px solid var(--border); border-radius: 19px; - box-shadow: 0 12px 34px rgba(37, 82, 78, 0.075); + box-shadow: 0 12px 34px oklch(0.145 0.008 326 / 8%); } .console-v2.v2-passive-display.v2-interactive-sync .v2-task-overview { - background: linear-gradient(145deg, rgba(247, 253, 251, 0.96), rgba(230, 246, 242, 0.92)); - border-color: rgba(23, 122, 114, 0.12); + background: linear-gradient(145deg, color-mix(in oklch, var(--card) 96%, transparent), color-mix(in oklch, var(--primary) 8%, var(--muted))); + border-color: var(--border); } .console-v2.v2-passive-display.v2-interactive-sync .v2-sync-log { - background: rgba(251, 253, 252, 0.8); - border-color: rgba(20, 111, 104, 0.1); - box-shadow: 0 12px 28px rgba(35, 84, 80, 0.055); + background: color-mix(in oklch, var(--card) 80%, transparent); + border-color: var(--border); + box-shadow: 0 12px 28px oklch(0.145 0.008 326 / 6%); backdrop-filter: blur(10px); } .console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail .mode-chips button { - color: #426967; - background: rgba(242, 250, 248, 0.94); - border: 1px solid rgba(20, 117, 109, 0.08); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.84); + color: var(--secondary-foreground); + background: color-mix(in oklch, var(--secondary) 94%, transparent); + border: 1px solid var(--border); + box-shadow: inset 0 1px 0 color-mix(in oklch, var(--card) 84%, transparent); transition: color 160ms ease, background 160ms ease, border-color 160ms ease, transform 160ms ease; } .console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail .mode-chips button:hover, .console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail .mode-chips button:focus-visible { - color: #0b6d67; - background: #e4f5f1; - border-color: rgba(13, 126, 117, 0.2); + color: var(--primary); + background: color-mix(in oklch, var(--primary) 9%, var(--background)); + border-color: color-mix(in oklch, var(--primary) 24%, var(--border)); transform: translateY(-1px); } .console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail .mode-chips button.active, .console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail .mode-chips button[aria-pressed="true"] { - color: #ffffff; - background: linear-gradient(135deg, #118a81, #0a6f69); + color: var(--primary-foreground); + background: var(--primary); border-color: transparent; - box-shadow: 0 7px 16px rgba(13, 126, 117, 0.2); + box-shadow: 0 7px 16px color-mix(in oklch, var(--primary) 20%, transparent); } .console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell.v2-output-page, @@ -6119,32 +6032,32 @@ body.device-power-drawer-open { .console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell > .twin-band, .console-v2.v2-passive-display.v2-interactive-sync #v2DevicePanel, .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-sync-mount { - border: 1px solid rgba(17, 112, 105, 0.12); + border: 1px solid var(--border); border-radius: 18px; box-shadow: - 0 14px 32px rgba(35, 78, 75, 0.075), - inset 0 1px 0 rgba(255, 255, 255, 0.78); + 0 14px 32px oklch(0.145 0.008 326 / 8%), + inset 0 1px 0 color-mix(in oklch, var(--card) 78%, transparent); } .console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell > .twin-band { overflow: hidden; - background: linear-gradient(180deg, #fbfdfc, #eff6f4); + background: linear-gradient(180deg, var(--card), var(--secondary)); } .console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-heading { background: - linear-gradient(90deg, rgba(240, 249, 247, 0.98), rgba(251, 253, 252, 0.98)); - border-bottom-color: rgba(18, 113, 106, 0.1); + linear-gradient(90deg, color-mix(in oklch, var(--secondary) 98%, transparent), color-mix(in oklch, var(--card) 98%, transparent)); + border-bottom-color: var(--border); } .console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-stage { background: - radial-gradient(ellipse at 53% 57%, rgba(108, 205, 190, 0.13), transparent 41%), - linear-gradient(165deg, #f7fbfa 0%, #eaf3f1 100%); + radial-gradient(ellipse at 53% 57%, color-mix(in oklch, var(--primary) 12%, transparent), transparent 41%), + linear-gradient(165deg, var(--background) 0%, var(--muted) 100%); } .console-v2.v2-passive-display.v2-interactive-sync #v2DevicePanel { - background: linear-gradient(180deg, rgba(252, 254, 253, 0.98), rgba(239, 247, 245, 0.97)); + background: linear-gradient(180deg, color-mix(in oklch, var(--card) 98%, transparent), color-mix(in oklch, var(--secondary) 97%, transparent)); } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-sync-board > header, @@ -6155,140 +6068,140 @@ body.device-power-drawer-open { .console-v2.v2-passive-display.v2-interactive-sync .v2-device-sync-board > header > button, .console-v2.v2-passive-display.v2-interactive-sync .v2-extension-sync-heading > button { - color: #527371; - background: rgba(255, 255, 255, 0.78); - border: 1px solid rgba(19, 105, 99, 0.12); - box-shadow: 0 3px 9px rgba(37, 84, 80, 0.055); + color: var(--muted-foreground); + background: color-mix(in oklch, var(--card) 78%, transparent); + border: 1px solid var(--border); + box-shadow: 0 3px 9px oklch(0.145 0.008 326 / 6%); } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-sync-board > header > button:hover, .console-v2.v2-passive-display.v2-interactive-sync .v2-extension-sync-heading > button:hover { - color: #0b756f; - background: #e3f4f1; - border-color: rgba(13, 126, 117, 0.21); + color: var(--primary); + background: color-mix(in oklch, var(--primary) 9%, var(--background)); + border-color: color-mix(in oklch, var(--primary) 24%, var(--border)); } .console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device { - background: rgba(255, 255, 255, 0.76); - border: 1px solid rgba(23, 110, 104, 0.085); - box-shadow: 0 4px 12px rgba(42, 87, 83, 0.045); + background: color-mix(in oklch, var(--card) 76%, transparent); + border: 1px solid var(--border); + box-shadow: 0 4px 12px oklch(0.145 0.008 326 / 5%); transition: background 160ms ease, border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease; } .console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device:hover, .console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device.active { - background: linear-gradient(145deg, #e8f7f3, #fbfdfc); - border-color: rgba(13, 126, 117, 0.22); - box-shadow: 0 8px 19px rgba(29, 100, 94, 0.09); + background: linear-gradient(145deg, color-mix(in oklch, var(--primary) 8%, var(--background)), var(--card)); + border-color: color-mix(in oklch, var(--primary) 24%, var(--border)); + box-shadow: 0 8px 19px color-mix(in oklch, var(--primary) 9%, transparent); transform: translateY(-1px); } /* The extension area now shares the same light surface hierarchy. */ .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-sync-mount { background: - radial-gradient(circle at 84% 8%, rgba(118, 211, 198, 0.15), transparent 30%), - linear-gradient(180deg, rgba(251, 254, 253, 0.99), rgba(237, 247, 244, 0.98)); + radial-gradient(circle at 84% 8%, color-mix(in oklch, var(--primary) 13%, transparent), transparent 30%), + linear-gradient(180deg, color-mix(in oklch, var(--card) 99%, transparent), color-mix(in oklch, var(--secondary) 98%, transparent)); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-sync-heading small { - color: #287f79; + color: var(--primary); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-sync-heading h3 { - color: #183536; + color: var(--foreground); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-gateway { - background: rgba(255, 255, 255, 0.74); - border: 1px solid rgba(21, 111, 104, 0.1); - box-shadow: 0 5px 14px rgba(39, 85, 81, 0.05); + background: color-mix(in oklch, var(--card) 74%, transparent); + border: 1px solid var(--border); + box-shadow: 0 5px 14px oklch(0.145 0.008 326 / 5%); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-gateway > span { - color: #168b82; - background: linear-gradient(145deg, #e3f6f2, #d5efea); - box-shadow: inset 0 0 0 1px rgba(21, 137, 128, 0.08); + color: var(--primary); + background: linear-gradient(145deg, color-mix(in oklch, var(--primary) 8%, var(--background)), var(--accent)); + box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--primary) 8%, transparent); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-gateway strong { - color: #294846; + color: var(--foreground); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-gateway small { - color: #718684; + color: var(--muted-foreground); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-gateway > b { - color: #0d7972; - background: #e1f3ef; + color: var(--primary); + background: var(--accent); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-synced-device { - background: rgba(255, 255, 255, 0.78); - border: 1px solid rgba(21, 111, 104, 0.1); - box-shadow: 0 5px 14px rgba(39, 85, 81, 0.05); + background: color-mix(in oklch, var(--card) 78%, transparent); + border: 1px solid var(--border); + box-shadow: 0 5px 14px oklch(0.145 0.008 326 / 5%); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-synced-device header > span { - color: #15877f; - background: #ddf2ee; + color: var(--primary); + background: var(--accent); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-synced-device header strong { - color: #294846; + color: var(--foreground); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-synced-device header small, .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-synced-device header b { - color: #738987; + color: var(--muted-foreground); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-synced-device > div span { - color: #3d6d68; - background: #e7f4f1; + color: var(--accent-foreground); + background: var(--accent); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-empty { min-height: 92px; - color: #668b87; - background: rgba(255, 255, 255, 0.52); - border: 1px dashed rgba(23, 121, 113, 0.15); + color: var(--muted-foreground); + background: color-mix(in oklch, var(--card) 52%, transparent); + border: 1px dashed var(--border); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-empty > svg { - color: #4c9e97; + color: var(--primary); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-empty strong { - color: #365957; + color: var(--foreground); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-empty span { - color: #7b908e; + color: var(--muted-foreground); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-linkage { - border-top-color: rgba(21, 112, 105, 0.11); + border-top-color: var(--border); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-linkage > svg { - color: #438f89; + color: var(--primary); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-linkage small { - color: #738987; + color: var(--muted-foreground); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-linkage strong { - color: #3d5f5c; + color: var(--foreground); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-linkage > span { - color: #0d7972; + color: var(--primary); } .console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel.is-collapsed .v2-extension-sync-mount { - background: linear-gradient(180deg, rgba(238, 248, 245, 0.98), rgba(215, 238, 233, 0.98)); + background: linear-gradient(180deg, color-mix(in oklch, var(--secondary) 98%, transparent), color-mix(in oklch, var(--accent) 98%, transparent)); border: 0; border-radius: 13px; box-shadow: none; @@ -6326,13 +6239,13 @@ body.device-power-drawer-open { } .console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device[data-control-open="true"] { - background: linear-gradient(155deg, #eff9f6, #fbfdfc); - border-color: rgba(13, 126, 117, 0.24); - box-shadow: 0 9px 22px rgba(29, 100, 94, 0.1); + background: linear-gradient(155deg, color-mix(in oklch, var(--primary) 8%, var(--background)), var(--card)); + border-color: color-mix(in oklch, var(--primary) 26%, var(--border)); + box-shadow: 0 9px 22px color-mix(in oklch, var(--primary) 10%, transparent); } .console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device[data-has-draft="true"] { - border-color: rgba(209, 143, 44, 0.3); + border-color: color-mix(in oklch, var(--chart-1) 34%, var(--border)); } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-header { @@ -6361,16 +6274,16 @@ body.device-power-drawer-open { height: 28px; display: grid; place-items: center; - color: #16877f; - background: #dff2ee; + color: var(--primary); + background: var(--accent); border-radius: 9px; transition: color 150ms ease, background 150ms ease; } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus:hover > span:first-child, .console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus:focus-visible > span:first-child { - color: #ffffff; - background: #12857d; + color: var(--primary-foreground); + background: var(--primary); } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus svg { @@ -6385,33 +6298,33 @@ body.device-power-drawer-open { } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus strong { - color: #1d3a3b; + color: var(--foreground); font-size: 10px; } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus small { overflow: hidden; - color: #7a908e; + color: var(--muted-foreground); font-size: 7px; text-overflow: ellipsis; white-space: nowrap; } .console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device[data-has-draft="true"] .v2-device-card-focus small { - color: #a16b1b; + color: color-mix(in oklch, var(--chart-1) 70%, var(--foreground)); font-weight: 800; } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus .v2-device-signal { width: 6px; height: 6px; - background: #aebcba; + background: var(--muted-foreground); border-radius: 50%; } .console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device[data-ready="true"] .v2-device-card-focus .v2-device-signal { - background: #18a783; - box-shadow: 0 0 0 3px rgba(24, 167, 131, 0.1); + background: var(--primary); + box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary) 10%, transparent); } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-adjust-toggle { @@ -6422,9 +6335,9 @@ body.device-power-drawer-open { justify-content: center; gap: 3px; padding: 0 6px; - color: #537572; - background: #f4f9f8; - border: 1px solid rgba(17, 108, 101, 0.12); + color: var(--muted-foreground); + background: var(--secondary); + border: 1px solid var(--border); border-radius: 8px; font-size: 7px; font-weight: 850; @@ -6433,9 +6346,9 @@ body.device-power-drawer-open { .console-v2.v2-passive-display.v2-interactive-sync .v2-device-adjust-toggle:hover, .console-v2.v2-passive-display.v2-interactive-sync .v2-device-adjust-toggle[aria-expanded="true"] { - color: #ffffff; - background: #117e77; - border-color: #117e77; + color: var(--primary-foreground); + background: var(--primary); + border-color: var(--primary); } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-adjust-toggle svg { @@ -6449,7 +6362,7 @@ body.device-power-drawer-open { gap: 4px; margin: 7px 0 0; padding: 7px 0 0; - border-top: 1px solid rgba(35, 105, 99, 0.08); + border-top: 1px solid var(--border); } .console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dl div { @@ -6457,7 +6370,7 @@ body.device-power-drawer-open { display: grid; gap: 2px; padding: 5px 6px; - background: rgba(239, 247, 245, 0.72); + background: color-mix(in oklch, var(--muted) 72%, transparent); border-radius: 7px; } @@ -6475,12 +6388,12 @@ body.device-power-drawer-open { } .console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dt { - color: #80928f; + color: var(--muted-foreground); font-size: 6.5px; } .console-v2.v2-passive-display.v2-interactive-sync .v2-synced-device dd { - color: #294e4b; + color: var(--foreground); font-size: 8px; font-weight: 850; } @@ -6490,8 +6403,8 @@ body.device-power-drawer-open { gap: 7px; margin-top: 8px; padding: 8px; - background: linear-gradient(155deg, rgba(224, 243, 239, 0.9), rgba(242, 249, 247, 0.96)); - border: 1px solid rgba(20, 117, 109, 0.12); + background: linear-gradient(155deg, color-mix(in oklch, var(--primary) 7%, var(--muted)), color-mix(in oklch, var(--secondary) 96%, transparent)); + border: 1px solid var(--border); border-radius: 10px; } @@ -6503,17 +6416,17 @@ body.device-power-drawer-open { display: grid; gap: 2px; padding-bottom: 6px; - border-bottom: 1px solid rgba(22, 111, 104, 0.1); + border-bottom: 1px solid var(--border); } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-controls-intro span { - color: #166f69; + color: var(--primary); font-size: 8px; font-weight: 900; } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-controls-intro small { - color: #718783; + color: var(--muted-foreground); font-size: 6.5px; } @@ -6523,13 +6436,13 @@ body.device-power-drawer-open { grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 4px 7px; - color: #456764; + color: var(--foreground); font-size: 7px; font-weight: 800; } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-row output { - color: #0e756f; + color: var(--primary); font: 850 7px/1 "SFMono-Regular", Consolas, monospace; } @@ -6538,13 +6451,13 @@ body.device-power-drawer-open { height: 15px; grid-column: 1 / -1; margin: 0; - accent-color: #0f8179; + accent-color: var(--primary); cursor: pointer; } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-row > small { grid-column: 1 / -1; - color: #899a97; + color: var(--muted-foreground); font-size: 6px; font-weight: 650; } @@ -6553,9 +6466,9 @@ body.device-power-drawer-open { width: 82px; height: 25px; padding: 0 5px; - color: #2f5753; - background: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(21, 108, 101, 0.14); + color: var(--foreground); + background: color-mix(in oklch, var(--background) 80%, transparent); + border: 1px solid var(--input); border-radius: 7px; outline: none; font-size: 7px; @@ -6563,8 +6476,8 @@ body.device-power-drawer-open { } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-select select:focus { - border-color: #16877f; - box-shadow: 0 0 0 2px rgba(22, 135, 127, 0.1); + border-color: var(--ring); + box-shadow: 0 0 0 2px color-mix(in oklch, var(--ring) 14%, transparent); } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-apply { @@ -6575,23 +6488,23 @@ body.device-power-drawer-open { justify-content: center; gap: 5px; margin-top: 1px; - color: #ffffff; - background: linear-gradient(135deg, #138b82, #0a706a); + color: var(--primary-foreground); + background: var(--primary); border: 0; border-radius: 8px; - box-shadow: 0 6px 14px rgba(14, 119, 112, 0.18); + box-shadow: 0 6px 14px color-mix(in oklch, var(--primary) 18%, transparent); font-size: 8px; font-weight: 900; cursor: pointer; } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-apply:hover:not(:disabled) { - background: linear-gradient(135deg, #15988e, #0b7770); + background: color-mix(in oklch, var(--primary) 86%, var(--foreground)); } .console-v2.v2-passive-display.v2-interactive-sync .v2-device-control-apply:disabled { - color: #6e8985; - background: #d8e7e4; + color: var(--muted-foreground); + background: var(--muted); box-shadow: none; cursor: wait; } @@ -6656,10 +6569,6 @@ body.device-power-drawer-open { grid-template-rows: auto minmax(88px, 1fr) auto; } -.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-link span { - font-size: 9px; -} - .console-v2.v2-passive-display.v2-interactive-sync .v2-sync-log header small, .console-v2.v2-passive-display.v2-interactive-sync .v2-sync-log time { font-size: 8.5px; @@ -6899,6 +6808,117 @@ body.device-power-drawer-open { } } +/* Expanded function-mode grid and reusable natural-language mode creator. */ +.console-v2 .v2-mode-rail > .mode-rail-bar { + grid-template-columns: auto minmax(0, 1fr) auto; +} + +.console-v2 .v2-mode-rail .mode-rail-actions { + min-width: 0; + display: flex; + align-items: center; + gap: 8px; +} + +.console-v2 .v2-mode-rail .mode-rail-actions > button { + min-height: 38px; + display: inline-flex !important; + align-items: center; + justify-content: center; + gap: 7px; + padding: 7px 11px; + color: var(--primary); + background: var(--card); + border: 1px solid color-mix(in oklch, var(--primary) 24%, var(--border)); + border-radius: var(--radius); + box-shadow: 0 5px 14px color-mix(in oklch, var(--foreground) 6%, transparent); + white-space: nowrap; +} + +.console-v2 .v2-mode-rail .mode-rail-actions > button:hover, +.console-v2 .v2-mode-rail .mode-rail-actions > button:focus-visible { + color: var(--accent-foreground); + background: var(--accent); + border-color: var(--ring); + outline: none; +} + +.console-v2 .v2-mode-rail .mode-rail-actions > button svg { + width: 16px; + height: 16px; + flex: 0 0 auto; +} + +.console-v2 .v2-mode-rail .mode-rail-actions > .mode-gallery-toggle strong { + min-width: 20px; + min-height: 20px; + display: inline-grid; + place-items: center; + padding: 0 5px; + color: var(--primary-foreground); + background: var(--primary); + border-radius: 999px; +} + +.console-v2 .v2-mode-rail > .mode-rail-content { + display: block; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail > .mode-rail-content .mode-chips { + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); + gap: 8px; + padding: 0; + overflow: visible; + scroll-behavior: auto; + overscroll-behavior: auto; + touch-action: auto; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail > .mode-rail-content .mode-chips > button { + width: 100%; + min-width: 0; + min-height: 38px; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail > .mode-rail-content .mode-group-divider { + width: auto; + height: 1px; + grid-column: 1 / -1; + margin: 1px 0; +} + +.console-v2 .mode-gallery-add-card { + padding-right: 9px; + border-style: dashed; + border-color: color-mix(in oklch, var(--primary) 42%, var(--border)); + color: var(--card-foreground); + background: color-mix(in oklch, var(--primary) 5%, var(--card)); +} + +.console-v2 .mode-gallery-add-card .mode-gallery-card-icon { + color: var(--primary); + background: color-mix(in oklch, var(--primary) 12%, var(--card)); +} + +.console-v2 .mode-gallery-add-card:hover, +.console-v2 .mode-gallery-add-card:focus-visible { + border-color: var(--primary); + background: var(--accent); +} + +@media (max-width: 980px) { + .console-v2 .v2-mode-rail > .mode-rail-bar { + grid-template-columns: auto minmax(0, 1fr); + } + + .console-v2 .v2-mode-rail .mode-rail-actions { + grid-column: 1 / -1; + justify-content: flex-end; + } +} + .console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail .mode-gallery-toggle strong, .console-v2.v2-passive-display.v2-interactive-sync .v2-task-timeline i, .console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-seat-label small, @@ -7395,7 +7415,6 @@ body.device-power-drawer-open { .console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-execution-field, .console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-simulation-fault-field, .console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-can-command-panel, -.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-link-stack, .console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sidebar-footer, .console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed .v2-sync-log { display: none !important; @@ -7421,3 +7440,980 @@ body.device-power-drawer-open { letter-spacing: 0.16em; writing-mode: vertical-rl; } + +/* Final semantic theme cascade ------------------------------------------- + This must remain last: older delivery-state layers contain fixed teal + values for the twin, icon tiles and nested control surfaces. */ +.console-v2 .v2-unified-brand-mark, +.console-v2 .v2-sidebar-brand-mark, +.console-v2 .v2-boot-mark { + color: var(--primary-foreground); + background: var(--primary); + border-color: color-mix(in oklch, var(--primary) 32%, var(--border)); + box-shadow: 0 10px 24px color-mix(in oklch, var(--primary) 20%, transparent); +} + +.console-v2 .v2-remote-control-state, +.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-transport-field, +.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field, +.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-simulation-fault-field, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel { + color: var(--card-foreground); + background: color-mix(in oklch, var(--card) 88%, transparent); + border-color: var(--border); + box-shadow: 0 5px 14px color-mix(in oklch, var(--foreground) 6%, transparent); +} + +.console-v2 .v2-remote-control-state, +.console-v2 .v2-remote-control-state strong, +.console-v2 .v2-remote-control-state svg { + color: var(--primary); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field > .field-label, +.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-simulation-fault-field > .field-label, +.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-transport-field > .field-label, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel summary { + color: var(--foreground); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field .provider-note, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list p, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li > span { + color: var(--muted-foreground); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field .mode-option.active, +.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-transport-field .mode-option.active { + color: var(--primary-foreground); + background: var(--primary); + border-color: var(--primary); + box-shadow: 0 4px 11px color-mix(in oklch, var(--primary) 20%, transparent); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field .mode-option:disabled { + color: var(--muted-foreground); + background: var(--muted); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list { + border-top-color: var(--border); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel="CAN0"], +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel="IR"] { + color: var(--foreground); + background: var(--muted); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel summary > strong, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-panel[data-state="ready"] summary > strong, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-channel, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-source, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-source[data-parse-source="local_rule"], +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-source[data-parse-source="direct_control"], +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-delivery, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-delivery[data-delivery-state="submitted"] { + color: var(--primary); + background: var(--accent); + border-color: color-mix(in oklch, var(--primary) 18%, var(--border)); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel="CAN0"] .v2-can-command-channel, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel="IR"] .v2-can-command-channel, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel="IR"] code, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel="IR"] .v2-can-command-frame, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li code, +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-frame { + color: var(--foreground); +} + +.console-v2.v2-passive-display.v2-interactive-sync #v2SidebarToggle, +.console-v2.v2-passive-display.v2-interactive-sync.v2-sidebar-collapsed #v2SidebarToggle { + color: var(--primary); + background: color-mix(in oklch, var(--card) 86%, transparent); + border-color: var(--sidebar-border); + box-shadow: 0 5px 14px color-mix(in oklch, var(--foreground) 6%, transparent); +} + +.console-v2.v2-passive-display.v2-interactive-sync #v2SidebarToggle:hover, +.console-v2.v2-passive-display.v2-interactive-sync #v2SidebarToggle:focus-visible { + color: var(--primary-foreground); + background: var(--primary); + border-color: var(--primary); +} + +.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell.v2-output-page, +.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell[data-open-panels] { + color: var(--foreground); + background: transparent; +} + +.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell > .twin-band, +.console-v2.v2-passive-display.v2-interactive-sync #v2DevicePanel, +.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-sync-mount, +.console-v2.v2-passive-display.v2-interactive-sync .v2-side-status-panel.is-collapsed { + color: var(--card-foreground); + background: + radial-gradient(circle at 78% 0%, color-mix(in oklch, var(--primary) 8%, transparent), transparent 18rem), + var(--card); + border-color: var(--border); + box-shadow: 0 14px 32px color-mix(in oklch, var(--foreground) 8%, transparent); +} + +.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-heading, +.console-v2.v2-passive-display.v2-interactive-sync .v2-device-sync-board > header, +.console-v2.v2-passive-display.v2-interactive-sync .v2-extension-sync-heading { + color: var(--foreground); + background: linear-gradient(90deg, var(--secondary), var(--card)); + border-bottom-color: var(--border); +} + +.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-heading .section-kicker, +.console-v2.v2-passive-display.v2-interactive-sync .v2-device-sync-board > header h3, +.console-v2.v2-passive-display.v2-interactive-sync .v2-extension-sync-heading h3 { + color: var(--foreground); +} + +.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-focus-badge, +.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-reset, +.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-seat-switch, +.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-seat-switch button { + color: var(--primary); + background: var(--accent); + border-color: var(--border); +} + +.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-seat-switch button.active, +.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-reset:hover, +.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-reset:focus-visible { + color: var(--primary-foreground); + background: var(--primary); + border-color: var(--primary); +} + +.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-stage { + background: + radial-gradient(ellipse at 53% 57%, color-mix(in oklch, var(--primary) 12%, transparent), transparent 41%), + linear-gradient(165deg, var(--background), var(--muted)); +} + +.console-v2 .v2-twin-hotspots button::before { + background: color-mix(in oklch, var(--primary) 72%, var(--border)); + box-shadow: 0 0 7px color-mix(in oklch, var(--primary) 28%, transparent); +} + +.console-v2 .v2-twin-hotspots button::after { + background: var(--primary); + border-color: var(--primary-foreground); + box-shadow: + 0 0 0 3px color-mix(in oklch, var(--primary) 17%, transparent), + 0 2px 8px color-mix(in oklch, var(--primary) 28%, transparent); +} + +.console-v2 .v2-twin-hotspot-label { + color: var(--popover-foreground); + background: color-mix(in oklch, var(--popover) 94%, transparent); + border-color: color-mix(in oklch, var(--primary) 24%, var(--border)); + box-shadow: 0 8px 20px color-mix(in oklch, var(--foreground) 18%, transparent); +} + +.console-v2 .v2-twin-hotspot-label svg, +.console-v2 .v2-twin-hotspot-label small { + color: var(--primary); +} + +.console-v2 .v2-twin-hotspots button:hover::before, +.console-v2 .v2-twin-hotspots button:focus-visible::before, +.console-v2 .v2-twin-hotspots button.active::before, +.console-v2 .v2-twin-hotspots button:hover::after, +.console-v2 .v2-twin-hotspots button:focus-visible::after, +.console-v2 .v2-twin-hotspots button.active::after { + background: var(--primary); + box-shadow: 0 0 0 5px color-mix(in oklch, var(--primary) 18%, transparent); +} + +.console-v2 .v2-twin-hotspots button:hover .v2-twin-hotspot-label, +.console-v2 .v2-twin-hotspots button:focus-visible .v2-twin-hotspot-label, +.console-v2 .v2-twin-hotspots button.active .v2-twin-hotspot-label { + color: var(--primary-foreground); + background: var(--primary); + border-color: color-mix(in oklch, var(--primary-foreground) 36%, transparent); + box-shadow: 0 11px 24px color-mix(in oklch, var(--primary) 28%, transparent); +} + +.console-v2 .v2-twin-hotspots button:hover .v2-twin-hotspot-label svg, +.console-v2 .v2-twin-hotspots button:hover .v2-twin-hotspot-label small, +.console-v2 .v2-twin-hotspots button:focus-visible .v2-twin-hotspot-label svg, +.console-v2 .v2-twin-hotspots button:focus-visible .v2-twin-hotspot-label small, +.console-v2 .v2-twin-hotspots button.active .v2-twin-hotspot-label svg, +.console-v2 .v2-twin-hotspots button.active .v2-twin-hotspot-label small { + color: var(--primary-foreground); +} + +.console-v2 .twin-callout { + color: var(--popover-foreground); + background: color-mix(in oklch, var(--popover) 94%, transparent); + border-color: var(--primary); + box-shadow: 0 6px 18px color-mix(in oklch, var(--foreground) 18%, transparent); +} + +.console-v2 .twin-callout b, +.console-v2 .twin-callout small, +.console-v2 .twin-orientation, +.console-v2 .twin-seat-label, +.console-v2 .twin-seat-label small { + color: var(--primary); +} + +.console-v2 .v2-twin-device-popover { + color: var(--popover-foreground); + background: color-mix(in oklch, var(--popover) 96%, transparent); + border-color: color-mix(in oklch, var(--primary) 24%, var(--border)); + box-shadow: 0 18px 42px color-mix(in oklch, var(--foreground) 20%, transparent); +} + +.console-v2 .v2-twin-device-popover header > span { + color: var(--primary-foreground); + background: var(--primary); +} + +.console-v2 .v2-twin-device-popover header small, +.console-v2 .v2-twin-device-popover header > b, +.console-v2 .v2-twin-device-popover dt { + color: var(--muted-foreground); +} + +.console-v2 .v2-twin-device-popover dl div, +.console-v2 .v2-twin-popover-close { + color: var(--foreground); + background: var(--muted); + border-color: var(--border); +} + +.console-v2 .v2-twin-device-popover dd { + color: var(--foreground); +} + +.console-v2 .v2-twin-popover-close:hover, +.console-v2 .v2-twin-popover-close:focus-visible { + color: var(--primary-foreground); + background: var(--primary); + border-color: var(--primary); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-device-card-focus > span:first-child, +.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-gateway > span, +.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel .v2-extension-synced-device header > span { + color: var(--primary); + background: var(--accent); + border-color: color-mix(in oklch, var(--primary) 18%, var(--border)); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-side-status-panel.is-collapsed, +.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel.is-collapsed .v2-extension-sync-mount { + background: linear-gradient(180deg, var(--secondary), var(--accent)); + border-color: var(--border); +} + +.console-v2.v2-passive-display.v2-interactive-sync #v2DevicePanel.is-collapsed > header > button, +.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel.is-collapsed .v2-extension-sync-heading > button { + color: var(--primary); + background: transparent; +} + +.console-v2.v2-passive-display.v2-interactive-sync #v2DevicePanel.is-collapsed > header > button:hover, +.console-v2.v2-passive-display.v2-interactive-sync #v2ExtensionPanel.is-collapsed .v2-extension-sync-heading > button:hover { + color: var(--primary-foreground); + background: var(--primary); +} + +.console-v2 .v2-reasoning-field { + color: var(--popover-foreground); + background: color-mix(in oklch, var(--popover) 98%, transparent); + border-color: var(--border); + box-shadow: 0 16px 38px color-mix(in oklch, var(--foreground) 20%, transparent); +} + +.console-v2 .v2-reasoning-field > .field-label { + color: var(--foreground); +} + +.console-v2 .v2-reasoning-field .reasoning-option { + color: var(--muted-foreground); +} + +.console-v2 .v2-reasoning-field .reasoning-option:hover, +.console-v2 .v2-reasoning-field .reasoning-option.active { + color: var(--accent-foreground); + background: var(--accent); + border-color: var(--border); +} + +.console-v2 .v2-reasoning-trigger { + color: var(--muted-foreground); + background: color-mix(in oklch, var(--muted) 96%, transparent); + border-color: var(--border); +} + +.console-v2 .v2-reasoning-trigger:hover, +.console-v2 .v2-reasoning-trigger.open { + color: var(--primary); + background: var(--accent); + border-color: color-mix(in oklch, var(--primary) 22%, var(--border)); +} + +.console-v2 .device-power-backdrop { + background: color-mix(in oklch, var(--foreground) 38%, transparent); +} + +.console-v2 .device-power-drawer { + color: var(--popover-foreground); + background: color-mix(in oklch, var(--popover) 98%, transparent); + border-color: var(--border); + box-shadow: -18px 0 48px color-mix(in oklch, var(--foreground) 18%, transparent); +} + +.console-v2 .device-power-drawer-heading { + background: var(--popover); + border-bottom-color: var(--border); +} + +.console-v2 .device-power-drawer-heading > button, +.console-v2 .device-power-energy-summary > div, +.console-v2 .device-power-components, +.console-v2 .device-power-chart, +.console-v2 .device-power-unavailable { + color: var(--foreground); + background: var(--muted); + border-color: var(--border); +} + +.console-v2 .device-power-drawer-heading > button:hover { + color: var(--primary-foreground); + background: var(--primary); +} + +.console-v2 .device-power-total { + color: var(--primary-foreground); + background: var(--primary); + box-shadow: 0 8px 20px color-mix(in oklch, var(--primary) 20%, transparent); +} + +.console-v2 .toast { + color: var(--popover-foreground); + background: var(--popover); + border: 1px solid var(--border); + box-shadow: 0 14px 36px color-mix(in oklch, var(--foreground) 16%, transparent); +} + +.console-v2 .v2-transport-field .mode-option { + color: var(--muted-foreground); + background: var(--secondary); + border-color: var(--border); +} + +.console-v2 .v2-transport-field .mode-option.active, +.console-v2 .v2-request-form .primary-action { + color: var(--primary-foreground); + background: var(--primary); + border-color: var(--primary); +} + +.console-v2 .v2-sync-log > header, +.console-v2.v2-interactive-sync .v2-sidebar-footer { + background: color-mix(in oklch, var(--card) 88%, transparent); + border-color: var(--border); +} + +.console-v2 .v2-sync-log header small { + color: var(--primary); +} + +.console-v2 .v2-sync-log h3, +.console-v2 .v2-sync-log li strong { + color: var(--foreground); +} + +.console-v2 .v2-sync-log select { + color: var(--foreground); + background: var(--background); + border-color: var(--input); +} + +.console-v2 .v2-sync-log time, +.console-v2 .v2-sync-log li small { + color: var(--muted-foreground); +} + +.console-v2 .v2-sync-log li:not(:last-child)::after { + background: var(--border); +} + +.console-v2 .v2-sync-log li > span { + color: var(--primary-foreground); + background: var(--primary); +} + +.console-v2 .v2-sync-log li[data-log-status="success"] > span { + color: var(--primary-foreground); + background: var(--chart-3); +} + +.console-v2 .v2-sync-log li[data-log-status="error"] > span { + color: var(--primary-foreground); + background: var(--destructive); +} + +/* Result modules: trace, configuration, CAN plan and execution feedback. */ +.console-v2 .result-workspace .tab-panel, +.console-v2 .result-workspace .tab-panel[hidden] { + color: var(--card-foreground); + background: var(--card); + border-color: var(--border); + box-shadow: 0 6px 18px color-mix(in oklch, var(--foreground) 6%, transparent); +} + +.console-v2 .result-workspace #panel-config.config-panel { + background: var(--muted); +} + +.console-v2 .workspace-panel-title, +.console-v2 .result-workspace .config-panel-heading, +.console-v2 .decision-list li, +.console-v2 .config-card-header, +.console-v2 .config-object-block, +.console-v2 .config-parameters-block, +.console-v2 .config-hardware-block { + border-color: var(--border); +} + +.console-v2 .workspace-panel-state, +.console-v2 .result-workspace .tab-panel.v2-scrollable::after, +.console-v2 .config-domain-badge, +.console-v2 .config-signal-list code, +.console-v2 .result-item code { + color: var(--primary); + background: var(--accent); +} + +.console-v2 .workspace-panel-state.succeeded { + color: color-mix(in oklch, var(--chart-3) 76%, var(--foreground)); + background: color-mix(in oklch, var(--chart-1) 16%, var(--card)); +} + +.console-v2 .workspace-panel-state.partial, +.console-v2 .validation-item { + color: color-mix(in oklch, var(--chart-4) 76%, var(--foreground)); + background: color-mix(in oklch, var(--chart-1) 12%, var(--card)); + border-color: var(--chart-4); +} + +.console-v2 .workspace-panel-state.failed, +.console-v2 .validation-item.error { + color: var(--destructive); + background: color-mix(in oklch, var(--destructive) 9%, var(--card)); + border-color: var(--destructive); +} + +.console-v2 .decision-list li, +.console-v2 .trace-event span, +.console-v2 .validation-item code, +.console-v2 .config-empty strong, +.console-v2 .config-object-name, +.console-v2 .config-route-node strong { + color: var(--foreground); +} + +.console-v2 .detail-heading span, +.console-v2 .trace-event small, +.console-v2 .empty-line, +.console-v2 .config-panel-heading p, +.console-v2 .config-card-identity small, +.console-v2 .config-block-label, +.console-v2 .config-object-path, +.console-v2 .config-parameter > span, +.console-v2 .config-route-node small, +.console-v2 .config-card-footer { + color: var(--muted-foreground); +} + +.console-v2 .decision-list li::before, +.console-v2 .trace-event code, +.console-v2 .result-item > svg, +.console-v2 .config-route-arrow, +.console-v2 .config-signal-list .config-signal-more { + color: var(--primary); +} + +.console-v2 .trace-list { + border-color: var(--border); +} + +.console-v2 .trace-event::before { + background: var(--primary); + border-color: var(--card); +} + +.console-v2 .trace-event.failed::before { + background: var(--destructive); +} + +.console-v2 .validation-block, +.console-v2 .result-item { + border-color: var(--border); +} + +.console-v2 .config-action, +.console-v2 .config-empty, +.console-v2 .config-card, +.console-v2 .config-parameter, +.console-v2 .config-route-node { + color: var(--foreground); + background: var(--card); + border-color: var(--border); +} + +.console-v2 .config-action:hover, +.console-v2 .config-action:focus-visible { + color: var(--accent-foreground); + background: var(--accent); + border-color: var(--ring); +} + +.console-v2 .config-empty svg, +.console-v2 .config-card-icon, +.console-v2 .config-scene-icon { + color: var(--primary); +} + +.console-v2 .config-scene-summary { + color: var(--primary-foreground); + background: var(--primary); +} + +.console-v2 .config-scene-icon { + background: color-mix(in oklch, var(--primary-foreground) 12%, transparent); + border-color: color-mix(in oklch, var(--primary-foreground) 20%, transparent); +} + +.console-v2 .config-scene-copy span, +.console-v2 .config-scene-stats small, +.console-v2 .config-scene-stats b { + color: var(--primary-foreground); +} + +.console-v2 .config-card.config-seat { --config-accent: var(--primary); } +.console-v2 .config-card.config-climate { --config-accent: var(--chart-2); } +.console-v2 .config-card.config-ambient { --config-accent: var(--chart-3); } +.console-v2 .config-card.config-roof { --config-accent: var(--chart-4); } + +.console-v2 .config-card-icon, +.console-v2 .config-domain-badge { + background: color-mix(in oklch, var(--config-accent) 10%, var(--card)); +} + +.console-v2 .config-parameter, +.console-v2 .config-card-footer, +.console-v2 .config-hardware-block { + background: var(--muted); +} + +.console-v2 .config-source-details, +.console-v2 .code-panel, +.console-v2 pre, +.console-v2 .plan-panel .frame-summary, +.console-v2 .frame-summary { + color: var(--primary-foreground); + background: color-mix(in oklch, var(--foreground) 90%, var(--background)); + border-color: color-mix(in oklch, var(--primary) 24%, transparent); +} + +.console-v2 .config-source-details summary, +.console-v2 .code-toolbar { + color: var(--primary-foreground); + background: color-mix(in oklch, var(--foreground) 82%, var(--background)); + border-color: color-mix(in oklch, var(--primary) 20%, transparent); +} + +.console-v2 .config-source-details summary svg, +.console-v2 .frame-summary code { + color: var(--chart-1); +} + +.console-v2 .config-source-details summary small, +.console-v2 .frame-summary span { + color: color-mix(in oklch, var(--primary-foreground) 68%, transparent); +} + +.console-v2 .execution-summary { + color: var(--foreground); + background: var(--muted); + border-color: var(--border); +} + +.console-v2 .execution-summary.succeeded { + background: color-mix(in oklch, var(--chart-1) 14%, var(--card)); + border-color: var(--chart-3); +} + +.console-v2 .execution-summary.partial { + background: color-mix(in oklch, var(--chart-2) 12%, var(--card)); + border-color: var(--chart-4); +} + +.console-v2 .execution-summary.failed { + background: color-mix(in oklch, var(--destructive) 9%, var(--card)); + border-color: var(--destructive); +} + +/* Control clarity and collapsible function modes ------------------------ */ +.console-v2.v2-passive-display.v2-interactive-sync .v2-operation-shell { + z-index: 20; + overflow: visible; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail { + min-width: 0; + position: relative; + display: block; + grid-column: 1; + grid-row: 3; + padding: 0; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail .mode-rail-heading { + min-width: 0; + display: flex !important; + grid-column: 1 / -1; + grid-row: 1; + justify-content: flex-end; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail #saveModeToggle { + display: inline-flex !important; + min-height: 36px; + align-items: center; + gap: 6px; + padding: 7px 10px; + color: var(--primary); + background: var(--accent); + border-color: color-mix(in oklch, var(--primary) 22%, var(--border)); + border-radius: var(--radius); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail .mode-chips { + min-width: 0; + display: flex; + grid-column: 1; + grid-row: 2; + gap: 7px; + padding: 1px; + overflow-x: auto; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail .mode-chips button { + min-width: 98px; + min-height: 36px; + padding: 7px 11px; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail .mode-rail-controls { + min-width: max-content; + display: flex !important; + grid-column: 2; + grid-row: 2; + align-items: center; + gap: 6px; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail .mode-gallery-toggle { + min-height: 36px; + padding: 6px 10px; + color: var(--primary); + background: var(--accent); + border-color: color-mix(in oklch, var(--primary) 22%, var(--border)); + border-radius: var(--radius); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail .save-mode-composer { + grid-column: 1 / -1; + grid-row: 3; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field .execution-mode-options { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-transport-field .mode-option, +.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-execution-field .mode-option { + min-height: 38px; + font-size: var(--text-control); +} + +.console-v2 button, +.console-v2 button span, +.console-v2 button strong, +.console-v2 button small, +.console-v2 button b { + font-size: var(--text-control) !important; +} + +.console-v2 input, +.console-v2 textarea, +.console-v2 select { + font-size: var(--text-input) !important; +} + +.console-v2 label, +.console-v2 .field-label, +.console-v2 dt { + font-size: var(--text-label) !important; +} + +.console-v2 .provider-note, +.console-v2 .composer-hint, +.console-v2 .v2-sidebar-footer small, +.console-v2 .v2-task-timeline i { + font-size: var(--text-caption) !important; +} + +.console-v2 .device-state, +.console-v2 .workspace-panel-state, +.console-v2 .v2-task-state, +.console-v2 .simulation-label, +.console-v2 .seat-channel-badge { + font-size: var(--text-caption); +} + +/* Inline function-mode collapsible and semantic command fields ---------- */ +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail { + min-width: 0; + position: relative; + display: block; + grid-column: 1; + grid-row: 3; + padding: 0; +} + +.console-v2 .v2-mode-rail > .mode-rail-bar { + width: 100%; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 9px; +} + +.console-v2 .v2-mode-rail .mode-rail-title { + color: var(--foreground); + font-size: var(--text-label); + font-weight: 850; + white-space: nowrap; +} + +.console-v2 .v2-mode-rail .mode-rail-toggle, +.console-v2 .v2-mode-rail > .mode-rail-bar > .mode-gallery-toggle { + min-width: 0; + min-height: 38px; + display: inline-flex; + align-items: center; + gap: 7px; + padding: 7px 11px; + color: var(--foreground); + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: 0 5px 14px color-mix(in oklch, var(--foreground) 6%, transparent); +} + +.console-v2 .v2-mode-rail .mode-rail-toggle { + justify-content: space-between; +} + +.console-v2 .v2-mode-rail .mode-rail-toggle:hover, +.console-v2 .v2-mode-rail .mode-rail-toggle:focus-visible, +.console-v2 .v2-mode-rail > .mode-rail-bar > .mode-gallery-toggle:hover, +.console-v2 .v2-mode-rail > .mode-rail-bar > .mode-gallery-toggle:focus-visible { + color: var(--accent-foreground); + background: var(--accent); + border-color: var(--ring); + outline: none; +} + +.console-v2 .v2-mode-rail .mode-rail-toggle svg, +.console-v2 .v2-mode-rail > .mode-rail-bar > .mode-gallery-toggle svg { + width: 16px; + height: 16px; + flex: 0 0 auto; +} + +.console-v2 .v2-mode-rail .mode-rail-toggle svg:last-child { + transition: transform 160ms ease; +} + +.console-v2 .v2-mode-rail .mode-rail-toggle[aria-expanded="true"] svg:last-child { + transform: rotate(180deg); +} + +.console-v2 .v2-mode-rail > .mode-rail-bar > .mode-gallery-toggle { + color: var(--primary); + white-space: nowrap; +} + +.console-v2 .v2-mode-rail > .mode-rail-bar > .mode-gallery-toggle strong { + min-width: 20px; + min-height: 20px; + display: inline-grid; + place-items: center; + padding: 0 5px; + color: var(--primary-foreground); + background: var(--primary); + border-radius: 999px; +} + +.console-v2 .v2-mode-rail > .mode-rail-content[hidden] { + display: none !important; +} + +.console-v2 .v2-mode-rail > .mode-rail-content { + min-width: 0; + position: static; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 9px; + margin-top: 8px; + padding: 10px; + color: var(--card-foreground); + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: 0 12px 28px color-mix(in oklch, var(--foreground) 10%, transparent); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail > .mode-rail-content .mode-rail-heading { + min-width: 0; + display: flex !important; + grid-column: 1 / -1; + justify-content: flex-end; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail > .mode-rail-content .mode-chips { + min-width: 0; + display: flex; + grid-column: 1; + grid-row: auto; + gap: 7px; + padding: 1px; + overflow-x: auto; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail > .mode-rail-content .mode-rail-controls { + min-width: max-content; + display: flex !important; + grid-column: 2; + grid-row: auto; + align-items: center; + gap: 6px; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail > .mode-rail-content .save-mode-composer { + width: auto; + position: static; + grid-column: 1 / -1; + grid-row: auto; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel] .v2-can-command-channel { + color: var(--command-channel); + background: color-mix(in oklch, var(--command-channel) 12%, var(--card)); + border-color: color-mix(in oklch, var(--command-channel) 30%, var(--border)); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel] .v2-can-command-source { + color: var(--command-source); + background: color-mix(in oklch, var(--command-source) 12%, var(--card)); + border-color: color-mix(in oklch, var(--command-source) 30%, var(--border)); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel] .v2-can-command-delivery { + color: var(--command-pending); + background: var(--muted); + border-color: var(--border); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel] .v2-can-command-delivery[data-delivery-state="submitted"] { + color: var(--command-delivered); + background: color-mix(in oklch, var(--command-delivered) 12%, var(--card)); + border-color: color-mix(in oklch, var(--command-delivered) 30%, var(--border)); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel] .v2-can-command-category { + color: var(--command-category); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel] code { + color: var(--command-control); +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-can-command-list li[data-can-channel] .v2-can-command-frame { + color: var(--command-frame); +} + +@media (max-width: 1120px) { + .console-v2 .v2-mode-rail > .mode-rail-bar { + grid-template-columns: auto minmax(150px, 1fr) auto; + } + + .console-v2 .v2-mode-rail > .mode-rail-content { + grid-template-columns: minmax(0, 1fr); + } + + .console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail > .mode-rail-content .mode-chips, + .console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail > .mode-rail-content .mode-rail-controls { + grid-column: 1; + } +} + +/* Final cascade: show every mode in a floating, vertically scrollable grid. */ +.console-v2 .v2-mode-rail > .mode-rail-content { + width: 100%; + max-height: clamp(180px, calc(100dvh - 210px), 420px); + position: absolute; + top: calc(100% + 8px); + right: 0; + left: 0; + display: block; + grid-template-columns: none; + margin-top: 0; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail > .mode-rail-content .mode-chips { + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); + grid-column: auto; + gap: 8px; + padding: 0; + overflow: visible; + scroll-behavior: auto; + overscroll-behavior: auto; + touch-action: auto; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail > .mode-rail-content .mode-chips > button { + width: 100%; + min-width: 0; + min-height: 38px; +} + +.console-v2.v2-passive-display.v2-interactive-sync .v2-mode-rail > .mode-rail-content .mode-group-divider { + width: auto; + height: 1px; + grid-column: 1 / -1; + margin: 1px 0; +} diff --git a/apps/web-console/console-v2.js b/apps/web-console/console-v2.js index 5ffb206..f22e8a6 100644 --- a/apps/web-console/console-v2.js +++ b/apps/web-console/console-v2.js @@ -806,6 +806,34 @@ const selectControl = (key, label, value, options) => ({ key, label, type: "select", value, options }); const hasControlValue = (values, key) => Object.hasOwn(values, key); + function normalizeDeviceReadout(value) { + const text = String(value ?? "").trim(); + if (!text || /^-+$/.test(text) || /^--\s*/.test(text)) return "-"; + if (/未知|等待反馈|同步中|状态未知/.test(text)) return "-"; + return text; + } + + function selectedTransportHasConnection() { + const selected = $('.vehicle-transport-options [data-control-transport][aria-pressed="true"]') + || $('.vehicle-transport-options [data-control-transport].active'); + return selected?.dataset.connectionState === "online"; + } + + function deviceFeedbackAvailable(deviceId) { + if (!selectedTransportHasConnection()) return false; + return deviceId === "passenger" + ? document.body.dataset.passengerFeedbackState === "online" + : document.body.dataset.vehicleFeedbackState === "online"; + } + + function deviceReadoutRows(device) { + const rows = device.rows(); + if (!deviceFeedbackAvailable(device.id)) { + return rows.map(([label]) => [label, "-"]); + } + return rows.map(([label, value]) => [label, normalizeDeviceReadout(value)]); + } + function deviceControlValue(deviceId, control) { const draft = displaySync.deviceDrafts[deviceId]; return draft && Object.hasOwn(draft, control.key) ? draft[control.key] : control.value; @@ -894,7 +922,9 @@ rows: () => { const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness"); const [frontColor, rearColor] = splitReadout("#ambientColorIndex"); - return [["开关状态", /OFF|关闭/.test(readText("#ambientPower")) ? "关闭" : "开启"], ["前排亮度", frontBrightness], ["前排颜色", frontColor], ["后排亮度", rearBrightness], ["后排颜色", rearColor]]; + const rawPower = normalizeDeviceReadout(readText("#ambientPower")); + const power = rawPower === "-" ? "-" : /OFF|关闭/.test(rawPower) ? "关闭" : "开启"; + return [["开关状态", power], ["前排亮度", frontBrightness], ["前排颜色", frontColor], ["后排亮度", rearBrightness], ["后排颜色", rearColor]]; }, controls: () => { const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness"); @@ -954,8 +984,8 @@ const grid = $("#v2DeviceSyncGrid"); if (!grid) return; grid.innerHTML = syncedDevices.map((device) => { - const rows = device.rows(); - const waiting = rows.some(([, value]) => /--|等待|同步中|未知/.test(value)); + const rows = deviceReadoutRows(device); + const waiting = rows.some(([, value]) => value === "-"); const controllable = device.controllable !== false; const controlOpen = controllable && displaySync.controlDevice === device.id; const hasDraft = Boolean(displaySync.deviceDrafts[device.id]); @@ -979,7 +1009,7 @@ if (displaySync.activeDevice) { $(`.v2-synced-device[data-device="${displaySync.activeDevice}"]`)?.classList.add("active"); } - displaySync.deviceFingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, device.rows()])); + displaySync.deviceFingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, deviceReadoutRows(device)])); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); } @@ -1071,7 +1101,7 @@ if (displaySync.deviceFrame) return; displaySync.deviceFrame = requestAnimationFrame(() => { displaySync.deviceFrame = null; - const fingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, device.rows()])); + const fingerprint = JSON.stringify(syncedDevices.map((device) => [device.id, deviceReadoutRows(device)])); if (fingerprint === displaySync.deviceFingerprint && $("#v2DeviceSyncGrid")?.childElementCount) return; renderSyncedDevices(); if (displaySync.activeDevice) presentTwinDevice(displaySync.activeDevice, { @@ -1157,7 +1187,7 @@ } displaySync.activeDevice = deviceId; if (pinned) displaySync.pinnedDevice = deviceId; - const rows = device.rows(); + const rows = deviceReadoutRows(device); popover.hidden = false; popover.dataset.device = deviceId; popover.dataset.pinned = String(Boolean(displaySync.pinnedDevice)); @@ -1341,30 +1371,9 @@ } function applyDisplaySnapshot(snapshot) { - const health = snapshot?.extension_health || {}; - const physicallyConnected = health.transport !== "simulation" - && Boolean(health.connected || health.peer_connected); const synchronizedCatalog = Array.isArray(snapshot?.extension?.catalog) ? snapshot.extension.catalog : []; - const connected = physicallyConnected || synchronizedCatalog.length > 0; - const listening = Boolean(health.listening); - const usbState = $("#v2UsbState"); - if (usbState) { - const usbLabel = $("span", usbState); - if (usbLabel) usbLabel.textContent = physicallyConnected - ? "USB 外设已接入" - : synchronizedCatalog.length - ? `${new Set(synchronizedCatalog.map((item) => item.device_id)).size} 个外设已同步` - : listening ? "等待 USB 外设" : "扩展链路检查中"; - usbState.dataset.state = connected ? "online" : listening ? "listening" : "offline"; - } - const vehicleOnline = health.vehicle?.connected !== false; - const vehicleBadge = $("#v2HeadUnitState"); - if (vehicleBadge) { - vehicleBadge.dataset.state = vehicleOnline ? "online" : "offline"; - $("span", vehicleBadge).textContent = vehicleOnline ? "车机链路在线" : "车机链路离线"; - } const revision = Number(snapshot?.revision ?? -1); if (revision !== displaySync.revision) { displaySync.revision = revision; @@ -1417,13 +1426,9 @@ }); if (!response.ok) throw new Error(`HTTP ${response.status}`); applyDisplaySnapshot(await response.json()); - $("#v2MirrorState")?.setAttribute("data-state", "online"); - if ($("#v2MirrorState span")) $("#v2MirrorState span").textContent = "同步正常"; appendSyncLog({ key: "sync:online", category: "sync", title: "网页同步链路已建立", detail: "车机任务与设备状态每 2 秒同步", status: "success" }); } catch (_error) { console.warn("[vehicle-display-sync]", _error); - $("#v2MirrorState")?.setAttribute("data-state", "offline"); - if ($("#v2MirrorState span")) $("#v2MirrorState span").textContent = "等待同步服务"; } } @@ -1488,11 +1493,6 @@ const syncSidebar = make("section", "v2-sync-sidebar", `
等待自然语言解析结果
`); canCommandPanel.id = "v2CanCommandPanel"; - if (linkStack) syncOverview.insertBefore(canCommandPanel, linkStack); - else syncOverview.append(canCommandPanel); + syncOverview.append(canCommandPanel); canCommandPanel.addEventListener("toggle", () => { requestAnimationFrame(syncSidebarLogBaseline); }); @@ -1790,7 +1787,6 @@ if (requestStatus && statusTarget && requestStatus.parentElement !== statusTarget) { statusTarget.append(requestStatus); } - requestAnimationFrame(updateModeEdgeHints); const drawer = $("#gestureDrawer"); if (!triggerGesture) { const gestureVisible = ui.controlMode === "gesture"; @@ -1902,32 +1898,19 @@ } }); window.addEventListener("resize", updateScrollCues); - window.addEventListener("resize", updateModeEdgeHints); window.addEventListener("cognitive-agent:result", () => requestAnimationFrame(updateScrollCues)); - const modeChips = $(".v2-mode-rail .mode-chips"); - modeChips?.addEventListener( - "scroll", - () => requestAnimationFrame(updateModeEdgeHints), - { passive: true }, - ); - modeChips?.addEventListener("wheel", (event) => { - if (modeChips.scrollWidth <= modeChips.clientWidth + 2) return; - const distance = Math.abs(event.deltaX) > Math.abs(event.deltaY) - ? event.deltaX - : event.deltaY; - if (!distance) return; - event.preventDefault(); - modeChips.scrollBy({ left: distance, behavior: "smooth" }); - window.requestAnimationFrame(updateModeEdgeHints); - }, { passive: false }); - if (modeChips) { - new MutationObserver(() => requestAnimationFrame(updateModeEdgeHints)) - .observe(modeChips, { childList: true }); - } - window.addEventListener("cognitive-agent:connection", (event) => { syncRuntimePresentation(event.detail); }); + window.addEventListener("cognitive-agent:vehicle-feedback-state", () => { + renderSyncedDevices(); + if (displaySync.activeDevice) { + presentTwinDevice(displaySync.activeDevice, { + pinned: Boolean(displaySync.pinnedDevice), + selectSeat: false, + }); + } + }); const observer = new MutationObserver(() => keepRunButtonAvailable()); [$("#hardwareMode"), $("#simulationMode")] .filter(Boolean) @@ -1982,7 +1965,6 @@ syncRuntimePresentation(); window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); requestAnimationFrame(() => { - updateModeEdgeHints(); document.body.classList.add("v2-ready"); window.setTimeout(() => $("#v2BootScreen")?.remove(), 220); }); diff --git a/apps/web-console/hardware-control.css b/apps/web-console/hardware-control.css index a3b476a..b644bea 100644 --- a/apps/web-console/hardware-control.css +++ b/apps/web-console/hardware-control.css @@ -1,39 +1,76 @@ -:root { color-scheme: dark; font-family: Inter, "Microsoft YaHei", system-ui, sans-serif; background: #070b12; color: #edf4ff; } +:root { + color-scheme: light; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0.008 326); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0.008 326); + --primary: oklch(0.488 0.243 264.376); + --primary-foreground: oklch(0.97 0.014 254.604); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.96 0.003 325.6); + --muted-foreground: oklch(0.542 0.034 322.5); + --accent: oklch(0.96 0.003 325.6); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0.005 325.62); + --ring: oklch(0.711 0.019 323.02); + --radius: 0.625rem; + font-family: Inter, "Microsoft YaHei", system-ui, sans-serif; + background: var(--background); + color: var(--foreground); +} +.dark { + color-scheme: dark; + --background: oklch(0.145 0.008 326); + --foreground: oklch(0.985 0 0); + --card: oklch(0.212 0.019 322.12); + --card-foreground: oklch(0.985 0 0); + --primary: oklch(0.424 0.199 265.638); + --primary-foreground: oklch(0.97 0.014 254.604); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.263 0.024 320.12); + --muted-foreground: oklch(0.711 0.019 323.02); + --accent: oklch(0.263 0.024 320.12); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --ring: oklch(0.542 0.034 322.5); +} * { box-sizing: border-box; } -body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 18% 0%, #17294b 0, transparent 35%), #070b12; } +body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 18% 0%, color-mix(in oklch, var(--primary) 12%, transparent) 0, transparent 35%), var(--background); } .shell { width: min(1040px, calc(100% - 40px)); margin: 0 auto; padding: 54px 0 80px; } header, .section-title, .policy, .request, .lease-details { display: flex; align-items: center; justify-content: space-between; gap: 20px; } header { margin-bottom: 34px; } h1, h2, p { margin: 0; } h1 { font-size: clamp(30px, 5vw, 48px); letter-spacing: -.04em; } h2 { font-size: 22px; } -.eyebrow { color: #65a8ff; font-size: 12px; letter-spacing: .16em; font-weight: 800; margin-bottom: 8px; } -.lead, small { color: #8d9caf; } +.eyebrow { color: var(--primary); font-size: 12px; letter-spacing: .16em; font-weight: 800; margin-bottom: 8px; } +.lead, small { color: var(--muted-foreground); } .lead { margin-top: 12px; } a, button { font: inherit; } -a { color: #cfe4ff; text-decoration: none; border: 1px solid #2b3a50; border-radius: 10px; padding: 10px 14px; } -.policy, .card { background: rgba(15, 23, 36, .88); border: 1px solid #26354a; border-radius: 18px; box-shadow: 0 20px 60px rgba(0,0,0,.22); } +a { color: var(--primary); text-decoration: none; border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; } +.policy, .card { color: var(--card-foreground); background: color-mix(in oklch, var(--card) 94%, transparent); border: 1px solid var(--border); border-radius: calc(var(--radius) + 8px); box-shadow: 0 20px 60px oklch(0.145 0.008 326 / 12%); } .policy { padding: 18px 22px; justify-content: flex-start; margin-bottom: 18px; } .policy div { display: grid; gap: 3px; flex: 1; } -.dot { width: 10px; height: 10px; border-radius: 50%; background: #41d390; box-shadow: 0 0 16px #41d390; } -#clock { color: #8d9caf; font-variant-numeric: tabular-nums; } +.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--primary); box-shadow: 0 0 16px color-mix(in oklch, var(--primary) 55%, transparent); } +#clock { color: var(--muted-foreground); font-variant-numeric: tabular-nums; } .card { padding: 24px; margin-top: 18px; } -.active-card { border-color: #315a86; } +.active-card { border-color: var(--ring); } .section-title { margin-bottom: 20px; } -.count { min-width: 30px; height: 30px; display: grid; place-items: center; border-radius: 999px; background: #1a2d48; color: #7fbbff; font-weight: 800; } -.empty { padding: 26px 4px; color: #74849a; text-align: center; } +.count { min-width: 30px; height: 30px; display: grid; place-items: center; border-radius: 999px; background: var(--accent); color: var(--primary); font-weight: 800; } +.empty { padding: 26px 4px; color: var(--muted-foreground); text-align: center; } .request-list { display: grid; gap: 12px; } -.request { padding: 16px; border-radius: 13px; border: 1px solid #26364d; background: #0b121e; } +.request { padding: 16px; border-radius: calc(var(--radius) + 3px); border: 1px solid var(--border); background: var(--muted); } .identity { display: grid; gap: 5px; min-width: 0; } .identity strong { font-size: 17px; } -.identity code, .identity span { color: #8394aa; font-size: 12px; overflow-wrap: anywhere; } +.identity code, .identity span { color: var(--muted-foreground); font-size: 12px; overflow-wrap: anywhere; } .actions { display: flex; gap: 8px; } -button { color: #f4f8ff; background: #1577db; border: 0; border-radius: 9px; padding: 10px 14px; cursor: pointer; font-weight: 750; } +button { color: var(--primary-foreground); background: var(--primary); border: 0; border-radius: var(--radius); padding: 10px 14px; cursor: pointer; font-weight: 750; } button:hover { filter: brightness(1.12); } button:disabled { opacity: .45; cursor: wait; } -button.secondary { background: #243348; } -button.danger { background: #b83449; } -.lease-details { justify-content: flex-start; flex-wrap: wrap; padding: 18px; border-radius: 13px; background: #0b121e; border: 1px solid #284f78; } -.lease-details .timer { margin-left: auto; color: #4ce0a0; font-weight: 850; font-size: 22px; font-variant-numeric: tabular-nums; } -.message { margin-top: 18px; min-height: 24px; color: #7fbbff; } +button.secondary { color: var(--secondary-foreground); background: var(--secondary); } +button.danger { color: var(--primary-foreground); background: var(--destructive); } +.lease-details { justify-content: flex-start; flex-wrap: wrap; padding: 18px; border-radius: calc(var(--radius) + 3px); background: var(--muted); border: 1px solid var(--ring); } +.lease-details .timer { margin-left: auto; color: var(--primary); font-weight: 850; font-size: 22px; font-variant-numeric: tabular-nums; } +.message { margin-top: 18px; min-height: 24px; color: var(--primary); } @media (max-width: 680px) { .shell { width: min(100% - 24px, 1040px); padding-top: 26px; } header, .request, .lease-details { align-items: flex-start; flex-direction: column; } .actions { width: 100%; } .actions button { flex: 1; } .lease-details .timer { margin-left: 0; } } diff --git a/apps/web-console/hardware-control.html b/apps/web-console/hardware-control.html index d584c52..8580b42 100644 --- a/apps/web-console/hardware-control.html +++ b/apps/web-console/hardware-control.html @@ -3,8 +3,10 @@ + +请先在指令框输入需要固化的功能组合
正在检查 CAN 适配器与 IAR 固件配置
@@ -554,9 +544,9 @@修改自然语言后需要重新生成指令,才能点击完成永久保存。