mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-07-01 04:31:31 +08:00
feat(hermes): add channel configuration editor
This commit is contained in:
@@ -89,6 +89,7 @@ export default {
|
||||
{ route: '/h/cron', label: t('sidebar.cron'), icon: 'clock' },
|
||||
{ route: '/h/profiles', label: t('engine.hermesProfilesTitle'), icon: 'agents' },
|
||||
{ route: '/h/gateways', label: t('engine.hermesGatewaysTitle'), icon: 'gateway' },
|
||||
{ route: '/h/channels', label: t('engine.hermesChannelsTitle'), icon: 'channels' },
|
||||
{ route: '/h/kanban', label: t('engine.hermesKanbanTitle'), icon: 'inbox' },
|
||||
{ route: '/h/oauth', label: t('engine.hermesOAuthTitle'), icon: 'memory' },
|
||||
{ route: '/h/files', label: t('engine.hermesFilesTitle'), icon: 'folder' },
|
||||
@@ -128,8 +129,6 @@ export default {
|
||||
{ path: '/h/lazy-deps', loader: () => import('./pages/lazy-deps.js') },
|
||||
{ path: '/h/services', loader: () => import('./pages/services.js') },
|
||||
{ path: '/h/config', loader: () => import('./pages/config.js') },
|
||||
// Batch 1 §A: /h/channels 当前是 placeholder(487 字节 stub)— 暂不挂 nav
|
||||
// 完整实现见 Batch 3,待 Hermes 渠道完整支持时启用 sidebar 入口
|
||||
{ path: '/h/channels', loader: () => import('./pages/channels.js') },
|
||||
{ path: '/h/env', loader: () => import('./pages/env-editor.js') },
|
||||
// 共用页面(引擎无关)
|
||||
|
||||
@@ -2,16 +2,339 @@
|
||||
* Hermes Agent 渠道配置
|
||||
*/
|
||||
import { t } from '../../../lib/i18n.js'
|
||||
import { api } from '../../../lib/tauri-api.js'
|
||||
import { toast } from '../../../components/toast.js'
|
||||
import { humanizeErrorText } from '../../../lib/humanize-error.js'
|
||||
import { icon } from '../../../lib/icons.js'
|
||||
|
||||
const CHANNELS = [
|
||||
{
|
||||
id: 'telegram',
|
||||
icon: 'message-circle',
|
||||
titleKey: 'engine.hermesChannelTelegram',
|
||||
descKey: 'engine.hermesChannelTelegramDesc',
|
||||
secretFields: ['botToken'],
|
||||
fields: [
|
||||
{ key: 'botToken', labelKey: 'engine.hermesChannelBotToken', type: 'password', placeholder: '123456:ABC-DEF...' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'discord',
|
||||
icon: 'message-square',
|
||||
titleKey: 'engine.hermesChannelDiscord',
|
||||
descKey: 'engine.hermesChannelDiscordDesc',
|
||||
secretFields: ['token'],
|
||||
fields: [
|
||||
{ key: 'token', labelKey: 'engine.hermesChannelBotToken', type: 'password', placeholder: 'MTA...' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'slack',
|
||||
icon: 'hash',
|
||||
titleKey: 'engine.hermesChannelSlack',
|
||||
descKey: 'engine.hermesChannelSlackDesc',
|
||||
secretFields: ['botToken', 'appToken', 'signingSecret'],
|
||||
fields: [
|
||||
{ key: 'botToken', labelKey: 'engine.hermesChannelSlackBotToken', type: 'password', placeholder: 'xoxb-...' },
|
||||
{ key: 'appToken', labelKey: 'engine.hermesChannelSlackAppToken', type: 'password', placeholder: 'xapp-...' },
|
||||
{ key: 'signingSecret', labelKey: 'engine.hermesChannelSigningSecret', type: 'password', placeholder: 'optional' },
|
||||
{ key: 'webhookPath', labelKey: 'engine.hermesChannelWebhookPath', type: 'text', placeholder: '/slack/events' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'feishu',
|
||||
icon: 'send',
|
||||
titleKey: 'engine.hermesChannelFeishu',
|
||||
descKey: 'engine.hermesChannelFeishuDesc',
|
||||
secretFields: ['appSecret'],
|
||||
fields: [
|
||||
{ key: 'appId', labelKey: 'engine.hermesChannelFeishuAppId', type: 'text', placeholder: 'cli_xxx' },
|
||||
{ key: 'appSecret', labelKey: 'engine.hermesChannelFeishuAppSecret', type: 'password', placeholder: 'app secret' },
|
||||
{ key: 'domain', labelKey: 'engine.hermesChannelFeishuDomain', type: 'select', options: [['feishu', 'engine.hermesChannelFeishuDomainCn'], ['lark', 'engine.hermesChannelFeishuDomainIntl']] },
|
||||
{ key: 'connectionMode', labelKey: 'engine.hermesChannelConnectionMode', type: 'select', options: [['websocket', 'WebSocket'], ['webhook', 'Webhook']] },
|
||||
{ key: 'webhookPath', labelKey: 'engine.hermesChannelWebhookPath', type: 'text', placeholder: '/feishu/webhook' },
|
||||
{ key: 'reactionNotifications', labelKey: 'engine.hermesChannelReactions', type: 'select', options: [['off', 'engine.hermesChannelReactionsOff'], ['basic', 'engine.hermesChannelReactionsBasic']] },
|
||||
],
|
||||
toggles: [
|
||||
{ key: 'typingIndicator', labelKey: 'engine.hermesChannelTypingIndicator' },
|
||||
{ key: 'resolveSenderNames', labelKey: 'engine.hermesChannelResolveSenderNames' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const COMMON_FIELDS = [
|
||||
{ key: 'dmPolicy', labelKey: 'engine.hermesChannelDmPolicy', type: 'select', options: [['pair', 'engine.hermesChannelPolicyPair'], ['open', 'engine.hermesChannelPolicyOpen'], ['allowlist', 'engine.hermesChannelPolicyAllowlist'], ['disabled', 'engine.hermesChannelPolicyDisabled']] },
|
||||
{ key: 'groupPolicy', labelKey: 'engine.hermesChannelGroupPolicy', type: 'select', options: [['allowlist', 'engine.hermesChannelPolicyAllowlist'], ['open', 'engine.hermesChannelPolicyOpen'], ['disabled', 'engine.hermesChannelPolicyDisabled']] },
|
||||
{ key: 'allowFrom', labelKey: 'engine.hermesChannelAllowFrom', type: 'textarea', placeholderKey: 'engine.hermesChannelAllowFromPlaceholder' },
|
||||
{ key: 'groupAllowFrom', labelKey: 'engine.hermesChannelGroupAllowFrom', type: 'textarea', placeholderKey: 'engine.hermesChannelGroupAllowFromPlaceholder' },
|
||||
]
|
||||
|
||||
function esc(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function channelMeta(id) {
|
||||
return CHANNELS.find(channel => channel.id === id) || CHANNELS[0]
|
||||
}
|
||||
|
||||
function defaultForm(platform) {
|
||||
const form = {
|
||||
enabled: false,
|
||||
dmPolicy: 'pair',
|
||||
groupPolicy: 'allowlist',
|
||||
allowFrom: '',
|
||||
groupAllowFrom: '',
|
||||
requireMention: true,
|
||||
}
|
||||
if (platform === 'feishu') {
|
||||
form.domain = 'feishu'
|
||||
form.connectionMode = 'websocket'
|
||||
form.webhookPath = '/feishu/webhook'
|
||||
form.reactionNotifications = 'off'
|
||||
form.typingIndicator = true
|
||||
form.resolveSenderNames = true
|
||||
}
|
||||
if (platform === 'slack') form.webhookPath = '/slack/events'
|
||||
return form
|
||||
}
|
||||
|
||||
function normalizeForm(platform, form = {}) {
|
||||
return { ...defaultForm(platform), ...(form || {}) }
|
||||
}
|
||||
|
||||
function valueOf(form, key) {
|
||||
const value = form?.[key]
|
||||
return value == null ? '' : String(value)
|
||||
}
|
||||
|
||||
function isConfigured(channel, form) {
|
||||
return channel.secretFields.some(key => valueOf(form, key).trim())
|
||||
}
|
||||
|
||||
function renderField(field, form, disabled) {
|
||||
const value = valueOf(form, field.key)
|
||||
const label = esc(t(field.labelKey))
|
||||
if (field.type === 'select') {
|
||||
return `
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${label}</span>
|
||||
<select class="hm-input hm-channel-input" data-key="${esc(field.key)}" ${disabled ? 'disabled' : ''}>
|
||||
${(field.options || []).map(([optionValue, optionLabel]) => `
|
||||
<option value="${esc(optionValue)}" ${value === optionValue ? 'selected' : ''}>${esc(optionLabel.startsWith('engine.') ? t(optionLabel) : optionLabel)}</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
</label>
|
||||
`
|
||||
}
|
||||
if (field.type === 'textarea') {
|
||||
return `
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${label}</span>
|
||||
<textarea class="hm-input hm-channel-input hm-channel-textarea" data-key="${esc(field.key)}" ${disabled ? 'disabled' : ''} placeholder="${esc(t(field.placeholderKey))}">${esc(value)}</textarea>
|
||||
</label>
|
||||
`
|
||||
}
|
||||
return `
|
||||
<label class="hm-field">
|
||||
<span class="hm-field-label">${label}</span>
|
||||
<input class="hm-input hm-channel-input" data-key="${esc(field.key)}" type="${esc(field.type || 'text')}" value="${esc(value)}" ${disabled ? 'disabled' : ''} placeholder="${esc(field.placeholder || '')}" autocomplete="off">
|
||||
</label>
|
||||
`
|
||||
}
|
||||
|
||||
function collectForm(el, platform) {
|
||||
const form = normalizeForm(platform, {})
|
||||
el.querySelectorAll('.hm-channel-input').forEach(input => {
|
||||
const key = input.dataset.key
|
||||
if (!key) return
|
||||
if (input.type === 'checkbox') form[key] = input.checked
|
||||
else form[key] = input.value
|
||||
})
|
||||
return form
|
||||
}
|
||||
|
||||
export function render() {
|
||||
const el = document.createElement('div')
|
||||
el.className = 'page'
|
||||
el.className = 'page hm-channels-page'
|
||||
el.dataset.engine = 'hermes'
|
||||
el.innerHTML = `
|
||||
<div class="page-header"><h1>${t('engine.hermesChannelsTitle')}</h1></div>
|
||||
<div class="card"><div class="card-body" style="padding:32px;text-align:center;color:var(--text-tertiary)">
|
||||
${t('engine.comingSoonPhase2')}
|
||||
</div></div>
|
||||
`
|
||||
|
||||
let active = 'telegram'
|
||||
let values = {}
|
||||
let configPath = ''
|
||||
let loading = true
|
||||
let saving = false
|
||||
let error = ''
|
||||
let success = ''
|
||||
|
||||
function draw() {
|
||||
const channel = channelMeta(active)
|
||||
const form = normalizeForm(active, values[active])
|
||||
const disabled = loading || saving
|
||||
const enabledCount = CHANNELS.filter(item => normalizeForm(item.id, values[item.id]).enabled).length
|
||||
const configuredCount = CHANNELS.filter(item => isConfigured(item, normalizeForm(item.id, values[item.id]))).length
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="hm-hero">
|
||||
<div class="hm-hero-title">
|
||||
<div class="hm-hero-eyebrow">${esc(t('engine.hermesChannelsEyebrow'))}</div>
|
||||
<h1 class="hm-hero-h1">${esc(t('engine.hermesChannelsTitle'))}</h1>
|
||||
<div class="hm-hero-sub">${esc(configPath || '~/.hermes/config.yaml')}</div>
|
||||
</div>
|
||||
<div class="hm-hero-actions">
|
||||
<button class="hm-btn hm-btn--ghost hm-btn--sm" id="hm-channels-reload" ${disabled ? 'disabled' : ''}>${icon('refresh-cw', 14)}${esc(t('engine.hermesConfigReload'))}</button>
|
||||
<button class="hm-btn hm-btn--cta hm-btn--sm" id="hm-channels-save" ${disabled ? 'disabled' : ''}>${saving ? esc(t('engine.hermesChannelSaving')) : esc(t('engine.hermesChannelSave'))}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="hm-channel-summary" aria-label="${esc(t('engine.hermesChannelSummary'))}">
|
||||
<div class="hm-channel-stat"><span>${esc(t('engine.hermesChannelEnabledCount'))}</span><strong>${enabledCount}</strong></div>
|
||||
<div class="hm-channel-stat"><span>${esc(t('engine.hermesChannelConfiguredCount'))}</span><strong>${configuredCount}</strong></div>
|
||||
<div class="hm-channel-stat"><span>${esc(t('engine.hermesChannelRuntimeWrite'))}</span><strong>YAML + .env</strong></div>
|
||||
</section>
|
||||
|
||||
${(error || success) ? `
|
||||
<div class="hm-channel-alert ${error ? 'is-error' : 'is-success'}">
|
||||
${icon(error ? 'alert-triangle' : 'check-circle', 15)}
|
||||
<span>${esc(error || success)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="hm-channel-layout">
|
||||
<section class="hm-panel hm-channel-list-panel">
|
||||
<div class="hm-panel-header">
|
||||
<div class="hm-panel-title">${esc(t('engine.hermesChannelPlatforms'))}</div>
|
||||
</div>
|
||||
<div class="hm-panel-body hm-panel-body--tight">
|
||||
<div class="hm-channel-list" role="tablist" aria-label="${esc(t('engine.hermesChannelPlatforms'))}">
|
||||
${CHANNELS.map(item => {
|
||||
const itemForm = normalizeForm(item.id, values[item.id])
|
||||
return `
|
||||
<button class="hm-channel-tab ${item.id === active ? 'is-active' : ''}" data-channel="${esc(item.id)}" role="tab" aria-selected="${item.id === active ? 'true' : 'false'}" ${disabled ? 'disabled' : ''}>
|
||||
<span class="hm-channel-tab-icon">${icon(item.icon, 16)}</span>
|
||||
<span class="hm-channel-tab-main">
|
||||
<strong>${esc(t(item.titleKey))}</strong>
|
||||
<small>${esc(itemForm.enabled ? t('engine.hermesChannelEnabled') : t('engine.hermesChannelDisabled'))}</small>
|
||||
</span>
|
||||
<span class="hm-channel-dot ${itemForm.enabled ? 'is-on' : ''}" aria-hidden="true"></span>
|
||||
</button>
|
||||
`
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hm-panel hm-channel-form-panel">
|
||||
<div class="hm-panel-header">
|
||||
<div>
|
||||
<div class="hm-panel-title">${icon(channel.icon, 15)}${esc(t(channel.titleKey))}</div>
|
||||
<div class="hm-channel-panel-desc">${esc(t(channel.descKey))}</div>
|
||||
</div>
|
||||
<label class="hm-channel-switch">
|
||||
<input class="hm-channel-input" data-key="enabled" type="checkbox" ${form.enabled ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${esc(form.enabled ? t('engine.hermesChannelEnabled') : t('engine.hermesChannelDisabled'))}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hm-panel-body">
|
||||
${loading ? `
|
||||
<div class="hm-channel-loading">${esc(t('common.loading'))}...</div>
|
||||
` : `
|
||||
<div class="hm-channel-section">
|
||||
<div class="hm-channel-section-title">${esc(t('engine.hermesChannelCredentials'))}</div>
|
||||
<div class="hm-field-row">
|
||||
${channel.fields.map(field => renderField(field, form, disabled)).join('')}
|
||||
</div>
|
||||
${(channel.toggles || []).length ? `
|
||||
<div class="hm-channel-toggle-grid">
|
||||
${channel.toggles.map(toggle => `
|
||||
<label class="hm-channel-check">
|
||||
<input class="hm-channel-input" data-key="${esc(toggle.key)}" type="checkbox" ${form[toggle.key] ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${esc(t(toggle.labelKey))}</span>
|
||||
</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="hm-channel-section">
|
||||
<div class="hm-channel-section-title">${esc(t('engine.hermesChannelAccessPolicy'))}</div>
|
||||
<div class="hm-field-row">
|
||||
${COMMON_FIELDS.slice(0, 2).map(field => renderField(field, form, disabled)).join('')}
|
||||
</div>
|
||||
<label class="hm-channel-check hm-channel-check--wide">
|
||||
<input class="hm-channel-input" data-key="requireMention" type="checkbox" ${form.requireMention ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
|
||||
<span>${esc(t('engine.hermesChannelRequireMention'))}</span>
|
||||
</label>
|
||||
<div class="hm-field-row">
|
||||
${COMMON_FIELDS.slice(2).map(field => renderField(field, form, disabled)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hm-channel-footnote">
|
||||
${icon('info', 14)}
|
||||
<span>${esc(t('engine.hermesChannelRestartHint'))}</span>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`
|
||||
|
||||
el.querySelector('#hm-channels-reload')?.addEventListener('click', load)
|
||||
el.querySelector('#hm-channels-save')?.addEventListener('click', save)
|
||||
el.querySelectorAll('.hm-channel-tab').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
if (!loading && !saving) values = { ...values, [active]: collectForm(el, active) }
|
||||
active = button.dataset.channel || active
|
||||
error = ''
|
||||
success = ''
|
||||
draw()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading = true
|
||||
error = ''
|
||||
success = ''
|
||||
draw()
|
||||
try {
|
||||
const data = await api.hermesChannelConfigRead()
|
||||
values = data?.values || {}
|
||||
configPath = data?.configPath || ''
|
||||
} catch (err) {
|
||||
error = humanizeErrorText(err, t('engine.hermesChannelLoadFailed'))
|
||||
} finally {
|
||||
loading = false
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const form = collectForm(el, active)
|
||||
values = { ...values, [active]: form }
|
||||
saving = true
|
||||
error = ''
|
||||
success = ''
|
||||
draw()
|
||||
try {
|
||||
const result = await api.hermesChannelConfigSave(active, form)
|
||||
values = { ...values, [active]: result?.values || form }
|
||||
success = t('engine.hermesChannelSaved')
|
||||
toast(success, 'success')
|
||||
} catch (err) {
|
||||
error = humanizeErrorText(err, t('engine.hermesChannelSaveFailed'))
|
||||
toast(error, 'error')
|
||||
} finally {
|
||||
saving = false
|
||||
draw()
|
||||
}
|
||||
}
|
||||
|
||||
draw()
|
||||
load()
|
||||
return el
|
||||
}
|
||||
|
||||
@@ -6593,6 +6593,210 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
[data-engine="hermes"].hm-channels-page {
|
||||
max-width: 1280px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin: -12px 0 18px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-stat {
|
||||
min-height: 72px;
|
||||
border: 1px solid var(--hm-border);
|
||||
border-radius: var(--hm-radius-md);
|
||||
background: var(--hm-surface-1);
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-stat span {
|
||||
font-family: var(--hm-font-serif);
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
color: var(--hm-text-tertiary);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-stat strong {
|
||||
font-family: var(--hm-font-mono);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--hm-text-primary);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-alert {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin: 0 0 18px;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--hm-radius-sm);
|
||||
border: 1px solid var(--hm-border);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-alert.is-error {
|
||||
color: var(--hm-error);
|
||||
background: var(--hm-error-soft);
|
||||
border-color: color-mix(in srgb, var(--hm-error) 24%, transparent);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-alert.is-success {
|
||||
color: var(--hm-success);
|
||||
background: var(--hm-success-soft);
|
||||
border-color: color-mix(in srgb, var(--hm-success) 24%, transparent);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-list-panel {
|
||||
position: sticky;
|
||||
top: 18px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-tab {
|
||||
width: 100%;
|
||||
min-height: 58px;
|
||||
border: 1px solid var(--hm-border);
|
||||
border-radius: var(--hm-radius-sm);
|
||||
background: var(--hm-surface-0);
|
||||
color: var(--hm-text-primary);
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr) 10px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--hm-dur-fast) var(--hm-ease),
|
||||
background var(--hm-dur-fast) var(--hm-ease);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-tab:hover:not(:disabled) {
|
||||
border-color: var(--hm-border-strong);
|
||||
background: var(--hm-surface-2);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-tab.is-active {
|
||||
border-color: var(--hm-accent);
|
||||
background: var(--hm-accent-soft);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-tab:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.58;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-tab:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--hm-accent-ring);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-tab-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid var(--hm-border);
|
||||
border-radius: var(--hm-radius-sm);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--hm-accent);
|
||||
background: var(--hm-surface-1);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-tab-main {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-tab-main strong {
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-tab-main small {
|
||||
color: var(--hm-text-tertiary);
|
||||
font-size: 11px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--hm-text-muted);
|
||||
box-shadow: 0 0 0 3px var(--hm-border-subtle);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-dot.is-on {
|
||||
background: var(--hm-success);
|
||||
box-shadow: 0 0 0 3px var(--hm-success-soft);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-form-panel .hm-panel-header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-panel-desc {
|
||||
margin-top: 6px;
|
||||
color: var(--hm-text-tertiary);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-width: 720px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-switch,
|
||||
[data-engine="hermes"] .hm-channel-check {
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--hm-text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
cursor: pointer;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-switch input,
|
||||
[data-engine="hermes"] .hm-channel-check input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--hm-accent);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-section {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-section + .hm-channel-section {
|
||||
margin-top: 26px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--hm-border-subtle);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-section-title {
|
||||
font-family: var(--hm-font-serif);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--hm-text-primary);
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-textarea {
|
||||
min-height: 104px;
|
||||
height: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-toggle-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px 16px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-check--wide {
|
||||
width: fit-content;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-footnote,
|
||||
[data-engine="hermes"] .hm-channel-loading {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
color: var(--hm-text-tertiary);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
[data-engine="hermes"].hm-usage-page {
|
||||
padding: 24px 22px 34px;
|
||||
@@ -6609,6 +6813,15 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
[data-engine="hermes"] .hm-services-action-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-list-panel {
|
||||
position: static;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@@ -6642,6 +6855,31 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
[data-engine="hermes"] .hm-services-health-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
[data-engine="hermes"].hm-channels-page {
|
||||
padding: 24px 18px 34px;
|
||||
}
|
||||
[data-engine="hermes"].hm-channels-page .hm-hero {
|
||||
align-items: stretch;
|
||||
}
|
||||
[data-engine="hermes"].hm-channels-page .hm-hero-actions {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
[data-engine="hermes"].hm-channels-page .hm-hero-actions .hm-btn {
|
||||
flex: 1 1 150px;
|
||||
min-height: 44px;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-summary,
|
||||
[data-engine="hermes"] .hm-channel-list,
|
||||
[data-engine="hermes"] .hm-channel-toggle-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-form-panel .hm-panel-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
[data-engine="hermes"] .hm-channel-switch {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Responsive breakpoints ---- */
|
||||
@@ -6720,4 +6958,3 @@ body[data-active-engine="hermes"][data-theme="dark"] {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user