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", `

511工程-智能座舱

-

运行日志 / 同步记录

@@ -1505,12 +1505,10 @@ const executionField = $(".field-group:has(.execution-mode-options)"); const simulationFaultField = $("#simulationFaultField"); if (syncOverview) { - const linkStack = $(".v2-sync-link-stack", syncOverview); const insertVisibleControl = (field, className) => { if (!field) return; field.classList.add(className); - if (linkStack) syncOverview.insertBefore(field, linkStack); - else syncOverview.append(field); + syncOverview.append(field); }; insertVisibleControl(transportField, "v2-sync-transport-field"); if (executionField) executionField.hidden = false; @@ -1526,8 +1524,7 @@

等待自然语言解析结果

`); 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 @@ + + 真实台架本地决策台 - +
diff --git a/apps/web-console/index.html b/apps/web-console/index.html index 80267d6..a0259f8 100755 --- a/apps/web-console/index.html +++ b/apps/web-console/index.html @@ -3,26 +3,26 @@ - - + + 511工程-智能座舱 - - - - + + + + - + - + @@ -37,29 +37,22 @@

说一句,就调好了

-
-
-
功能模式
- -
-
- - - -
-
-
- - +
+
+ 功能模式 + +
+ +
-
- diff --git a/apps/web-console/login.css b/apps/web-console/login.css index 53f4102..9c242c3 100755 --- a/apps/web-console/login.css +++ b/apps/web-console/login.css @@ -1,10 +1,52 @@ :root { - color: #17232b; - background: #e8efed; + 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); + --popover: oklch(1 0 0); + --popover-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); + --accent-foreground: oklch(0.212 0.019 322.12); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0.005 325.62); + --input: oklch(0.922 0.005 325.62); + --ring: oklch(0.711 0.019 323.02); + --radius: 0.625rem; + color: var(--foreground); + background: var(--background); font-family: Inter, "Noto Sans SC", "Microsoft YaHei", system-ui, sans-serif; letter-spacing: 0; } +.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); + --popover: oklch(0.212 0.019 322.12); + --popover-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); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.542 0.034 322.5); +} + * { box-sizing: border-box; } @@ -14,9 +56,9 @@ body { min-height: 100vh; margin: 0; background: - linear-gradient(rgba(217, 229, 225, 0.7) 1px, transparent 1px), - linear-gradient(90deg, rgba(217, 229, 225, 0.7) 1px, transparent 1px), - #eef3f1; + linear-gradient(color-mix(in oklch, var(--border) 70%, transparent) 1px, transparent 1px), + linear-gradient(90deg, color-mix(in oklch, var(--border) 70%, transparent) 1px, transparent 1px), + var(--muted); background-size: 34px 34px; } @@ -30,10 +72,12 @@ body { .login-panel { width: min(420px, 100%); padding: 28px; - background: #fff; - border: 1px solid #cbd7d3; - border-top: 4px solid #08766e; - box-shadow: 0 18px 50px rgba(24, 42, 48, 0.12); + color: var(--card-foreground); + background: var(--card); + border: 1px solid var(--border); + border-top: 4px solid var(--primary); + border-radius: var(--radius); + box-shadow: 0 18px 50px oklch(0.145 0.008 326 / 12%); } .login-brand { @@ -41,7 +85,7 @@ body { align-items: center; gap: 12px; padding-bottom: 24px; - border-bottom: 1px solid #d9e1de; + border-bottom: 1px solid var(--border); } .login-brand > span { @@ -49,8 +93,8 @@ body { height: 42px; display: grid; place-items: center; - color: #fff; - background: #08766e; + color: var(--primary-foreground); + background: var(--primary); border-radius: 5px; font-size: 13px; font-weight: 800; @@ -67,7 +111,7 @@ body { .login-brand small { margin-top: 4px; - color: #67747b; + color: var(--muted-foreground); font: 8px "SFMono-Regular", Consolas, monospace; } @@ -77,7 +121,7 @@ body { .login-heading p { margin: 0 0 7px; - color: #08766e; + color: var(--primary); font: 750 9px "SFMono-Regular", Consolas, monospace; } @@ -92,7 +136,7 @@ form { label { margin: 0 0 7px; - color: #4f5f66; + color: var(--muted-foreground); font-size: 11px; font-weight: 700; } @@ -102,32 +146,32 @@ input { height: 44px; margin-bottom: 18px; padding: 0 12px; - color: #17232b; - background: #f7f9f8; - border: 1px solid #bbc8c4; - border-radius: 4px; + color: var(--foreground); + background: var(--background); + border: 1px solid var(--input); + border-radius: calc(var(--radius) - 2px); font: inherit; letter-spacing: 0; } input:focus { - outline: 3px solid rgba(50, 105, 168, 0.16); - border-color: #3269a8; + outline: 3px solid color-mix(in oklch, var(--ring) 24%, transparent); + border-color: var(--ring); } .login-message { min-height: 17px; margin: -4px 0 10px; - color: #bd3f49; + color: var(--destructive); font-size: 11px; } button { height: 44px; - color: #fff; - background: #08766e; + color: var(--primary-foreground); + background: var(--primary); border: 0; - border-radius: 4px; + border-radius: calc(var(--radius) - 2px); font: inherit; font-weight: 750; letter-spacing: 0; @@ -135,7 +179,7 @@ button { } button:hover { - background: #075c57; + background: color-mix(in oklch, var(--primary) 86%, var(--foreground)); } button:disabled { @@ -148,7 +192,7 @@ footer { align-items: center; gap: 8px; margin-top: 26px; - color: #67747b; + color: var(--muted-foreground); font: 8px "SFMono-Regular", Consolas, monospace; } @@ -156,8 +200,8 @@ footer i { width: 7px; height: 7px; border-radius: 50%; - background: #08766e; - box-shadow: 0 0 0 4px rgba(8, 118, 110, 0.12); + background: var(--primary); + box-shadow: 0 0 0 4px color-mix(in oklch, var(--primary) 12%, transparent); } @media (max-width: 460px) { diff --git a/apps/web-console/login.html b/apps/web-console/login.html index 324e1fe..e2f35b3 100755 --- a/apps/web-console/login.html +++ b/apps/web-console/login.html @@ -3,9 +3,10 @@ - + + 登录 · Cognitive Agent - + diff --git a/apps/web-console/manifest.webmanifest b/apps/web-console/manifest.webmanifest index 50895e0..b9f5fd4 100644 --- a/apps/web-console/manifest.webmanifest +++ b/apps/web-console/manifest.webmanifest @@ -7,8 +7,8 @@ "scope": "/", "display": "standalone", "orientation": "any", - "background_color": "#eef3f1", - "theme_color": "#146b65", + "background_color": "oklch(1 0 0)", + "theme_color": "oklch(0.488 0.243 264.376)", "icons": [ { "src": "/icons/app-icon-192.png", diff --git a/apps/web-console/self-extension.css b/apps/web-console/self-extension.css index 4731701..0a8474f 100644 --- a/apps/web-console/self-extension.css +++ b/apps/web-console/self-extension.css @@ -1,11 +1,11 @@ /* Self-extension workspace ------------------------------------------------- */ -[data-extension-type="light"] { --extension-card-color: #eab858; } -[data-extension-type="sound"] { --extension-card-color: #658bd5; } -[data-extension-type="action"] { --extension-card-color: #db7468; } -[data-extension-type="doll"] { --extension-card-color: #cb86a4; } -[data-extension-type="mechanical"] { --extension-card-color: #758e94; } -[data-extension-type="other"] { --extension-card-color: #579a88; } +[data-extension-type="light"] { --extension-card-color: var(--chart-1); } +[data-extension-type="sound"] { --extension-card-color: var(--chart-2); } +[data-extension-type="action"] { --extension-card-color: var(--destructive); } +[data-extension-type="doll"] { --extension-card-color: var(--chart-3); } +[data-extension-type="mechanical"] { --extension-card-color: var(--chart-4); } +[data-extension-type="other"] { --extension-card-color: var(--chart-5); } .console-v2 .v2-extension-sidebar, .console-v2 .v2-extension-footer, @@ -62,7 +62,7 @@ z-index: 24; display: block; transform: translate3d(var(--pet-x), var(--pet-y), 0); - filter: drop-shadow(0 12px 14px rgba(9, 66, 63, 0.18)); + filter: drop-shadow(0 12px 14px color-mix(in oklch, var(--primary) 18%, transparent)); touch-action: none; user-select: none; } @@ -75,11 +75,11 @@ max-width: calc(100vw - 24px); box-sizing: border-box; padding: 10px 12px; - color: #285153; - background: rgba(255, 255, 255, 0.94); - border: 1px solid rgba(14, 119, 115, 0.18); + color: var(--popover-foreground); + background: color-mix(in oklch, var(--popover) 94%, transparent); + border: 1px solid color-mix(in oklch, var(--primary) 18%, transparent); border-radius: 11px 11px 2px 11px; - box-shadow: 0 9px 24px rgba(22, 72, 70, 0.12); + box-shadow: 0 9px 24px oklch(0.145 0.008 326 / 12%); font-size: 13px; font-weight: 800; line-height: 1.5; @@ -3543,3 +3543,353 @@ form > .v2-extension-primary { display: none; } } + +/* Final semantic theme layer for extension surfaces and nested dialogs --- */ +.console-v2 #v2ExtensionPanel .v2-extension-sync-mount, +.console-v2 #v2ExtensionPanel .v2-extension-gateway, +.console-v2 #v2ExtensionPanel .v2-extension-synced-device, +.console-v2 #v2ExtensionPanel .v2-nfc-inline, +.console-v2 #v2ExtensionPanel .v2-extension-empty { + color: var(--card-foreground); + background: color-mix(in oklch, var(--card) 86%, transparent); + border-color: var(--border); +} + +.console-v2 #v2ExtensionPanel .v2-extension-sync-mount { + background: + radial-gradient(circle at 84% 8%, color-mix(in oklch, var(--primary) 10%, transparent), transparent 18rem), + var(--card); +} + +.console-v2 #v2ExtensionPanel .v2-extension-sync-heading h3, +.console-v2 #v2ExtensionPanel .v2-extension-gateway strong, +.console-v2 #v2ExtensionPanel .v2-extension-synced-device header strong, +.console-v2 #v2ExtensionPanel .v2-nfc-inline strong, +.console-v2 #v2ExtensionPanel .v2-extension-empty strong, +.console-v2 #v2ExtensionPanel .v2-extension-linkage strong { + color: var(--foreground); +} + +.console-v2 #v2ExtensionPanel .v2-extension-sync-heading small, +.console-v2 #v2ExtensionPanel .v2-extension-gateway small, +.console-v2 #v2ExtensionPanel .v2-extension-synced-device header small, +.console-v2 #v2ExtensionPanel .v2-extension-synced-device header b, +.console-v2 #v2ExtensionPanel .v2-nfc-inline p, +.console-v2 #v2ExtensionPanel .v2-extension-empty, +.console-v2 #v2ExtensionPanel .v2-extension-linkage small { + color: var(--muted-foreground); +} + +.console-v2 #v2ExtensionPanel .v2-extension-gateway > span, +.console-v2 #v2ExtensionPanel .v2-extension-synced-device header > span, +.console-v2 #v2ExtensionPanel .v2-nfc-inline-icon { + color: var(--primary); + background: var(--accent); + border-color: color-mix(in oklch, var(--primary) 18%, var(--border)); +} + +.console-v2 #v2ExtensionPanel .v2-extension-gateway > b, +.console-v2 #v2ExtensionPanel .v2-extension-synced-device > div span, +.console-v2 #v2ExtensionPanel .v2-nfc-inline small, +.console-v2 #v2ExtensionPanel .v2-nfc-inline > div > div i, +.console-v2 #v2ExtensionPanel .v2-extension-linkage > span, +.console-v2 #v2ExtensionPanel .v2-extension-linkage > svg { + color: var(--primary); + background: var(--accent); +} + +.console-v2 #v2ExtensionPanel .v2-extension-linkage { + border-top-color: var(--border); +} + +.console-v2 #v2ExtensionPanel .v2-extension-nfc-launch, +.console-v2 #v2ExtensionPanel .v2-nfc-inline > button { + color: var(--primary-foreground); + background: var(--primary); + border-color: var(--primary); +} + +.console-v2 #v2ExtensionPanel .v2-extension-nfc-launch:hover, +.console-v2 #v2ExtensionPanel .v2-nfc-inline > button:hover { + color: var(--primary-foreground); + background: color-mix(in oklch, var(--primary) 86%, var(--foreground)); + border-color: var(--primary); +} + +.v2-extension-dialog, +.v2-nfc-dialog { + color: var(--popover-foreground); + background: var(--popover); + border: 1px solid var(--border); + box-shadow: 0 30px 90px color-mix(in oklch, var(--foreground) 24%, transparent); +} + +.v2-extension-dialog::backdrop { + background: color-mix(in oklch, var(--foreground) 50%, transparent); +} + +.v2-extension-dialog > header, +.v2-extension-dialog form > header, +.v2-nfc-dialog > header { + color: var(--foreground); + background: color-mix(in oklch, var(--popover) 94%, transparent); + border-bottom-color: var(--border); +} + +.v2-extension-dialog header p, +.v2-extension-dialog form > label, +.v2-extension-reset-dialog > label, +.v2-extension-type-chooser small, +.v2-extension-preset-preview > span, +.v2-extension-learn-steps, +.v2-extension-learn-fields label, +.v2-extension-learning-page p, +.v2-extension-test-page p, +.v2-gscu-status small, +.v2-gscu-status > b, +.v2-gscu-config-form > label, +.v2-gscu-actions label, +.v2-gscu-config-form label small, +.v2-gscu-actions > header small, +.v2-gscu-command-catalog > header small, +#extensionGscuNamedCommands article > header span, +#extensionGscuNamedCommands article label span, +.v2-nfc-waiting p, +.v2-nfc-detected-heading p, +.v2-nfc-recognized-heading p, +.v2-nfc-create-grid label > span, +.v2-nfc-existing > span, +.v2-nfc-capability-primary small, +.v2-nfc-recognized-actions > span, +.v2-nfc-footnote { + color: var(--muted-foreground); +} + +.v2-extension-dialog header > button, +.v2-extension-dialog input, +.v2-extension-dialog select, +.v2-extension-dialog textarea, +.v2-extension-type-chooser > button, +.v2-extension-dialog-actions button, +.v2-gscu-action-grid button, +.v2-gscu-advanced button, +#extensionGscuNamedCommands button, +#extensionGscuNamedCommands input, +.v2-nfc-create-grid input, +.v2-nfc-create-grid select, +.v2-nfc-existing select { + color: var(--foreground); + background: var(--background); + border-color: var(--input); +} + +.v2-extension-dialog header > button:hover, +.v2-extension-dialog header > button:focus-visible, +.v2-gscu-action-grid button:hover, +.v2-gscu-advanced button:hover, +#extensionGscuNamedCommands button:hover { + color: var(--accent-foreground); + background: var(--accent); + border-color: var(--ring); +} + +.v2-extension-dialog input:focus, +.v2-extension-dialog select:focus, +.v2-extension-dialog textarea:focus, +#extensionGscuNamedCommands input:focus, +.v2-nfc-create-grid input:focus, +.v2-nfc-create-grid select:focus, +.v2-nfc-existing select:focus { + border-color: var(--ring); + outline: none; + box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring) 20%, transparent); +} + +.v2-extension-primary, +.v2-extension-dialog-actions .v2-extension-primary, +.v2-nfc-primary-actions button, +.v2-nfc-recognized-actions button:not(.v2-extension-danger) { + color: var(--primary-foreground); + background: var(--primary); + border-color: var(--primary); +} + +.v2-extension-danger, +.v2-gscu-advanced button.danger, +#extensionGscuNamedCommands button.danger { + color: var(--primary-foreground); + background: var(--destructive); + border-color: var(--destructive); +} + +.v2-extension-type-chooser > button.active { + color: var(--primary); + background: color-mix(in oklch, var(--primary) 9%, var(--card)); + border-color: color-mix(in oklch, var(--primary) 30%, var(--border)); + box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--primary) 18%, transparent); +} + +.v2-extension-type-chooser > button > span, +.v2-extension-learn-target > span, +.v2-extension-learn-animation, +.v2-extension-preset-preview i, +.v2-extension-learn-steps b, +.v2-nfc-reader-visual, +.v2-nfc-detected-heading > span, +.v2-nfc-recognized-heading > span { + color: var(--primary); + background: var(--accent); + border-color: color-mix(in oklch, var(--primary) 20%, var(--border)); +} + +.v2-extension-preset-preview, +.v2-extension-learn-target, +.v2-gscu-status, +.v2-gscu-actions, +.v2-gscu-device-group, +.v2-gscu-device-command-grid > article, +.v2-gscu-result, +#extensionGscuNamedCommands > p, +.v2-nfc-stage, +.v2-nfc-detected-heading, +.v2-nfc-recognized-heading, +.v2-nfc-capability-card { + color: var(--card-foreground); + background: var(--card); + border-color: var(--border); +} + +.v2-extension-learn-steps > i, +.v2-gscu-advanced, +.v2-gscu-command-catalog, +.v2-gscu-result[open] summary, +.v2-nfc-relearn { + border-color: var(--border); +} + +.v2-extension-learn-steps span.active, +.v2-extension-learn-target small, +.v2-extension-learning-page > strong, +.v2-gscu-status[data-state="online"] > svg, +.v2-gscu-status[data-state="online"] > b, +.v2-gscu-status[data-state="listening"] > svg, +.v2-gscu-status[data-state="listening"] > b, +.v2-gscu-device-group > header small, +.v2-gscu-device-group > header > b, +#extensionGscuNamedCommands b, +.v2-nfc-waiting small, +.v2-nfc-detected-heading small, +.v2-nfc-recognized-heading small, +.v2-nfc-capability-heading small, +.v2-nfc-capability-primary svg, +.v2-nfc-footnote svg { + color: var(--primary); +} + +.v2-extension-learn-steps span.active b, +.v2-extension-learn-animation > span, +.v2-extension-test-page > span { + color: var(--primary-foreground); + background: var(--primary); + box-shadow: 0 0 0 12px color-mix(in oklch, var(--primary) 10%, transparent); +} + +.v2-gscu-status[data-state="online"] { + background: color-mix(in oklch, var(--primary) 8%, var(--card)); + border-color: color-mix(in oklch, var(--primary) 24%, var(--border)); +} + +.v2-gscu-status[data-state="listening"] { + background: color-mix(in oklch, var(--chart-2) 8%, var(--card)); + border-color: color-mix(in oklch, var(--chart-2) 24%, var(--border)); +} + +.v2-gscu-device-group > header strong, +.v2-gscu-device-group > header span, +.v2-gscu-result summary, +.v2-nfc-waiting strong, +.v2-nfc-detected-heading strong, +.v2-nfc-recognized-heading strong, +.v2-nfc-capability-primary span { + color: var(--foreground); +} + +#extensionGscuTcpResult { + color: var(--primary-foreground); + background: color-mix(in oklch, var(--foreground) 90%, var(--background)); +} + +.v2-nfc-stage > header { + color: var(--primary-foreground); + background: var(--primary); +} + +.v2-nfc-stage > header > span, +.v2-nfc-stage > header small, +.v2-nfc-stage > header h3, +.v2-nfc-stage > header p, +.v2-nfc-stage > header > b, +.v2-nfc-stage > header > b[data-state="online"] { + color: var(--primary-foreground); +} + +.v2-nfc-stage > header > span, +.v2-nfc-stage > header > b, +.v2-nfc-stage > header > b[data-state="online"] { + background: color-mix(in oklch, var(--primary-foreground) 12%, transparent); + border-color: color-mix(in oklch, var(--primary-foreground) 20%, transparent); +} + +.v2-nfc-inline[data-state="recognized"], +.v2-nfc-capability-card.learned { + background: color-mix(in oklch, var(--chart-1) 12%, var(--card)); + border-color: color-mix(in oklch, var(--chart-3) 26%, var(--border)); +} + +.v2-nfc-inline[data-state="new-card"], +.v2-nfc-capability-card.pending { + background: color-mix(in oklch, var(--chart-2) 10%, var(--card)); + border-color: color-mix(in oklch, var(--chart-2) 24%, var(--border)); +} + +.v2-nfc-relearn { + color: var(--primary); + background: var(--accent); +} + +.v2-nfc-relearn:hover { + color: var(--primary-foreground); + background: var(--primary); +} + +.v2-extension-toast { + color: var(--popover-foreground); + background: var(--popover); + border: 1px solid var(--border); + box-shadow: 0 14px 36px color-mix(in oklch, var(--foreground) 18%, transparent); +} + +.v2-extension-toast.success { + color: var(--primary-foreground); + background: var(--primary); + border-color: var(--primary); +} + +.v2-extension-toast.error { + color: var(--primary-foreground); + background: var(--destructive); + border-color: var(--destructive); +} + +.console-v2 #v2ExtensionPanel .v2-extension-gateway > button { + color: var(--primary); + background: var(--accent); + border-color: color-mix(in oklch, var(--primary) 22%, var(--border)); +} + +.console-v2 #v2ExtensionPanel .v2-extension-gateway > button:hover, +.console-v2 #v2ExtensionPanel .v2-extension-gateway > button:focus-visible { + color: var(--primary-foreground); + background: var(--primary); + border-color: var(--primary); +} diff --git a/apps/web-console/service-worker.js b/apps/web-console/service-worker.js index fbc7f34..87a56b0 100644 --- a/apps/web-console/service-worker.js +++ b/apps/web-console/service-worker.js @@ -1,19 +1,19 @@ -const CACHE_VERSION = "cognitive-agent-pad-v221"; +const CACHE_VERSION = "cognitive-agent-pad-v227"; const SHELL_CACHE = `${CACHE_VERSION}-shell`; const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`; const APP_SHELL = [ "/", "/index.html", - "/styles.css?v=dual-seat-16", + "/styles.css?v=dual-seat-21", "/network.js?v=android-pad-2", - "/app.js?v=dual-seat-109", + "/app.js?v=dual-seat-114", "/gesture-control.js?v=android-pad-9", "/pwa.js?v=android-pad-3", - "/console-v2.css?v=delivery-state-43", - "/console-v2.js?v=delivery-state-50", - "/self-extension.css?v=gscu-tcp-39", - "/self-extension.js?v=gscu-tcp-40", - "/speech-input.css?v=sensevoice-4", + "/console-v2.css?v=delivery-state-50", + "/console-v2.js?v=delivery-state-54", + "/self-extension.css?v=gscu-tcp-42", + "/self-extension.js?v=gscu-tcp-41", + "/speech-input.css?v=sensevoice-5", "/speech-input.js?v=sensevoice-6", "/speech-audio-worklet.js?v=sensevoice-4", "/assets/pets/self-extension-cat.png?v=3", diff --git a/apps/web-console/speech-input.css b/apps/web-console/speech-input.css index 7d72d54..3c85cdc 100644 --- a/apps/web-console/speech-input.css +++ b/apps/web-console/speech-input.css @@ -22,7 +22,7 @@ place-items: center; margin: 0; padding: 0; - color: #284647; + color: var(--foreground); background: transparent; border: 0; border-radius: 50%; @@ -35,19 +35,19 @@ .console-v2 .v2-request-form .speech-dictation:hover:not(:disabled), .console-v2 .v2-request-form .speech-dictation:focus-visible { - color: #08766e; - background: #eaf5f3; - box-shadow: 0 0 0 3px rgba(8, 118, 110, 0.08); + color: var(--primary); + background: color-mix(in oklch, var(--primary) 9%, var(--background)); + box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary) 8%, transparent); transform: translateY(-50%) scale(1.05); } .console-v2 .v2-request-form .speech-dictation:focus-visible { - outline: 2px solid rgba(8, 118, 110, 0.48); + outline: 2px solid var(--ring); outline-offset: 2px; } .console-v2 .v2-request-form .speech-dictation:disabled { - color: #9aacaa; + color: var(--muted-foreground); cursor: not-allowed; opacity: 0.56; } @@ -86,9 +86,9 @@ .console-v2 .v2-request-form .speech-dictation.is-listening, .console-v2 .v2-request-form .speech-dictation.is-listening:hover:not(:disabled), .console-v2 .v2-request-form .speech-dictation.is-listening:focus-visible { - color: #fff; - background: #132c2c; - box-shadow: 0 0 0 5px rgba(19, 44, 44, 0.09), 0 7px 18px rgba(17, 47, 46, 0.2); + color: var(--primary-foreground); + background: var(--primary); + box-shadow: 0 0 0 5px color-mix(in oklch, var(--primary) 9%, transparent), 0 7px 18px color-mix(in oklch, var(--primary) 20%, transparent); } .console-v2 .v2-request-form .speech-dictation.is-listening .speech-mic-icon, @@ -118,8 +118,8 @@ } .console-v2 .v2-request-form .speech-dictation.is-processing { - color: #08766e; - background: #e5f3f1; + color: var(--primary); + background: color-mix(in oklch, var(--primary) 10%, var(--background)); } .console-v2 .v2-request-form .speech-dictation.is-processing .speech-waveform { @@ -134,7 +134,7 @@ left: 194px; z-index: 12; overflow: hidden; - color: #5f7474; + color: var(--muted-foreground); font-size: 10px; font-weight: 650; line-height: 16px; @@ -145,29 +145,29 @@ } .console-v2 .v2-request-form .command-composer[data-speech-state="listening"] { - border-color: rgba(19, 76, 73, 0.36); - box-shadow: 0 12px 35px rgba(25, 72, 70, 0.1), inset 0 0 0 1px rgba(19, 76, 73, 0.08); + border-color: color-mix(in oklch, var(--primary) 36%, transparent); + box-shadow: 0 12px 35px color-mix(in oklch, var(--primary) 10%, transparent), inset 0 0 0 1px color-mix(in oklch, var(--primary) 8%, transparent); } .console-v2 .v2-request-form .command-composer[data-speech-state="listening"] .speech-status { - color: #183e3d; + color: var(--foreground); } .console-v2 .v2-request-form .command-composer[data-speech-state="processing"] .speech-status { - color: #39706d; + color: color-mix(in oklch, var(--primary) 72%, var(--foreground)); } .console-v2 .v2-request-form .command-composer[data-speech-state="success"] .speech-status { - color: #08766e; + color: var(--primary); } .console-v2 .v2-request-form #utterance[data-speech-uncertain="true"] { - background: linear-gradient(90deg, rgba(201, 132, 31, 0.055), transparent 42%); - box-shadow: inset 3px 0 0 rgba(201, 132, 31, 0.58); + background: linear-gradient(90deg, color-mix(in oklch, var(--chart-1) 10%, transparent), transparent 42%); + box-shadow: inset 3px 0 0 color-mix(in oklch, var(--chart-1) 58%, transparent); } .console-v2 .v2-request-form .command-composer[data-speech-state="error"] .speech-status { - color: #b54b42; + color: var(--destructive); } .console-v2 .v2-request-form .command-composer:not([data-speech-state="idle"]) .composer-hint { @@ -188,15 +188,15 @@ top: 50%; right: 98px; bottom: auto; - color: #75908e; + color: var(--muted-foreground); border-radius: 50%; transform: translateY(-50%); } .console-v2 .v2-request-form .context-reset:hover, .console-v2 .v2-request-form .context-reset:focus-visible { - color: #08766e; - background: #edf6f4; + color: var(--primary); + background: color-mix(in oklch, var(--primary) 8%, var(--background)); } .console-v2 .v2-request-form .composer-hint, diff --git a/apps/web-console/styles.css b/apps/web-console/styles.css index 407dd46..7e01bcf 100755 --- a/apps/web-console/styles.css +++ b/apps/web-console/styles.css @@ -1,34 +1,113 @@ :root { - --bg: #f2f5f4; - --surface: #ffffff; - --surface-muted: #f7f9f8; - --ink: #17232b; - --muted: #67747b; - --line: #d9e1de; - --line-strong: #bbc8c4; - --teal: #08766e; - --teal-dark: #075c57; - --teal-soft: #dcefeb; - --blue: #3269a8; - --blue-soft: #e5eef8; - --amber: #b86f0b; - --amber-soft: #fff1d9; - --red: #bd3f49; - --red-soft: #fbe7e8; - --shadow: 0 8px 24px rgba(25, 43, 49, 0.07); + 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); + --popover: oklch(1 0 0); + --popover-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); + --accent-foreground: oklch(0.212 0.019 322.12); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0.005 325.62); + --input: oklch(0.922 0.005 325.62); + --ring: oklch(0.711 0.019 323.02); + --chart-1: oklch(0.828 0.111 230.318); + --chart-2: oklch(0.685 0.169 237.323); + --chart-3: oklch(0.588 0.158 241.966); + --chart-4: oklch(0.5 0.134 242.749); + --chart-5: oklch(0.443 0.11 240.79); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0.008 326); + --sidebar-primary: oklch(0.546 0.245 262.881); + --sidebar-primary-foreground: oklch(0.97 0.014 254.604); + --sidebar-accent: oklch(0.96 0.003 325.6); + --sidebar-accent-foreground: oklch(0.212 0.019 322.12); + --sidebar-border: oklch(0.922 0.005 325.62); + --sidebar-ring: oklch(0.711 0.019 323.02); + --status-online: oklch(0.64 0.18 152); + --status-offline: oklch(0.577 0.245 27.325); + --command-channel: var(--amber); + --command-source: var(--chart-3); + --command-delivered: var(--status-online); + --command-pending: var(--muted-foreground); + --command-category: color-mix(in oklch, var(--primary) 62%, var(--destructive)); + --command-control: var(--foreground); + --command-frame: var(--muted-foreground); + --text-caption: 0.6875rem; + --text-control: 0.75rem; + --text-input: 0.8125rem; + --text-label: 0.75rem; + + /* Compatibility aliases for the existing non-Tailwind console. */ + --bg: var(--background); + --surface: var(--card); + --surface-muted: var(--muted); + --ink: var(--foreground); + --line: var(--border); + --line-strong: var(--input); + --teal: var(--primary); + --teal-dark: color-mix(in oklch, var(--primary) 80%, var(--foreground)); + --teal-soft: color-mix(in oklch, var(--primary) 10%, var(--background)); + --blue: var(--primary); + --blue-soft: color-mix(in oklch, var(--primary) 10%, var(--background)); + --amber: oklch(0.65 0.17 70); + --amber-soft: color-mix(in oklch, var(--amber) 12%, var(--background)); + --red: var(--destructive); + --red-soft: color-mix(in oklch, var(--destructive) 10%, var(--background)); + --shadow: 0 8px 24px oklch(0.145 0.008 326 / 7%); + --accent-bright: color-mix(in oklch, var(--primary) 82%, white); + --accent-ink: var(--primary); + --accent-wash: color-mix(in oklch, var(--primary) 9%, var(--background)); + --soft-shadow: 0 18px 50px oklch(0.145 0.008 326 / 9%); font-family: Inter, "Noto Sans SC", "Microsoft YaHei", system-ui, sans-serif; color: var(--ink); background: var(--bg); letter-spacing: 0; } -/* Unified natural-language + gesture experience */ -:root { - --accent: #0a8178; - --accent-bright: #16a394; - --accent-ink: #075b56; - --accent-wash: #e7f6f2; - --soft-shadow: 0 18px 50px rgba(27, 58, 58, 0.09); +.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); + --popover: oklch(0.212 0.019 322.12); + --popover-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); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.542 0.034 322.5); + --chart-1: oklch(0.828 0.111 230.318); + --chart-2: oklch(0.685 0.169 237.323); + --chart-3: oklch(0.588 0.158 241.966); + --chart-4: oklch(0.5 0.134 242.749); + --chart-5: oklch(0.443 0.11 240.79); + --sidebar: oklch(0.212 0.019 322.12); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.623 0.214 259.815); + --sidebar-primary-foreground: oklch(0.97 0.014 254.604); + --sidebar-accent: oklch(0.263 0.024 320.12); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.542 0.034 322.5); + --status-online: oklch(0.76 0.18 152); + --status-offline: oklch(0.704 0.191 22.216); } html { @@ -37,8 +116,8 @@ html { body { background: - radial-gradient(circle at 84% -8%, rgba(59, 183, 166, 0.12), transparent 27rem), - linear-gradient(180deg, #f6f9f8 0, #eef3f2 100%); + radial-gradient(circle at 84% -8%, color-mix(in oklch, var(--primary) 12%, transparent), transparent 27rem), + linear-gradient(180deg, var(--background) 0, var(--muted) 100%); } body.gesture-open { @@ -80,12 +159,13 @@ body.mode-editor-open { .panel-intro { margin: 0 0 20px; - color: var(--muted); + color: var(--muted-foreground); font-size: 13px; line-height: 1.7; } .mode-rail { + position: relative; margin-bottom: 18px; padding: 14px; border: 1px solid rgba(8, 118, 110, 0.14); @@ -110,7 +190,7 @@ body.mode-editor-open { } .mode-rail-heading small { - color: var(--muted); + color: var(--muted-foreground); font-size: 9px; font-weight: 600; } @@ -194,7 +274,7 @@ body.mode-editor-open { .mode-chips svg { width: 15px; height: 15px; - color: var(--accent); + color: var(--primary); } .mode-rail-controls { @@ -269,7 +349,7 @@ body.mode-editor-open { padding: 0 5px; border-radius: 999px; color: #fff; - background: var(--accent); + background: var(--primary); font-size: 9px; } @@ -298,7 +378,7 @@ body.mode-editor-open { overflow: hidden; color: var(--ink); background: - radial-gradient(circle at 98% 2%, color-mix(in srgb, var(--v2-accent, var(--accent)) 12%, white), transparent 17rem), + radial-gradient(circle at 98% 2%, color-mix(in srgb, var(--v2-accent, var(--primary)) 12%, white), transparent 17rem), #f9fbfa; border: 1px solid rgba(255, 255, 255, 0.84); border-radius: 24px; @@ -323,7 +403,7 @@ body.mode-editor-open { .mode-gallery-kicker { display: block; margin-bottom: 4px; - color: var(--v2-accent, var(--accent)); + color: var(--v2-accent, var(--primary)); font-size: 9px; font-weight: 900; letter-spacing: 0.17em; @@ -357,7 +437,7 @@ body.mode-editor-open { } .mode-gallery-close:hover { - color: var(--v2-accent, var(--accent)); + color: var(--v2-accent, var(--primary)); background: #fff; } @@ -371,7 +451,7 @@ body.mode-editor-open { align-items: center; justify-content: space-between; padding: 8px 11px; - border: 1px solid color-mix(in srgb, var(--v2-accent, var(--accent)) 12%, white); + border: 1px solid color-mix(in srgb, var(--v2-accent, var(--primary)) 12%, white); border-radius: 11px; color: #5e7273; background: color-mix(in srgb, var(--v2-accent-soft, var(--accent-wash)) 42%, white); @@ -388,7 +468,7 @@ body.mode-editor-open { .mode-gallery-summary svg { width: 14px; height: 14px; - color: var(--v2-accent, var(--accent)); + color: var(--v2-accent, var(--primary)); } .mode-gallery-summary strong { @@ -442,7 +522,7 @@ body.mode-editor-open { .mode-gallery-group > header svg { width: 17px; height: 17px; - color: var(--v2-accent, var(--accent)); + color: var(--v2-accent, var(--primary)); } .mode-gallery-group > header strong { @@ -566,7 +646,7 @@ body.mode-editor-open { .mode-gallery-card:hover { transform: translateY(-1px); - border-color: color-mix(in srgb, var(--v2-accent, var(--accent)) 35%, white); + border-color: color-mix(in srgb, var(--v2-accent, var(--primary)) 35%, white); box-shadow: 0 8px 20px rgba(26, 78, 76, 0.09); } @@ -580,7 +660,7 @@ body.mode-editor-open { display: inline-grid; place-items: center; border-radius: 10px; - color: var(--v2-accent, var(--accent)); + color: var(--v2-accent, var(--primary)); background: color-mix(in srgb, var(--v2-accent-soft, var(--accent-wash)) 76%, white); } @@ -691,7 +771,7 @@ body.mode-editor-open { .mode-gallery-empty svg { width: 17px; - color: var(--v2-accent, var(--accent)); + color: var(--v2-accent, var(--primary)); } .mode-gallery-empty span { @@ -719,7 +799,7 @@ body.mode-editor-open { .mode-gallery-footer svg { width: 13px; height: 13px; - color: var(--v2-accent, var(--accent)); + color: var(--v2-accent, var(--primary)); } .mode-editor-backdrop { @@ -807,7 +887,7 @@ body.mode-editor-open { } .mode-editor-header > div > span { - color: var(--v2-accent, var(--accent)); + color: var(--v2-accent, var(--primary)); font-size: 9px; font-weight: 900; letter-spacing: 0.16em; @@ -892,7 +972,7 @@ body.mode-editor-open { .mode-editor-field input:focus, .mode-editor-field textarea:focus { - border-color: color-mix(in srgb, var(--v2-accent, var(--accent)) 58%, white); + border-color: color-mix(in srgb, var(--v2-accent, var(--primary)) 58%, white); box-shadow: 0 0 0 3px color-mix(in srgb, var(--v2-accent-soft, var(--accent-wash)) 70%, transparent); } @@ -1209,7 +1289,7 @@ body.mode-editor-open { .mode-editor-actions .mode-editor-complete { color: #fff; - background: var(--v2-accent, var(--accent)); + background: var(--v2-accent, var(--primary)); } .mode-editor-actions button:disabled { @@ -1251,7 +1331,7 @@ body.mode-editor-open { .save-mode-source small { display: block; margin-bottom: 3px; - color: var(--muted); + color: var(--muted-foreground); font-size: 9px; } @@ -1266,7 +1346,7 @@ body.mode-editor-open { } .save-mode-source p.empty { - color: var(--muted); + color: var(--muted-foreground); } .save-mode-actions { @@ -1281,13 +1361,13 @@ body.mode-editor-open { border-radius: 9px; padding: 6px 12px; color: #fff; - background: var(--accent); + background: var(--primary); font-weight: 750; cursor: pointer; } .save-mode-composer .save-mode-actions button:first-child { - color: var(--muted); + color: var(--muted-foreground); background: #edf2f1; } @@ -2002,7 +2082,7 @@ label, display: inline-flex; align-items: center; justify-content: center; - color: var(--muted); + color: var(--muted-foreground); background: transparent; border: 0; border-radius: 4px; @@ -2089,7 +2169,7 @@ textarea:focus { border: 0; border-radius: 4px; background: transparent; - color: var(--muted); + color: var(--muted-foreground); font-size: 12px; font-weight: 700; cursor: pointer; @@ -2129,7 +2209,7 @@ textarea:focus { .provider-note { min-height: 32px; margin: 8px 0 0; - color: var(--muted); + color: var(--muted-foreground); font-size: 11px; line-height: 1.5; } @@ -2186,7 +2266,7 @@ textarea:focus { .request-header p { margin: 7px 0 0; - color: var(--muted); + color: var(--muted-foreground); font-family: "SFMono-Regular", Consolas, monospace; font-size: 11px; } @@ -2199,7 +2279,7 @@ textarea:focus { } .run-status.idle { - color: var(--muted); + color: var(--muted-foreground); background: #e7eceb; } @@ -2247,7 +2327,7 @@ textarea:focus { .matrix-stat span { margin-top: 2px; - color: var(--muted); + color: var(--muted-foreground); font-size: 9px; } @@ -2322,13 +2402,13 @@ textarea:focus { .domain-panel header small { margin-top: 2px; - color: var(--muted); + color: var(--muted-foreground); font: 8px "SFMono-Regular", Consolas, monospace; } .device-state { padding: 4px 5px; - color: var(--muted); + color: var(--muted-foreground); background: #edf1f0; border-radius: 3px; font: 800 8px "SFMono-Regular", Consolas, monospace; @@ -2361,7 +2441,7 @@ textarea:focus { } .zone-temperature span { - color: var(--muted); + color: var(--muted-foreground); font-size: 9px; } @@ -2560,7 +2640,7 @@ textarea:focus { } .domain-readouts dt { - color: var(--muted); + color: var(--muted-foreground); } .domain-readouts dd { @@ -2648,7 +2728,7 @@ textarea:focus { min-width: 58px; height: 31px; padding: 0 12px; - color: var(--muted); + color: var(--muted-foreground); background: transparent; border: 0; border-radius: 4px; @@ -2894,7 +2974,7 @@ textarea:focus { .twin-readout-heading > div span { margin-top: 2px; - color: var(--muted); + color: var(--muted-foreground); font: 9px "SFMono-Regular", Consolas, monospace; } @@ -2924,7 +3004,7 @@ textarea:focus { align-items: center; gap: 6px; position: relative; - color: var(--muted); + color: var(--muted-foreground); font-size: 10px; } @@ -2976,7 +3056,7 @@ textarea:focus { gap: 6px; height: 42px; padding: 0 8px; - color: var(--muted); + color: var(--muted-foreground); background: rgba(255, 255, 255, 0.58); border: 1px solid rgba(203, 215, 211, 0.8); border-radius: 4px; @@ -3053,7 +3133,7 @@ textarea:focus { left: 28px; bottom: 24px; z-index: 5; - color: var(--muted); + color: var(--muted-foreground); font-size: 10px; } @@ -3097,7 +3177,7 @@ textarea:focus { } .simulation-label { - color: var(--muted); + color: var(--muted-foreground); font: 700 9px "SFMono-Regular", Consolas, monospace; } @@ -3131,7 +3211,7 @@ textarea:focus { gap: 7px; padding-bottom: 7px; overflow: hidden; - color: var(--muted); + color: var(--muted-foreground); font-size: 10px; } @@ -3193,7 +3273,7 @@ textarea:focus { .seat-identity span { margin-top: 2px; - color: var(--muted); + color: var(--muted-foreground); font: 10px "SFMono-Regular", Consolas, monospace; } @@ -3210,7 +3290,7 @@ textarea:focus { align-items: center; gap: 4px; padding: 6px 7px; - color: var(--muted); + color: var(--muted-foreground); background: var(--surface-muted); border: 1px solid #e7ecea; border-radius: 4px; @@ -3223,7 +3303,7 @@ textarea:focus { } .thermal-states b { - color: var(--muted); + color: var(--muted-foreground); } .thermal-states > span.on[data-function="ventilation"] { @@ -3267,7 +3347,7 @@ textarea:focus { } .coverage-section dt { - color: var(--muted); + color: var(--muted-foreground); } .coverage-section dd { @@ -3307,7 +3387,7 @@ textarea:focus { min-height: 48px; gap: 7px; padding: 0 13px; - color: var(--muted); + color: var(--muted-foreground); background: transparent; border: 0; border-bottom: 2px solid transparent; @@ -3344,7 +3424,7 @@ textarea:focus { } .detail-heading span { - color: var(--muted); + color: var(--muted-foreground); font: 10px "SFMono-Regular", Consolas, monospace; } @@ -3413,7 +3493,7 @@ textarea:focus { } .trace-event small { - color: var(--muted); + color: var(--muted-foreground); } .validation-block { @@ -3593,7 +3673,7 @@ pre { .execution-summary span { margin-top: 3px; - color: var(--muted); + color: var(--muted-foreground); font-size: 11px; } @@ -3629,7 +3709,7 @@ pre { .result-item span { margin-top: 4px; - color: var(--muted); + color: var(--muted-foreground); font-size: 11px; overflow-wrap: anywhere; } @@ -3696,7 +3776,7 @@ pre { .joint-twin-heading p { margin: 6px 0 0; - color: var(--muted); + color: var(--muted-foreground); font-size: 11px; } @@ -4147,7 +4227,7 @@ pre { .config-panel-heading p { margin: 5px 0 0; - color: var(--muted); + color: var(--muted-foreground); font-size: 10px; } @@ -4194,7 +4274,7 @@ pre { place-items: center; align-content: center; gap: 7px; - color: var(--muted); + color: var(--muted-foreground); background: white; border: 1px dashed #c8d4d0; border-radius: 6px; @@ -4342,7 +4422,7 @@ pre { .config-card-identity small { margin-bottom: 3px; - color: var(--muted); + color: var(--muted-foreground); font: 7px "SFMono-Regular", Consolas, monospace; } @@ -4410,7 +4490,7 @@ pre { } .config-parameter > span { - color: var(--muted); + color: var(--muted-foreground); font-size: 8px; } @@ -4459,7 +4539,7 @@ pre { .config-route-node small { margin-bottom: 3px; - color: var(--muted); + color: var(--muted-foreground); font-size: 7px; } @@ -4608,7 +4688,7 @@ pre { .result-workspace-heading p, .workspace-panel-title p { margin: 5px 0 0; - color: var(--muted); + color: var(--muted-foreground); font-size: 10px; } @@ -5245,7 +5325,7 @@ pre { } .vehicle-transport-options .mode-option[data-connection-state="offline"] { - color: var(--muted); + color: var(--muted-foreground); } .vehicle-transport-options .mode-option[data-connection-state="online"]::after { @@ -5347,3 +5427,455 @@ pre { padding-top: 18px; } } + +/* Final semantic theme layer --------------------------------------------- + Keep legacy modules and every overlay on the shared shadcn token system. */ +.mode-gallery-backdrop, +.mode-editor-backdrop { + background: color-mix(in oklch, var(--foreground) 48%, transparent); +} + +.mode-gallery, +.mode-editor { + color: var(--popover-foreground); + background: + radial-gradient(circle at 98% 2%, color-mix(in oklch, var(--primary) 10%, transparent), transparent 17rem), + var(--popover); + border-color: var(--border); + box-shadow: 0 30px 90px color-mix(in oklch, var(--foreground) 22%, transparent); +} + +.mode-gallery-header h2, +.mode-gallery-group > header strong, +.mode-gallery-card, +.mode-gallery-empty strong, +.mode-editor-header h2, +.mode-editor-field input, +.mode-editor-field textarea, +.mode-editor-flow-preview, +.mode-editor-command-card > summary > code { + color: var(--foreground); +} + +.mode-gallery-header p, +.mode-gallery-group > header small, +.mode-gallery-state, +.mode-gallery-card-description, +.mode-gallery-empty, +.mode-gallery-footer, +.mode-editor-header p, +.mode-editor-field, +.mode-editor-flow-preview.empty, +.mode-editor-command-meta strong, +.mode-editor-message-sequence > header, +.mode-editor-message-sequence > p, +.mode-editor-status { + color: var(--muted-foreground); +} + +.mode-gallery-close, +.mode-gallery-group, +.mode-gallery-card, +.mode-gallery-empty, +.mode-editor-header button, +.mode-editor-field input, +.mode-editor-field textarea, +.mode-editor-command-card, +.mode-editor-actions button, +.save-mode-composer, +.save-mode-composer input { + background: var(--card); + border-color: var(--border); +} + +.mode-gallery-close, +.mode-editor-header button, +.mode-editor-actions button { + color: var(--muted-foreground); +} + +.mode-gallery-close:hover, +.mode-editor-header button:hover, +.mode-editor-header button:focus-visible, +.mode-editor-actions button:hover, +.mode-editor-actions button:focus-visible { + color: var(--accent-foreground); + background: var(--accent); + border-color: var(--ring); +} + +.mode-gallery-summary, +.mode-gallery-card-icon, +.mode-editor-flow, +.mode-editor-command-card, +.mode-editor-message-sequence, +.mode-editor-actions .mode-editor-execute { + background: var(--muted); + border-color: var(--border); +} + +.mode-gallery-summary, +.mode-gallery-summary strong, +.mode-gallery-card-icon, +.mode-editor-flow header, +.mode-editor-flow header svg, +.mode-editor-flow header b, +.mode-editor-actions .mode-editor-execute { + color: var(--primary); +} + +.mode-gallery-edit { + color: var(--primary); + background: color-mix(in oklch, var(--primary) 9%, var(--card)); + border-color: color-mix(in oklch, var(--primary) 24%, var(--border)); +} + +.mode-gallery-edit:hover { + color: var(--primary-foreground); + background: var(--primary); + border-color: var(--primary); +} + +.mode-gallery-delete { + color: var(--destructive); + background: color-mix(in oklch, var(--destructive) 8%, var(--card)); + border-color: color-mix(in oklch, var(--destructive) 24%, var(--border)); +} + +.mode-gallery-delete:hover { + color: var(--primary-foreground); + background: var(--destructive); + border-color: var(--destructive); +} + +.mode-gallery-edit:focus-visible, +.mode-gallery-delete:focus-visible, +.mode-editor-field input:focus, +.mode-editor-field textarea:focus, +.save-mode-composer input:focus { + border-color: var(--ring); + outline-color: var(--ring); + box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring) 20%, transparent); +} + +.mode-gallery-card:hover { + border-color: color-mix(in oklch, var(--primary) 28%, var(--border)); + box-shadow: 0 8px 20px color-mix(in oklch, var(--primary) 9%, transparent); +} + +.mode-gallery-card.running { + color: var(--primary); + background: color-mix(in oklch, var(--primary) 10%, var(--card)); + border-color: color-mix(in oklch, var(--primary) 36%, var(--border)); + box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary) 9%, transparent); +} + +.mode-gallery-card.running .mode-gallery-card-icon, +.mode-gallery-card.running .mode-gallery-state { + color: var(--primary); + background: var(--accent); +} + +.mode-gallery-card.succeeded { + color: color-mix(in oklch, var(--chart-3) 72%, var(--foreground)); + background: color-mix(in oklch, var(--chart-1) 14%, var(--card)); + border-color: color-mix(in oklch, var(--chart-3) 32%, var(--border)); +} + +.mode-gallery-card.succeeded .mode-gallery-state { + color: var(--chart-3); +} + +.mode-gallery-card.failed { + color: var(--destructive); + background: color-mix(in oklch, var(--destructive) 9%, var(--card)); + border-color: color-mix(in oklch, var(--destructive) 32%, var(--border)); +} + +.mode-gallery-card.failed .mode-gallery-state { + color: var(--destructive); +} + +.mode-editor-flow header { + background: color-mix(in oklch, var(--primary) 7%, var(--card)); + border-bottom: 1px solid var(--border); +} + +.mode-editor-flow-tools button { + color: var(--primary); + background: var(--card); + border-color: var(--border); +} + +.mode-editor-flow-tools button:hover, +.mode-editor-flow-tools button:focus-visible { + color: var(--primary-foreground); + background: var(--primary); + border-color: var(--primary); +} + +.mode-editor-command-meta b, +.mode-editor-command-card[data-can-channel="CAN0"] .mode-editor-command-meta b { + color: var(--primary); + background: var(--accent); + border-color: var(--border); +} + +.mode-editor-message-sequence li { + color: var(--primary-foreground); + background: color-mix(in oklch, var(--foreground) 90%, var(--background)); + border-color: color-mix(in oklch, var(--primary) 24%, transparent); +} + +.mode-editor-message-sequence li > div, +.mode-editor-message-sequence li > div code, +.mode-editor-message-sequence li > pre { + color: var(--primary-foreground); + background: transparent; +} + +.mode-editor-actions .mode-editor-complete { + color: var(--primary-foreground); + background: var(--primary); + border-color: var(--primary); +} + +.domain-panel, +.seat-card, +.config-card, +.result-panel, +.tab-panel { + color: var(--card-foreground); + background: var(--card); + border-color: var(--border); +} + +.domain-icon, +.config-card-icon, +.config-scene-icon, +.mode-gallery-card-icon { + color: var(--primary); + background: var(--accent); + border-color: color-mix(in oklch, var(--primary) 18%, var(--border)); +} + +.gesture-drawer { + color: var(--card-foreground); + background: var(--card); + border-color: var(--border); + box-shadow: -20px 0 60px color-mix(in oklch, var(--foreground) 18%, transparent); +} + +.gesture-drawer-header h2, +.gesture-now strong, +.gesture-guide-card strong, +.gesture-card-heading strong { + color: var(--foreground); +} + +.gesture-drawer-header p, +.gesture-status-strip, +.gesture-now span, +.gesture-guide-card p, +.gesture-card-heading span { + color: var(--muted-foreground); +} + +.gesture-state-pill, +.gesture-now, +.gesture-guide-card, +.gesture-customize { + color: var(--secondary-foreground); + background: var(--secondary); + border-color: var(--border); +} + +.gesture-state-pill.armed, +.gesture-state-pill.moving { + color: var(--primary); + background: color-mix(in oklch, var(--primary) 9%, var(--card)); + border-color: color-mix(in oklch, var(--primary) 28%, var(--border)); +} + +.gesture-camera-empty > span, +.gesture-live-badge { + color: var(--primary); + background: color-mix(in oklch, var(--primary) 10%, var(--popover)); + border-color: color-mix(in oklch, var(--primary) 28%, var(--border)); +} + +.toast { + color: var(--popover-foreground); + background: var(--popover); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: 0 14px 36px color-mix(in oklch, var(--foreground) 16%, transparent); +} + +.mode-cycle-badge, +.dynamic-mode-state { + color: var(--primary); + background: color-mix(in oklch, var(--primary) 9%, var(--card)); + border-color: color-mix(in oklch, var(--primary) 24%, var(--border)); +} + +.dynamic-mode-state b, +.dynamic-mode-state.running b { + color: var(--foreground); +} + +.vehicle-transport-options .mode-option[data-connection-state="online"]::after { + background: var(--status-online); + box-shadow: 0 0 0 3px color-mix(in oklch, var(--status-online) 18%, transparent); +} + +/* Compact, consistent control typography and explicit connection states. */ +.execution-mode-options { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.vehicle-transport-options .mode-option[data-connection-state]::after { + content: ""; + width: 8px; + height: 8px; + flex: 0 0 8px; + margin-left: 4px; + border: 1px solid color-mix(in oklch, var(--card) 72%, transparent); + border-radius: 50%; +} + +.vehicle-transport-options .mode-option[data-connection-state="offline"]::after { + background: var(--status-offline); + box-shadow: 0 0 0 3px color-mix(in oklch, var(--status-offline) 15%, transparent); +} + +.mode-rail > .mode-rail-bar { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 9px; +} + +.mode-rail-title { + color: var(--foreground); + font-size: var(--text-label); + font-weight: 850; + white-space: nowrap; +} + +.mode-rail-toggle, +.mode-rail-bar > .mode-gallery-toggle { + 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); +} + +.mode-rail-toggle { + justify-content: space-between; +} + +.mode-rail-toggle svg, +.mode-rail-bar > .mode-gallery-toggle svg { + width: 16px; + height: 16px; +} + +.mode-rail-toggle svg:last-child { + transition: transform 160ms ease; +} + +.mode-rail-toggle[aria-expanded="true"] svg:last-child { + transform: rotate(180deg); +} + +.mode-rail-content[hidden] { + display: none !important; +} + +.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; + margin-top: 0; + padding: 10px; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--soft-shadow); +} + +.mode-rail-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.mode-rail-actions > button { + min-height: 38px; + display: inline-flex; + 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); + white-space: nowrap; +} + +.mode-rail-content .mode-chips { + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); + gap: 8px; + overflow: visible; +} + +.mode-rail-content .mode-chips > button { + width: 100%; + min-width: 0; +} + +.mode-gallery-add-card { + 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)); +} + +.mode-gallery-add-card .mode-gallery-card-icon { + color: var(--primary); + background: color-mix(in oklch, var(--primary) 12%, var(--card)); +} + +.mode-gallery-add-card:hover, +.mode-gallery-add-card:focus-visible { + border-color: var(--primary); + background: var(--accent); +} + +@media (max-width: 760px) { + .mode-rail > .mode-rail-bar { + grid-template-columns: auto minmax(0, 1fr); + } + + .mode-rail-actions { + grid-column: 1 / -1; + justify-content: flex-end; + } +} diff --git a/src/cognitive_agent/application/seat_service.py b/src/cognitive_agent/application/seat_service.py index 3908e5f..bc04595 100755 --- a/src/cognitive_agent/application/seat_service.py +++ b/src/cognitive_agent/application/seat_service.py @@ -671,8 +671,43 @@ class SeatDemoService: ) -> dict[str, Any]: """Persist one edited mode from a server-generated control flow.""" + return self._persist_mode_profile_card( + mode_id, + name, + instruction, + source_request_id, + vehicle_id, + ) + + def create_mode_profile_card( + self, + name: str, + instruction: str, + source_request_id: str, + vehicle_id: str | None = None, + ) -> dict[str, Any]: + """Create one user mode from a server-generated control flow.""" + + return self._persist_mode_profile_card( + None, + name, + instruction, + source_request_id, + vehicle_id, + ) + + def _persist_mode_profile_card( + self, + mode_id: str | None, + name: str, + instruction: str, + source_request_id: str, + vehicle_id: str | None = None, + ) -> dict[str, Any]: + """Validate and persist a created or edited mode command flow.""" + resolved_vehicle_id = vehicle_id or self.vehicle_profiles.default().id - clean_mode_id = str(mode_id).strip() + clean_mode_id = str(mode_id or "").strip() clean_name = str(name).strip() clean_instruction = str(instruction).strip() clean_request_id = str(source_request_id).strip() @@ -690,15 +725,19 @@ class SeatDemoService: visible_profiles = self.mode_profiles._profiles_for_vehicle( # noqa: SLF001 resolved_vehicle_id ) - profile = next( - (item for item in visible_profiles if item.id == clean_mode_id), - None, + profile = ( + next( + (item for item in visible_profiles if item.id == clean_mode_id), + None, + ) + if clean_mode_id + else None ) - if profile is None: + if clean_mode_id and profile is None: raise KeyError(clean_mode_id) normalized_name = re.sub(r"\s+", "", clean_name).casefold() if any( - item.id != profile.id + (profile is None or item.id != profile.id) and re.sub(r"\s+", "", item.name).casefold() == normalized_name for item in visible_profiles ): @@ -813,14 +852,18 @@ class SeatDemoService: saved = self.mode_profiles.save_mode( clean_name, operations, - mode_id=profile.id, - aliases=list(profile.aliases), + mode_id=profile.id if profile is not None else None, + aliases=list(profile.aliases) if profile is not None else [], vehicle_id=resolved_vehicle_id, - description=profile.description, - icon=profile.icon, + description=( + profile.description + if profile is not None + else "由自然语言生成的自定义模式。" + ), + icon=profile.icon if profile is not None else "bookmark-plus", instruction=clean_instruction, command_flow=command_flow, - timeline=profile.timeline, + timeline=profile.timeline if profile is not None else None, ) public_profile = next( ( diff --git a/src/cognitive_agent/server.py b/src/cognitive_agent/server.py index 4433a93..87be433 100755 --- a/src/cognitive_agent/server.py +++ b/src/cognitive_agent/server.py @@ -418,7 +418,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler): if path.startswith("/api/self-repair/"): self._handle_self_repair_post(path) return - if path.startswith("/api/mode-profiles/"): + if path == "/api/mode-profiles" or path.startswith("/api/mode-profiles/"): self._handle_mode_profile_post(path) return supplement_match = re.fullmatch( @@ -1506,8 +1506,9 @@ class SeatDemoHandler(BaseHTTPRequestHandler): self._handle_mode_profile_execute(execute_match.group("mode_id")) return + create_mode = path == "/api/mode-profiles" mode_id = path.removeprefix("/api/mode-profiles/").strip() - if not mode_id: + if not create_mode and not mode_id: self._json( {"error": "invalid_request", "message": "模式编号不能为空。"}, HTTPStatus.BAD_REQUEST, @@ -1515,14 +1516,18 @@ class SeatDemoHandler(BaseHTTPRequestHandler): return try: payload = self._read_json_body(max_bytes=16 * 1024) - result = self.service.save_mode_profile_card( - mode_id, + arguments = ( str(payload.get("name", "")), str(payload.get("instruction", "")), str(payload.get("source_request_id", "")), str(payload.get("vehicle_id", "")) or None, ) - self._json(result, HTTPStatus.OK) + result = ( + self.service.create_mode_profile_card(*arguments) + if create_mode + else self.service.save_mode_profile_card(mode_id, *arguments) + ) + self._json(result, HTTPStatus.CREATED if create_mode else HTTPStatus.OK) except KeyError: self._json( {"error": "not_found", "message": "未找到需要编辑的模式卡片。"}, diff --git a/tests/e2e/test_seat_web_api.py b/tests/e2e/test_seat_web_api.py index 992205f..24aa64a 100755 --- a/tests/e2e/test_seat_web_api.py +++ b/tests/e2e/test_seat_web_api.py @@ -288,6 +288,35 @@ operations: self.assertTrue( (user_root / "vehicles" / "denza_n7" / "user" / "office.yaml").is_file() ) + + create_request = urllib.request.Request( + f"{self.base_url}/api/mode-profiles", + method="POST", + headers={"Content-Type": "application/json"}, + data=json.dumps( + { + "name": "通勤模式", + "instruction": instruction, + "source_request_id": request_id, + "vehicle_id": "denza_n7", + }, + ensure_ascii=False, + ).encode("utf-8"), + ) + with self.opener.open(create_request, timeout=3) as response: + created = json.loads(response.read().decode("utf-8")) + self.assertEqual(response.status, 201) + self.assertEqual(created["mode_profile"]["origin"], "user") + self.assertEqual(created["mode_profile"]["name"], "通勤模式") + self.assertTrue( + ( + user_root + / "vehicles" + / "denza_n7" + / "user" + / f"{created['mode_profile']['id']}.yaml" + ).is_file() + ) finally: self.service.mode_profiles = original_registry self.service.artifact_root = original_artifact_root @@ -588,18 +617,18 @@ operations: def test_static_console_and_runtime_endpoint(self) -> None: with self.opener.open(f"{self.base_url}/", timeout=3) as response: html = response.read().decode("utf-8") - with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-109", timeout=3) as response: + with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-114", timeout=3) as response: app_script = response.read().decode("utf-8") with self.opener.open( f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3 ) as response: gesture_script = response.read().decode("utf-8") with self.opener.open( - f"{self.base_url}/console-v2.js?v=delivery-state-50", timeout=3 + f"{self.base_url}/console-v2.js?v=delivery-state-54", timeout=3 ) as response: console_script = response.read().decode("utf-8") with self.opener.open( - f"{self.base_url}/console-v2.css?v=delivery-state-43", timeout=3 + f"{self.base_url}/console-v2.css?v=delivery-state-50", timeout=3 ) as response: console_styles = response.read().decode("utf-8") with self.opener.open( @@ -607,7 +636,7 @@ operations: ) as response: speech_script = response.read().decode("utf-8") with self.opener.open( - f"{self.base_url}/speech-input.css?v=sensevoice-4", timeout=3 + f"{self.base_url}/speech-input.css?v=sensevoice-5", timeout=3 ) as response: speech_styles = response.read().decode("utf-8") with self.opener.open( @@ -615,11 +644,11 @@ operations: ) as response: speech_worklet = response.read().decode("utf-8") with self.opener.open( - f"{self.base_url}/self-extension.js?v=gscu-tcp-40", timeout=3 + f"{self.base_url}/self-extension.js?v=gscu-tcp-41", timeout=3 ) as response: extension_script = response.read().decode("utf-8") with self.opener.open( - f"{self.base_url}/self-extension.css?v=gscu-tcp-39", timeout=3 + f"{self.base_url}/self-extension.css?v=gscu-tcp-42", timeout=3 ) as response: extension_styles = response.read().decode("utf-8").replace("\r\n", "\n") with self.opener.open( @@ -652,21 +681,21 @@ operations: self.assertIn("CMD 0x431 · FB 0x41A · CH1", html) self.assertIn("CMD 0x431 · FB 0x388 · CH1", html) self.assertNotIn("透明车体 · 实体设备", html) - self.assertIn('src="/app.js?v=dual-seat-109"', html) - self.assertIn('href="/styles.css?v=dual-seat-16"', html) + self.assertIn('src="/app.js?v=dual-seat-114"', html) + self.assertIn('href="/styles.css?v=dual-seat-21"', html) self.assertIn('src="/gesture-control.js?v=android-pad-9"', html) self.assertNotIn('class="vehicle-system-hud"', html) - self.assertIn('src="/console-v2.js?v=delivery-state-50"', html) - self.assertIn('href="/console-v2.css?v=delivery-state-43"', html) + self.assertIn('src="/console-v2.js?v=delivery-state-54"', html) + self.assertIn('href="/console-v2.css?v=delivery-state-50"', html) self.assertIn('src="/speech-input.js?v=sensevoice-6"', html) - self.assertIn('href="/speech-input.css?v=sensevoice-4"', html) + self.assertIn('href="/speech-input.css?v=sensevoice-5"', html) self.assertIn('id="speechDictationButton"', html) self.assertIn('class="speech-waveform"', html) self.assertIn('', html) self.assertIn('id="v2BootScreen"', html) self.assertIn('正在加载车机控制与实时状态', html) - self.assertIn('src="/self-extension.js?v=gscu-tcp-40"', html) - self.assertIn('href="/self-extension.css?v=gscu-tcp-39"', html) + self.assertIn('src="/self-extension.js?v=gscu-tcp-41"', html) + self.assertIn('href="/self-extension.css?v=gscu-tcp-42"', html) self.assertNotIn("self-calibration", html) self.assertNotIn("self-repair", html) self.assertNotIn("self-evolution", html) @@ -680,7 +709,7 @@ operations: 'panelHeading.classList.add("v2-operation-heading", "v2-module-heading")', console_script, ) - self.assertIn('kicker.textContent = "REMOTE ORCHESTRATION · LIVE SYNC"', console_script) + self.assertIn('if (title) title.textContent = "一句话远程编排"', console_script) self.assertIn('fetch("/api/vehicle-display-sync"', console_script) self.assertIn('document.body.classList.add("v2-passive-display", "v2-interactive-sync")', console_script) self.assertIn('input.readOnly = false', console_script) @@ -713,7 +742,7 @@ operations: self.assertNotIn('id="v2VehicleState"', console_script) self.assertIn('actionRail.append(cameraToggle)', console_script) self.assertNotIn("function enhanceConfiguredDropdowns()", console_script) - self.assertIn("function updateModeEdgeHints()", console_script) + self.assertNotIn('modeChips?.addEventListener("wheel"', console_script) self.assertIn('classList.toggle("v2-twin-active", definition.index === 0)', console_script) self.assertIn('label: "执行结果与硬件回读"', console_script) self.assertIn("function keepRunButtonAvailable()", console_script) @@ -793,15 +822,33 @@ operations: ) self.assertIn('data-control-transport="wired_can"', html) self.assertIn('data-control-transport="wireless_mqtt"', html) + self.assertIn('
', html) + self.assertIn('id="modeRailToggle"', html) + self.assertIn('id="modeRailContent"', html) + self.assertIn('role="region"', html) + self.assertIn('aria-label="功能模式列表"', html) + self.assertIn('自定义', html) + self.assertIn('模拟下发', html) + self.assertIn('真实下发', html) + self.assertNotIn('id="simulationMode"', html) self.assertIn('control_transport: state.controlTransport', app_script) + self.assertIn('button.dataset.connectionState = connectionState', app_script) + self.assertIn('信号链路${connectionState === "online" ? "正常" : "断开"}', app_script) self.assertIn('const transportField = $("#vehicleTransportField", originalForm)', console_script) self.assertNotIn('runtimeControls.append(transportField)', console_script) self.assertIn('insertVisibleControl(transportField, "v2-sync-transport-field")', console_script) + self.assertNotIn('class="v2-sync-link-stack"', console_script) + self.assertNotIn('id="v2HeadUnitState"', console_script) self.assertIn('.v2-sync-execution-field .execution-mode-options', console_styles) - self.assertIn('grid-template-columns: repeat(3, minmax(0, 1fr))', console_styles) + self.assertIn( + '.v2-sync-execution-field .execution-mode-options {\n grid-template-columns: repeat(2, minmax(0, 1fr))', + console_styles, + ) self.assertNotIn('class="v2-sync-protocol"', console_script) self.assertIn('.console-v2 .v2-transport-field .vehicle-transport-options', console_styles) self.assertIn('.v2-sync-transport-field .mode-option', console_styles) + self.assertIn('.v2-mode-rail > .mode-rail-content[hidden]', console_styles) + self.assertIn('font-size: var(--text-control) !important', console_styles) self.assertIn('overflow-wrap: anywhere', console_styles) self.assertIn('button.disabled = false', app_script) self.assertIn('button.setAttribute("aria-pressed", String(selected))', app_script) @@ -846,7 +893,7 @@ operations: self.assertIn('外设玩具能力学习', extension_script) self.assertIn('外设玩具与车端联动', extension_script) self.assertGreaterEqual(extension_script.count('class="v2-module-heading"'), 2) - self.assertGreaterEqual(extension_script.count('SELF-EXTENSION'), 2) + self.assertNotIn('class="section-kicker"', extension_script) self.assertIn('id="extensionExecuteButton"', extension_script) self.assertIn('data-lucide="play"', extension_script) self.assertNotIn('id="extensionControlHealth"', extension_script) @@ -912,10 +959,10 @@ operations: self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script) self.assertIn('canvas.addEventListener("webglcontextlost"', app_script) self.assertIn('"cognitive-agent:module-change"', app_script) - self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v221"', service_worker) - self.assertIn('"/app.js?v=dual-seat-109"', service_worker) + self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v227"', service_worker) + self.assertIn('"/app.js?v=dual-seat-114"', service_worker) self.assertIn('"/speech-input.js?v=sensevoice-6"', service_worker) - self.assertIn('"/speech-input.css?v=sensevoice-4"', service_worker) + self.assertIn('"/speech-input.css?v=sensevoice-5"', service_worker) self.assertIn('"/speech-audio-worklet.js?v=sensevoice-4"', service_worker) self.assertIn('fetch("/api/speech/transcribe"', speech_script) self.assertIn('"X-Speech-Utterance-Id"', speech_script) @@ -956,7 +1003,7 @@ operations: self.assertNotIn("gestureDebug", gesture_script) self.assertIn("HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000", app_script) self.assertIn( - "requestTimeoutForExecutionMode(state.executionMode)", app_script + "requestTimeoutForExecutionMode(executionMode)", app_script ) self.assertIn('error.code === "hardware_busy"', app_script) self.assertIn("BENCH BUSY", app_script) @@ -977,12 +1024,15 @@ operations: self.assertIn('id="saveModeToggle"', html) self.assertIn("根据指令创建", html) self.assertIn('aria-label="快捷功能模式"', html) - self.assertIn('id="modeRailPrev"', html) - self.assertIn('id="modeRailNext"', html) + self.assertIn('id="modeRailContent"', html) + self.assertIn('aria-label="功能模式列表"', html) + self.assertNotIn('id="modeRailPrev"', html) + self.assertNotIn('id="modeRailNext"', html) + self.assertLess(html.index('id="saveModeToggle"'), html.index('id="modeRailContent"')) self.assertIn('id="modeGalleryToggle"', html) self.assertIn('id="modeGallery"', html) self.assertIn('id="modeGalleryGroups"', html) - self.assertIn("全部功能模式", html) + self.assertIn("自定义模式", html) self.assertIn('id="modeEditorBackdrop"', html) self.assertIn('id="modeEditorName"', html) self.assertIn('id="modeEditorInstruction"', html) @@ -990,7 +1040,7 @@ operations: self.assertIn('id="modeEditorComplete"', html) self.assertIn('id="modeEditorFlowPreview"', html) self.assertIn('class="mode-editor-flow-preview empty"', html) - self.assertIn('id="saveModeSource"', html) + self.assertNotIn('id="saveModeSource"', html) self.assertNotIn("主驾 24℃", html) populate_modes = app_script[ app_script.index("function populateModeChips()") @@ -998,13 +1048,18 @@ operations: ] self.assertNotIn("ch1_body_can_driver_only", populate_modes) self.assertIn("function populateModeGallery", app_script) - self.assertIn("function updateModeRailNavigation", app_script) - self.assertIn("function scrollModeRail", app_script) + self.assertNotIn("function updateModeRailNavigation", app_script) + self.assertNotIn("function scrollModeRail", app_script) self.assertIn('className = "mode-gallery-card"', app_script) + self.assertIn("function createModeGalleryAddCard", app_script) + self.assertIn('button.dataset.modeCreate = "true"', app_script) + self.assertIn("function openModeEditorForCreate", app_script) + self.assertIn('apiRequest(endpoint', app_script) + self.assertIn('? "/api/mode-profiles"', app_script) self.assertIn("#modeGallery button[data-mode-profile-id]", app_script) self.assertIn("function setModeExecutionState", app_script) - self.assertIn("function requestCommandAndWait", app_script) - self.assertIn("已创建并添加到功能模式栏", app_script) + self.assertNotIn("function requestCommandAndWait", app_script) + self.assertIn("已永久保存到模式卡片", app_script) self.assertLess( app_script.index('id: "personal"'), app_script.index('id: "frequent"'), @@ -1039,7 +1094,7 @@ operations: self.assertIn('id="cabinCarouselNext"', app_script) self.assertIn('aria-label="左右浏览设备卡片"', app_script) self.assertNotIn('class="cabin-category-tabs"', app_script) - self.assertIn('LIVE VEHICLE DIGITAL TWIN', html) + self.assertNotIn('class="section-kicker"', html) self.assertIn('.joint-twin-shell > .twin-band {', console_styles) self.assertIn('grid-template-rows: 62px minmax(0, 1fr)', console_styles) self.assertIn('grid-template-columns: repeat(2, minmax(0, 1fr))', console_styles) @@ -1327,7 +1382,7 @@ operations: ) as response: extension_script = response.read().decode("utf-8") with self.opener.open( - f"{self.base_url}/app.js?v=dual-seat-109", timeout=3 + f"{self.base_url}/app.js?v=dual-seat-114", timeout=3 ) as response: app_script = response.read().decode("utf-8") diff --git a/tests/integration/test_multidomain_service.py b/tests/integration/test_multidomain_service.py index 9b3d014..e24c565 100755 --- a/tests/integration/test_multidomain_service.py +++ b/tests/integration/test_multidomain_service.py @@ -621,6 +621,25 @@ operations: if item["id"] == "office" ) self.assertEqual(persisted["name"], "专注办公模式") + created = self.service.create_mode_profile_card( + "自然舒享模式", + instruction, + generated["request_id"], + "denza_n7", + ) + self.assertEqual(created["status"], "succeeded") + self.assertEqual(created["mode_profile"]["origin"], "user") + self.assertEqual(created["mode_profile"]["instruction"], instruction) + self.assertNotEqual(created["mode_profile"]["id"], "office") + self.assertTrue( + ( + user_root + / "vehicles" + / "denza_n7" + / "user" + / f"{created['mode_profile']['id']}.yaml" + ).is_file() + ) with self.assertRaisesRegex(ValueError, "生成指令"): self.service.save_mode_profile_card( "office", diff --git a/tests/unit/test_frontend_theme_contract.py b/tests/unit/test_frontend_theme_contract.py new file mode 100644 index 0000000..e54f89b --- /dev/null +++ b/tests/unit/test_frontend_theme_contract.py @@ -0,0 +1,221 @@ +from pathlib import Path +import re + + +ROOT = Path(__file__).resolve().parents[2] +WEB = ROOT / "apps" / "web-console" + + +def source(name: str) -> str: + return (WEB / name).read_text(encoding="utf-8") + + +def test_shadcn_semantic_theme_tokens_drive_the_console() -> None: + styles = source("styles.css") + console = source("console-v2.css") + speech = source("speech-input.css") + + expected_tokens = { + "--background: oklch(1 0 0)", + "--foreground: oklch(0.145 0.008 326)", + "--primary: oklch(0.488 0.243 264.376)", + "--primary-foreground: oklch(0.97 0.014 254.604)", + "--destructive: oklch(0.577 0.245 27.325)", + "--sidebar-primary: oklch(0.546 0.245 262.881)", + "--background: oklch(0.145 0.008 326)", + "--foreground: oklch(0.985 0 0)", + "--primary: oklch(0.424 0.199 265.638)", + "--sidebar-primary: oklch(0.623 0.214 259.815)", + } + token_lines = {line.strip() for line in styles.replace(";", "").splitlines()} + assert expected_tokens <= token_lines + assert ".dark {" in styles + assert "--v2-accent: var(--primary)" in console + assert "--v2-sidebar: var(--sidebar)" in console + assert "color: var(--destructive)" in speech + + +def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None: + html = source("index.html") + service_worker = source("service-worker.js") + + for asset in ( + "/styles.css?v=dual-seat-21", + "/console-v2.css?v=delivery-state-50", + "/self-extension.css?v=gscu-tcp-42", + "/speech-input.css?v=sensevoice-5", + ): + assert asset in html + assert f'"{asset}"' in service_worker + assert 'const CACHE_VERSION = "cognitive-agent-pad-v227"' in service_worker + + +def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None: + styles = source("styles.css") + console = source("console-v2.css") + extension = source("self-extension.css") + + expected_console_rules = { + ".console-v2 .v2-twin-hotspot-label", + ".console-v2 .v2-twin-device-popover", + ".console-v2 .v2-reasoning-field", + ".console-v2 .device-power-drawer", + ".console-v2 .v2-sync-log li[data-log-status=\"error\"] > span", + "background: var(--primary)", + "background: var(--popover)", + } + expected_overlay_rules = { + ".mode-gallery-backdrop", + ".mode-editor-backdrop", + ".gesture-drawer", + "background: var(--card)", + "border-color: var(--border)", + } + expected_extension_rules = { + ".v2-extension-dialog::backdrop", + ".v2-gscu-tcp-dialog", + ".v2-nfc-dialog", + ".v2-extension-toast.error", + ".console-v2 #v2ExtensionPanel .v2-extension-gateway > button", + "background: var(--destructive)", + } + + assert all(rule in console for rule in expected_console_rules) + assert all(rule in styles for rule in expected_overlay_rules) + assert all(rule in extension for rule in expected_extension_rules) + + +def test_operator_controls_share_status_and_typography_tokens() -> None: + html = source("index.html") + styles = source("styles.css") + console = source("console-v2.css") + script = source("console-v2.js") + + for token in ( + "--status-online:", + "--status-offline:", + "--text-caption:", + "--text-control:", + "--text-input:", + "--text-label:", + ): + assert token in styles + assert '
' in html + assert 'id="modeRailToggle"' in html + assert 'aria-controls="modeRailContent"' in html + assert 'id="modeRailContent"' in html + assert 'role="region"' in html + assert 'aria-label="功能模式列表"' in html + assert '自定义' in html + assert "模拟下发" in html + assert "真实下发" in html + assert 'id="simulationMode"' not in html + assert 'class="v2-sync-link-stack"' not in script + assert '.mode-option[data-connection-state="online"]::after' in styles + assert '.mode-option[data-connection-state="offline"]::after' in styles + assert "font-size: var(--text-control) !important" in console + + +def test_wired_can_link_requires_real_device_connection_evidence() -> None: + app = source("app.js") + + assert 'runtime?.state_sync?.state === "online"' in app + assert '["online", "ready"].includes(runtime?.state_sync?.state)' not in app + assert 'wiredCanProbe: { passenger: false, vehicle: false }' in app + assert 'updateWiredCanProbe("passenger", true, requestedTransport)' in app + assert 'updateWiredCanProbe("vehicle", true, requestedTransport)' in app + assert 'updateWiredCanProbe("passenger", false, requestedTransport)' in app + assert 'updateWiredCanProbe("vehicle", false, requestedTransport)' in app + assert "resetTransportConnectionEvidence();" in app + assert 'label.textContent = `${transportLabel} · 信号链路断开`' in app + + +def test_command_fields_use_distinct_semantic_colors() -> None: + styles = source("styles.css") + console = source("console-v2.css") + + for token in ( + "--command-channel:", + "--command-source:", + "--command-delivered:", + "--command-pending:", + "--command-category:", + "--command-control:", + "--command-frame:", + ): + assert token in styles + for mapping in ( + "color: var(--command-channel)", + "color: var(--command-source)", + "color: var(--command-pending)", + "color: var(--command-delivered)", + "color: var(--command-category)", + "color: var(--command-control)", + "color: var(--command-frame)", + ): + assert mapping in console + + +def test_function_modes_use_an_inline_accessible_collapsible() -> None: + html = source("index.html") + app = source("app.js") + console = source("console-v2.css") + + assert '
' in html + assert 'id="modeRailSelection">常用模式' in html + assert 'id="modeGalleryToggle"' in html + assert '自定义' in html + assert '
.mode-rail-content[hidden]' in console + assert ".mode-rail-expanded .main-panel" not in console + assert "position: absolute" in console + assert "top: calc(100% + 8px)" in console + assert "overflow-y: auto" in console + assert "scrollbar-gutter: stable" in console + + +def test_vehicle_device_cards_hide_stale_values_without_signal() -> None: + console = source("console-v2.js") + + assert "function normalizeDeviceReadout(value)" in console + assert "function deviceFeedbackAvailable(deviceId)" in console + assert 'return rows.map(([label]) => [label, "-"])' in console + assert 'const waiting = rows.some(([, value]) => value === "-")' in console + assert '"cognitive-agent:vehicle-feedback-state"' in console + + +def test_final_theme_layers_do_not_reintroduce_raw_ui_colors() -> None: + sections = ( + source("styles.css").split("/* Final semantic theme layer", 1)[1], + source("console-v2.css").split("/* Final semantic theme cascade", 1)[1], + source("self-extension.css").split( + "/* Final semantic theme layer for extension surfaces", 1 + )[1], + ) + raw_color = re.compile(r"#[0-9a-fA-F]{3,8}\b|rgba?\(") + + for section in sections: + assert not raw_color.search(section) + + +def test_all_five_output_modules_are_covered_by_the_final_theme_layer() -> None: + console = source("console-v2.css") + final_layer = console.split("/* Final semantic theme cascade", 1)[1] + + for selector in ( + ".joint-twin-shell.v2-output-page", + ".decision-list li", + ".result-workspace #panel-config.config-panel", + ".plan-panel .frame-summary", + ".execution-summary", + ): + assert selector in final_layer + + assert "--config-accent: var(--primary)" in final_layer + assert "background: var(--card)" in final_layer + assert "border-color: var(--border)" in final_layer diff --git a/tests/unit/test_nfc_frontend_contract.py b/tests/unit/test_nfc_frontend_contract.py index 217c0e5..8ddd18b 100644 --- a/tests/unit/test_nfc_frontend_contract.py +++ b/tests/unit/test_nfc_frontend_contract.py @@ -17,7 +17,7 @@ def test_unified_console_exposes_device_id_based_nfc_authorization() -> None: assert 'data-open-nfc-binding' in script assert '靠近读卡器后' in script - assert 'NFC · DEVICE AUTHORIZATION' in script + assert '

NFC 设备授权

' in script assert 'NFC 设备授权' in script assert 'api("nfc/status")' in script assert 'function bindCardToDevice(device, cardUid)' in script @@ -60,8 +60,8 @@ def test_unified_console_exposes_device_id_based_nfc_authorization() -> None: assert '.v2-nfc-command-list' in styles assert '.v2-nfc-command-option' in styles assert '.v2-nfc-select-all' in styles - assert 'self-extension.js?v=gscu-tcp-40' in html - assert 'self-extension.css?v=gscu-tcp-39' in html + assert 'self-extension.js?v=gscu-tcp-41' in html + assert 'self-extension.css?v=gscu-tcp-42' in html def test_unified_console_exposes_gscu_tcp_configuration_and_actions() -> None: @@ -139,4 +139,4 @@ def test_main_parsed_control_panel_renders_validated_infrared_operations() -> No assert 'frameLabel: "GSCU1B TCP"' in console assert "已解析控制指令" in console assert 'li[data-can-channel="IR"]' in styles - assert "console-v2.js?v=delivery-state-50" in html + assert "console-v2.js?v=delivery-state-54" in html