Remove the API relay link and the ESP32 device feature

Two pieces of upstream promotion came out of this deployment.

The sidebar "apiRelay" tab only opened a registration page on a third-party
site, so it goes with no replacement.

The circuit badge next to Settings was not what it looked like. It opened an
ESP32 device manager: add a device, show LAN status, remote connect/disconnect,
rename, delete, backed by six REST routes and an mcu_devices table. The purchase
button inside that dialog was the promotional part; the rest was real device
management. Since the hardware is out of scope here, the entire feature is
removed rather than just the link that pointed upstream - UI entry, client API,
routes, controller, store, schema, locale strings, and the one GlobalAgentServer
method that only existed to answer its status column.

Table creation is dropped but no DROP TABLE was added, so an already-provisioned
database keeps its rows as harmless orphans instead of losing data on upgrade.

DevicesView and the hermes.devices route are untouched: that is LAN device
discovery, a separate feature.

AGENTS.md now records the removal so the badge does not come back.
This commit is contained in:
2026-10-19 01:32:51 +08:00
parent 3c963bb1d9
commit 77b1858163
22 changed files with 49 additions and 1405 deletions
@@ -19,7 +19,6 @@ import OutlinePanel from "./OutlinePanel.vue";
import FilesPanel from "./FilesPanel.vue";
import TerminalPanel from "./TerminalPanel.vue";
import PageSidebarNav from "@/components/layout/PageSidebarNav.vue";
import SettingsCircuitBadge from "@/components/layout/SettingsCircuitBadge.vue";
import { isStoredSuperAdmin } from "@/api/client";
import { useDefaultWorkspace } from "@/composables/useDefaultWorkspace";
@@ -1289,7 +1288,6 @@ async function handleSessionModelCustomSubmit() {
</svg>
<span>{{ t("sidebar.settings") }}</span>
</button>
<SettingsCircuitBadge />
</div>
</aside>
@@ -11,7 +11,6 @@ import GroupChatInput from './GroupChatInput.vue'
import FolderPicker from '@/components/hermes/chat/FolderPicker.vue'
import ProfileAvatar from '@/components/hermes/profiles/ProfileAvatar.vue'
import PageSidebarNav from '@/components/layout/PageSidebarNav.vue'
import SettingsCircuitBadge from '@/components/layout/SettingsCircuitBadge.vue'
import { copyToClipboard } from '@/utils/clipboard'
import type { Attachment } from '@/stores/hermes/chat'
import type { RoomAgent, RoomInfo } from '@/api/hermes/group-chat'
@@ -512,7 +511,6 @@ async function handleApproval(choice: 'once' | 'session' | 'always' | 'deny') {
</svg>
<span>{{ t('sidebar.settings') }}</span>
</button>
<SettingsCircuitBadge />
</div>
</div>
@@ -1,7 +1,6 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import SettingsCircuitBadge from '@/components/layout/SettingsCircuitBadge.vue'
const router = useRouter()
const { t } = useI18n()
@@ -20,7 +19,6 @@ function openSettingsPage() {
</svg>
<span>{{ t('sidebar.settings') }}</span>
</button>
<SettingsCircuitBadge />
</div>
</template>
@@ -48,11 +48,6 @@ function openWorkflow() {
if (props.active === 'workflow') return
void router.push({ name: 'hermes.workflow' })
}
function openApiRelay() {
if (typeof window === 'undefined') return
window.open('https://apikey.fun/register?aff=LIBAPI', '_blank', 'noopener,noreferrer')
}
</script>
<template>
@@ -124,21 +119,6 @@ function openApiRelay() {
</svg>
<span>{{ historyButtonLabel }}</span>
</button>
<button class="page-sidebar-tab" type="button" @click="openApiRelay">
<svg
width="15"
height="15"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
>
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" />
<polyline points="15 3 21 3 21 9" />
<line x1="10" y1="14" x2="21" y2="3" />
</svg>
<span>{{ t('sidebar.apiRelay') }}</span>
</button>
</div>
<div v-if="showModeSwitch" class="conversation-switch conversation-switch--three" role="tablist" aria-label="Conversation type">
<a-tooltip trigger="hover" position="top" :content="t('sidebar.singleChat')">
@@ -1,696 +0,0 @@
<script setup lang="ts">
import { computed, h, ref, resolveComponent } from 'vue'
import { useDialog } from '@/composables/useDialog'
import { useMessage } from '@/composables/useMessage'
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
import { useI18n } from 'vue-i18n'
import { connectMcuDeviceRemote, createMcuDevice, deleteMcuDevice, disconnectMcuDeviceRemote, fetchMcuDevices, updateMcuDeviceName, type McuDevice } from '@/api/hermes/mcu-devices'
// a-table 的 column.render 里不能用 kebab-case 标签(h('a-tag') 只会生成
// 同名原生元素,不会解析全局注册的组件),必须走 resolveComponent。
const Tag = resolveComponent('a-tag')
const Button = resolveComponent('a-button')
const Space = resolveComponent('a-space')
const { t } = useI18n()
const message = useMessage()
const dialog = useDialog()
const purchaseUrl = 'https://hermes-studio.ai/docs/hermes-esp32-intro/index.html'
const showModal = ref(false)
const showAddModal = ref(false)
const loading = ref(false)
const saving = ref(false)
const connectingDeviceId = ref(0)
const editing = ref(false)
const devices = ref<McuDevice[]>([])
const form = ref({
name: '',
device_code: '',
})
const editForm = ref({
id: 0,
name: '',
})
function renderConnectionStatus(connected: boolean | undefined) {
return h(Tag, {
size: 'small',
color: connected ? 'green' : 'gray',
bordered: false,
}, { default: () => connected ? t('mcuDevices.connected') : t('mcuDevices.disconnected') })
}
const columns = computed<TableColumnData[]>(() => [
{
title: t('mcuDevices.name'),
dataIndex: 'name',
ellipsis: true,
tooltip: true,
},
{
title: t('mcuDevices.deviceCode'),
dataIndex: 'device_code',
ellipsis: true,
tooltip: true,
},
{
title: t('mcuDevices.channel'),
dataIndex: 'is_official',
width: 96,
render({ record }) {
const row = record as McuDevice
return h(Tag, {
size: 'small',
color: row.is_official ? 'green' : 'orange',
bordered: false,
}, { default: () => row.is_official ? t('mcuDevices.official') : t('mcuDevices.unofficial') })
},
},
{
title: t('mcuDevices.lanStatus'),
dataIndex: 'lan_connected',
width: 96,
render({ record }) {
return renderConnectionStatus((record as McuDevice).lan_connected)
},
},
{
title: t('mcuDevices.remoteStatus'),
dataIndex: 'remote_connected',
width: 96,
render({ record }) {
return renderConnectionStatus((record as McuDevice).remote_connected)
},
},
{
title: t('mcuDevices.actions'),
dataIndex: 'actions',
width: 220,
render({ record }) {
const row = record as McuDevice
return h(Space, { size: 4 }, {
default: () => [
h(Button, {
size: 'mini',
type: 'text',
onClick: () => openEditModal(row),
}, { default: () => t('mcuDevices.edit') }),
h(Button, {
size: 'mini',
type: 'text',
status: row.remote_connected ? 'warning' : 'normal',
loading: connectingDeviceId.value === row.id,
disabled: connectingDeviceId.value !== 0 || (!row.is_official && !row.remote_connected),
onClick: () => toggleRemoteDevice(row),
}, { default: () => row.remote_connected ? t('mcuDevices.remoteDisconnect') : t('mcuDevices.remoteConnect') }),
h(Button, {
size: 'mini',
type: 'text',
status: 'danger',
onClick: () => confirmDeleteDevice(row),
}, { default: () => t('mcuDevices.delete') }),
],
})
},
},
])
async function loadDevices() {
loading.value = true
try {
const response = await fetchMcuDevices()
devices.value = response.devices
} catch (error: any) {
message.error(error?.message || t('mcuDevices.loadFailed'))
} finally {
loading.value = false
}
}
function openModal() {
showModal.value = true
void loadDevices()
}
function openAddModal() {
form.value = { name: '', device_code: '' }
showAddModal.value = true
}
function openPurchasePage() {
window.open(purchaseUrl, '_blank', 'noopener,noreferrer')
}
async function submitDevice(): Promise<boolean | void> {
const deviceCode = form.value.device_code.trim()
if (!deviceCode) {
message.warning(t('mcuDevices.deviceCodeRequired'))
return false
}
saving.value = true
try {
const response = await createMcuDevice({
name: form.value.name.trim(),
device_code: deviceCode,
})
devices.value = response.devices
form.value = { name: '', device_code: '' }
showAddModal.value = false
message.success(t('mcuDevices.added'))
} catch (error: any) {
message.error(error?.message || t('mcuDevices.addFailed'))
return false
} finally {
saving.value = false
}
}
function openEditModal(device: McuDevice) {
editForm.value = {
id: device.id,
name: device.name,
}
editing.value = true
}
async function submitEdit() {
if (!editForm.value.id) return
saving.value = true
try {
const response = await updateMcuDeviceName(editForm.value.id, editForm.value.name)
devices.value = response.devices
editing.value = false
message.success(t('mcuDevices.nameUpdated'))
} catch (error: any) {
message.error(error?.message || t('mcuDevices.nameUpdateFailed'))
} finally {
saving.value = false
}
}
async function toggleRemoteDevice(device: McuDevice) {
connectingDeviceId.value = device.id
try {
const response = device.remote_connected
? await disconnectMcuDeviceRemote(device.id)
: await connectMcuDeviceRemote(device.id)
devices.value = response.devices
message.success(t(device.remote_connected ? 'mcuDevices.remoteDisconnected' : 'mcuDevices.remoteConnected'))
} catch (error: any) {
message.error(error?.message || t(device.remote_connected ? 'mcuDevices.remoteDisconnectFailed' : 'mcuDevices.remoteConnectFailed'))
} finally {
connectingDeviceId.value = 0
}
}
function confirmDeleteDevice(device: McuDevice) {
dialog.warning({
title: t('mcuDevices.deleteTitle'),
content: t('mcuDevices.deleteConfirm', { name: device.name || device.device_code }),
positiveText: t('mcuDevices.delete'),
negativeText: t('common.cancel'),
onPositiveClick: async () => {
saving.value = true
try {
const response = await deleteMcuDevice(device.id)
devices.value = response.devices
message.success(t('mcuDevices.deleted'))
} catch (error: any) {
message.error(error?.message || t('mcuDevices.deleteFailed'))
return false
} finally {
saving.value = false
}
},
})
}
</script>
<template>
<!-- Style 1: chip badge
<svg class="settings-circuit-badge settings-circuit-badge--chip" viewBox="0 0 34 22" fill="none" aria-hidden="true">
<rect class="settings-circuit-badge-shell" x="4.5" y="3.5" width="25" height="15" rx="4" />
<path class="settings-circuit-pin" d="M2.5 7h3M2.5 11h3M2.5 15h3M28.5 7h3M28.5 11h3M28.5 15h3" />
<path class="settings-circuit-pin" d="M11 1.8v2M17 1.8v2M23 1.8v2M11 18.2v2M17 18.2v2M23 18.2v2" />
<rect class="settings-circuit-chip" x="12" y="6" width="10" height="10" rx="2" />
<path class="settings-circuit-track" d="M17 11H7" />
<path class="settings-circuit-track" d="M17 11h10" />
<path class="settings-circuit-track" d="M17 11V5" />
<path class="settings-circuit-track" d="M17 11v6" />
<path class="settings-circuit-track" d="M14 8h-4V6H7" />
<path class="settings-circuit-track" d="M20 8h4V6h3" />
<path class="settings-circuit-track" d="M14 14h-4v2H7" />
<path class="settings-circuit-track" d="M20 14h4v2h3" />
<path class="settings-circuit-pulse settings-circuit-pulse-main" d="M17 11H7" pathLength="1" />
<path class="settings-circuit-pulse settings-circuit-pulse-main settings-circuit-pulse-late" d="M17 11h10" pathLength="1" />
<path class="settings-circuit-pulse settings-circuit-pulse-branch" d="M17 11V5" pathLength="1" />
<path class="settings-circuit-pulse settings-circuit-pulse-branch settings-circuit-pulse-late" d="M17 11v6" pathLength="1" />
<path class="settings-circuit-pulse settings-circuit-pulse-branch" d="M14 8h-4V6H7" pathLength="1" />
<path class="settings-circuit-pulse settings-circuit-pulse-branch settings-circuit-pulse-late" d="M20 14h4v2h3" pathLength="1" />
<circle class="settings-circuit-core" cx="17" cy="11" r="1.35" />
<circle class="settings-circuit-node" cx="7" cy="6" r="0.9" />
<circle class="settings-circuit-node" cx="27" cy="6" r="0.9" />
<circle class="settings-circuit-node" cx="7" cy="16" r="0.9" />
<circle class="settings-circuit-node" cx="27" cy="16" r="0.9" />
</svg>
-->
<a-tooltip trigger="hover" position="top" :content="t('mcuDevices.title')">
<button class="settings-circuit-link" type="button" :aria-label="t('mcuDevices.title')" @click.stop="openModal">
<svg class="settings-circuit-badge settings-circuit-badge--pcb" viewBox="0 0 36 22" fill="none" aria-hidden="true">
<rect class="settings-pcb-board" x="3" y="3" width="30" height="16" rx="3" />
<path class="settings-pcb-copper" d="M7 7h6v4h7v-3h9" />
<path class="settings-pcb-copper" d="M7 15h5v-3h5v4h12" />
<path class="settings-pcb-copper" d="M12 7v-2h8" />
<path class="settings-pcb-copper" d="M24 15v2h5" />
<path class="settings-pcb-current settings-pcb-current-main" d="M7 7h6v4h7v-3h9" />
<path class="settings-pcb-current settings-pcb-current-main settings-pcb-current-late" d="M7 15h5v-3h5v4h12" />
<path class="settings-pcb-current settings-pcb-current-branch" d="M12 7v-2h8" />
<path class="settings-pcb-current settings-pcb-current-branch settings-pcb-current-late" d="M24 15v2h5" />
<circle class="settings-pcb-pad settings-pcb-pad-live" cx="7" cy="7" r="1.2" />
<circle class="settings-pcb-pad" cx="13" cy="11" r="1" />
<circle class="settings-pcb-pad settings-pcb-pad-live" cx="20" cy="11" r="1.2" />
<circle class="settings-pcb-pad" cx="29" cy="8" r="1" />
<circle class="settings-pcb-pad settings-pcb-pad-live settings-pcb-pad-late" cx="17" cy="12" r="1.2" />
<circle class="settings-pcb-pad" cx="29" cy="15" r="1" />
<circle class="settings-pcb-via" cx="20" cy="5" r="0.8" />
<circle class="settings-pcb-via" cx="29" cy="17" r="0.8" />
</svg>
</button>
</a-tooltip>
<a-modal v-model:visible="showModal" :footer="false" :closable="false">
<div class="mcu-device-dialog">
<div class="mcu-device-header">
<div>
<div class="mcu-device-title">{{ t('mcuDevices.title') }}</div>
<div class="mcu-device-subtitle">{{ t('mcuDevices.subtitle') }}</div>
</div>
<button class="mcu-device-close" type="button" :aria-label="t('mcuDevices.close')" @click="showModal = false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<div class="mcu-device-table">
<a-table
style="height: 100%;"
size="small"
:columns="columns"
:data="devices"
:loading="loading"
:bordered="false"
:pagination="false"
row-key="id"
:scroll="{ y: '100%' }"
/>
</div>
<div class="mcu-device-actions">
<a-button type="primary" @click="openAddModal">{{ t('mcuDevices.add') }}</a-button>
<a-button type="secondary" @click="openPurchasePage">{{ t('mcuDevices.purchase') }}</a-button>
</div>
</div>
</a-modal>
<a-modal
v-model:visible="showAddModal"
:title="t('mcuDevices.addTitle')"
:ok-text="t('mcuDevices.add')"
:cancel-text="t('common.cancel')"
:ok-loading="saving"
@before-ok="submitDevice"
>
<a-form :model="form" layout="vertical">
<a-form-item :label="t('mcuDevices.name')">
<a-input v-model="form.name" :placeholder="t('mcuDevices.nameOptional')" :disabled="saving" />
</a-form-item>
<a-form-item :label="t('mcuDevices.deviceCode')" required>
<a-input
v-model="form.device_code"
:placeholder="t('mcuDevices.deviceCodePlaceholder')"
:disabled="saving"
@keydown.enter.prevent="submitDevice"
/>
</a-form-item>
</a-form>
</a-modal>
<a-modal
v-model:visible="editing"
:title="t('mcuDevices.editNameTitle')"
:ok-text="t('common.save')"
:cancel-text="t('common.cancel')"
:ok-loading="saving"
@before-ok="submitEdit"
>
<a-input
v-model="editForm.name"
:placeholder="t('mcuDevices.nameOptional')"
:disabled="saving"
@keydown.enter.prevent="submitEdit"
/>
</a-modal>
<!-- Style 3: current scan line
<svg class="settings-circuit-badge settings-circuit-badge--scan" viewBox="0 0 36 20" fill="none" aria-hidden="true">
<path class="settings-scan-track" d="M3 10h6l3-5h7l3 10h5l3-5h3" />
<path class="settings-scan-track settings-scan-branch" d="M12 5V3h6" />
<path class="settings-scan-track settings-scan-branch" d="M22 15v2h7" />
<path class="settings-scan-glow" d="M3 10h6l3-5h7l3 10h5l3-5h3" pathLength="1" />
<path class="settings-scan-glow settings-scan-glow-branch" d="M12 5V3h6" pathLength="1" />
<path class="settings-scan-glow settings-scan-glow-branch settings-scan-glow-late" d="M22 15v2h7" pathLength="1" />
<circle class="settings-scan-node settings-scan-node-live" cx="12" cy="5" r="1.2" />
<circle class="settings-scan-node" cx="19" cy="5" r="0.9" />
<circle class="settings-scan-node settings-scan-node-live settings-scan-node-late" cx="22" cy="15" r="1.2" />
<circle class="settings-scan-node" cx="30" cy="10" r="0.9" />
</svg>
-->
</template>
<style scoped lang="scss">
.settings-circuit-badge {
flex: 0 0 auto;
width: 34px;
height: 20px;
color: #d6a019;
overflow: visible;
filter: drop-shadow(0 0 4px rgba(214, 160, 25, 0.45));
}
.settings-circuit-link {
display: inline-grid;
place-items: center;
flex: 0 0 auto;
width: 36px;
height: 28px;
border: 0;
border-radius: 8px;
background: transparent;
align-items: center;
margin-left: auto;
color: inherit;
cursor: pointer;
transition:
background-color 0.16s ease,
color 0.16s ease;
&:hover {
background: rgba(214, 160, 25, 0.08);
}
}
.mcu-device-dialog {
width: min(880px, calc(100vw - 48px));
height: min(680px, calc(100vh - 72px));
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 10px;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.32);
overflow: hidden;
display: flex;
flex-direction: column;
}
.mcu-device-header {
flex: 0 0 auto;
min-height: 68px;
padding: 16px 18px;
display: flex;
align-items: center;
justify-content: space-between;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.mcu-device-title {
font-size: 16px;
font-weight: 650;
line-height: 22px;
color: var(--text-primary);
}
.mcu-device-subtitle {
margin-top: 2px;
font-size: 12px;
line-height: 18px;
color: var(--text-muted);
}
.mcu-device-close {
width: 32px;
height: 32px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text-secondary);
display: inline-grid;
place-items: center;
cursor: pointer;
&:hover {
background: rgba(var(--text-muted-rgb), 0.14);
color: var(--text-primary);
}
}
.mcu-device-actions {
flex: 0 0 auto;
padding: 12px 18px 16px;
display: flex;
justify-content: flex-end;
gap: 10px;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
}
.mcu-device-table {
flex: 1 1 auto;
min-height: 0;
padding: 12px 18px;
overflow: hidden;
:deep(.arco-table-container) {
height: 100%;
}
:deep(.arco-table-body) {
overflow-y: auto;
}
:deep(.arco-table-th) {
background-color: var(--bg-secondary);
color: var(--text-secondary);
border-bottom: 1px solid var(--border-color);
}
:deep(.arco-table-td) {
background-color: var(--bg-card);
color: var(--text-primary);
border-bottom-color: var(--border-color);
}
}
@media (max-width: 640px) {
.mcu-device-dialog {
width: calc(100vw - 24px);
height: calc(100vh - 32px);
}
.mcu-device-actions {
justify-content: stretch;
:deep(.arco-btn) {
flex: 1;
}
}
}
.settings-circuit-badge--scan {
height: 18px;
color: #d6a019;
filter:
drop-shadow(0 0 3px rgba(214, 160, 25, 0.55))
drop-shadow(0 0 8px rgba(214, 160, 25, 0.22));
}
.settings-scan-track,
.settings-scan-glow {
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
}
.settings-scan-track {
stroke-width: 1.25;
opacity: 0.24;
}
.settings-scan-branch {
opacity: 0.18;
}
.settings-scan-glow {
stroke-width: 1.85;
stroke-dasharray: 4 18;
animation: settings-circuit-flow 1.05s linear infinite;
}
.settings-scan-glow-branch {
stroke-width: 1.55;
animation-duration: 1.65s;
}
.settings-scan-glow-late {
animation-delay: -0.55s;
}
.settings-scan-node {
fill: rgba(214, 160, 25, 0.38);
stroke: currentColor;
stroke-width: 0.8;
}
.settings-scan-node-live {
fill: currentColor;
animation: settings-circuit-node 1.05s ease-in-out infinite;
}
.settings-scan-node-late {
animation-delay: -0.55s;
}
.settings-pcb-board {
fill: rgba(214, 160, 25, 0.045);
stroke: rgba(214, 160, 25, 0.46);
stroke-width: 1;
}
.settings-pcb-copper,
.settings-pcb-current {
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
}
.settings-pcb-copper {
stroke-width: 1.15;
opacity: 0.32;
}
.settings-pcb-current {
stroke-width: 1.75;
stroke-dasharray: 4 16;
animation: settings-circuit-flow 1.35s linear infinite;
}
.settings-pcb-current-branch {
stroke-dasharray: 3 13;
animation-duration: 1.8s;
}
.settings-pcb-current-late {
animation-delay: -0.65s;
}
.settings-pcb-pad,
.settings-pcb-via {
fill: rgba(214, 160, 25, 0.34);
stroke: currentColor;
stroke-width: 0.9;
}
.settings-pcb-via {
opacity: 0.55;
}
.settings-pcb-pad-live {
fill: currentColor;
animation: settings-circuit-node 1.35s ease-in-out infinite;
}
.settings-pcb-pad-late {
animation-delay: -0.65s;
}
.settings-circuit-badge-shell {
fill: rgba(214, 160, 25, 0.1);
stroke: rgba(214, 160, 25, 0.55);
stroke-width: 1;
}
.settings-circuit-chip {
fill: rgba(214, 160, 25, 0.18);
stroke: rgba(255, 220, 125, 0.75);
stroke-width: 1;
}
.settings-circuit-pin,
.settings-circuit-track,
.settings-circuit-pulse {
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
}
.settings-circuit-pin {
stroke-width: 1.1;
opacity: 0.48;
}
.settings-circuit-track {
stroke-width: 1;
opacity: 0.32;
}
.settings-circuit-pulse {
stroke-width: 1.6;
stroke-dasharray: 4 16;
animation: settings-circuit-flow 1.2s linear infinite;
}
.settings-circuit-pulse-branch {
stroke-dasharray: 3 13;
animation-duration: 1.9s;
}
.settings-circuit-pulse-late {
animation-delay: -0.65s;
}
.settings-circuit-core,
.settings-circuit-node {
fill: currentColor;
opacity: 0.9;
animation: settings-circuit-node 1.45s ease-in-out infinite;
}
.settings-circuit-core {
filter: drop-shadow(0 0 4px rgba(255, 220, 125, 0.9));
}
@keyframes settings-circuit-flow {
to {
stroke-dashoffset: -20;
}
}
@keyframes settings-circuit-node {
0%,
100% {
opacity: 0.45;
}
50% {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.settings-scan-glow,
.settings-scan-node-live,
.settings-pcb-current,
.settings-pcb-pad-live,
.settings-circuit-pulse,
.settings-circuit-node {
animation: none;
}
}
</style>