diff --git a/server/src/wled_controller/static/css/cards.css b/server/src/wled_controller/static/css/cards.css
index 8b149f3..92e02b2 100644
--- a/server/src/wled_controller/static/css/cards.css
+++ b/server/src/wled_controller/static/css/cards.css
@@ -204,6 +204,26 @@ body.cs-drag-active .card-drag-handle {
position: static;
}
+.card-actions .color-picker-wrapper,
+.template-card-actions .color-picker-wrapper {
+ display: flex;
+ align-items: center;
+ margin-left: auto;
+}
+
+.card-actions .color-picker-popover,
+.template-card-actions .color-picker-popover {
+ top: auto;
+ bottom: calc(100% + 8px);
+ left: auto;
+ right: 0;
+}
+
+.card.cp-elevated,
+.template-card.cp-elevated {
+ z-index: 10;
+}
+
.card-autostart-btn {
background: none;
border: none;
diff --git a/server/src/wled_controller/static/css/layout.css b/server/src/wled_controller/static/css/layout.css
index 034f4a3..7bb18d0 100644
--- a/server/src/wled_controller/static/css/layout.css
+++ b/server/src/wled_controller/static/css/layout.css
@@ -315,6 +315,20 @@ h2 {
color: var(--text-secondary);
cursor: pointer;
}
+.color-picker-reset {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 6px;
+ padding-top: 6px;
+ border-top: 1px solid var(--border-color);
+ font-size: 0.78rem;
+ color: var(--text-secondary);
+ cursor: pointer;
+}
+.color-picker-reset:hover {
+ color: var(--danger-color);
+}
.color-picker-custom input[type="color"] {
width: 28px;
height: 28px;
diff --git a/server/src/wled_controller/static/js/core/card-colors.js b/server/src/wled_controller/static/js/core/card-colors.js
new file mode 100644
index 0000000..52c9f9d
--- /dev/null
+++ b/server/src/wled_controller/static/js/core/card-colors.js
@@ -0,0 +1,113 @@
+/**
+ * Card color assignment — localStorage-backed color labels for any card.
+ *
+ * Usage in card creation functions:
+ * import { wrapCard } from '../core/card-colors.js';
+ *
+ * return wrapCard({
+ * dataAttr: 'data-device-id',
+ * id: device.id,
+ * removeOnclick: `removeDevice('${device.id}')`,
+ * removeTitle: t('common.delete'),
+ * content: `
`,
+ * actions: ``,
+ * });
+ *
+ * The helper wraps content in a standard card shell:
+ * - Outer div (.card / .template-card) with border-left color
+ * - .card-top-actions with remove button + optional top buttons
+ * - Bottom actions (.card-actions / .template-card-actions) with color picker
+ */
+
+import { createColorPicker, registerColorPicker } from './color-picker.js';
+
+const STORAGE_KEY = 'cardColors';
+const DEFAULT_SWATCH = '#808080';
+
+function _getAll() {
+ try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}; }
+ catch { return {}; }
+}
+
+export function getCardColor(id) {
+ return _getAll()[id] || '';
+}
+
+export function setCardColor(id, hex) {
+ const m = _getAll();
+ if (hex) m[id] = hex; else delete m[id];
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(m));
+}
+
+/**
+ * Returns inline style string for card border-left.
+ * Empty string when no color is set.
+ */
+export function cardColorStyle(entityId) {
+ const c = getCardColor(entityId);
+ return c ? `border-left: 3px solid ${c}` : '';
+}
+
+/**
+ * Returns color picker HTML + registers the pick callback.
+ * @param {string} entityId Unique entity ID
+ * @param {string} cardAttr Data attribute selector, e.g. 'data-device-id'
+ */
+export function cardColorButton(entityId, cardAttr) {
+ const color = getCardColor(entityId) || DEFAULT_SWATCH;
+ const pickerId = `cc-${entityId}`;
+
+ registerColorPicker(pickerId, (hex) => {
+ setCardColor(entityId, hex);
+ const card = document.querySelector(`[${cardAttr}="${entityId}"]`);
+ if (card) card.style.borderLeft = hex ? `3px solid ${hex}` : '';
+ });
+
+ return createColorPicker({ id: pickerId, currentColor: color, anchor: 'left', showReset: true, resetColor: DEFAULT_SWATCH });
+}
+
+/**
+ * Build a standard card shell with color support.
+ *
+ * Provides consistent structure across all card types:
+ * - .card-top-actions: remove button + optional extra top buttons
+ * - Bottom actions: action buttons + color picker (always last)
+ * - Automatic border-left color from localStorage
+ *
+ * @param {object} opts
+ * @param {'card'|'template-card'} [opts.type='card'] Card CSS class
+ * @param {string} opts.dataAttr Data attribute name, e.g. 'data-device-id'
+ * @param {string} opts.id Entity ID value
+ * @param {string} [opts.classes] Extra CSS classes on root element
+ * @param {string} [opts.topButtons] HTML for extra top-right buttons (power, autostart)
+ * @param {string} opts.removeOnclick onclick handler string for remove button
+ * @param {string} opts.removeTitle title attribute for remove button
+ * @param {string} opts.content Inner HTML (header, props, metrics, etc.)
+ * @param {string} opts.actions Action button HTML (without wrapper div)
+ */
+export function wrapCard({
+ type = 'card',
+ dataAttr,
+ id,
+ classes = '',
+ topButtons = '',
+ removeOnclick,
+ removeTitle,
+ content,
+ actions,
+}) {
+ const actionsClass = type === 'template-card' ? 'template-card-actions' : 'card-actions';
+ const colorStyle = cardColorStyle(id);
+ return `
+
+
+ ${topButtons}
+
+
+ ${content}
+
+ ${actions}
+ ${cardColorButton(id, dataAttr)}
+
+
`;
+}
diff --git a/server/src/wled_controller/static/js/core/color-picker.js b/server/src/wled_controller/static/js/core/color-picker.js
index 79bcf0c..0688583 100644
--- a/server/src/wled_controller/static/js/core/color-picker.js
+++ b/server/src/wled_controller/static/js/core/color-picker.js
@@ -28,12 +28,16 @@ const PRESETS = [
/**
* Build the HTML string for a color-picker widget.
*/
-export function createColorPicker({ id, currentColor, onPick, anchor = 'right' }) {
+export function createColorPicker({ id, currentColor, onPick, anchor = 'right', showReset = false, resetColor = '#808080' }) {
const dots = PRESETS.map(c => {
const active = c.toLowerCase() === currentColor.toLowerCase() ? ' active' : '';
return ``;
}).join('');
+ const resetBtn = showReset
+ ? `${t('accent.reset')}
`
+ : '';
+
return `` +
`` +
`` +
@@ -44,6 +48,7 @@ export function createColorPicker({ id, currentColor, onPick, anchor = 'right' }
`onchange="event.stopPropagation(); window._cpPick('${id}',this.value)">` +
`${t('accent.custom')}` +
`
` +
+ resetBtn +
`` +
``;
}
@@ -65,14 +70,21 @@ function _rgbToHex(rgb) {
}
window._cpToggle = function (id) {
- // Close all other pickers first
+ // Close all other pickers first (and drop their card elevation)
document.querySelectorAll('.color-picker-popover').forEach(p => {
- if (p.id !== `cp-pop-${id}`) p.style.display = 'none';
+ if (p.id !== `cp-pop-${id}`) {
+ p.style.display = 'none';
+ const card = p.closest('.card, .template-card');
+ if (card) card.classList.remove('cp-elevated');
+ }
});
const pop = document.getElementById(`cp-pop-${id}`);
if (!pop) return;
const show = pop.style.display === 'none';
pop.style.display = show ? '' : 'none';
+ // Elevate the card so the popover isn't clipped by sibling cards
+ const card = pop.closest('.card, .template-card');
+ if (card) card.classList.toggle('cp-elevated', show);
if (show) {
// Mark active dot
const swatch = document.getElementById(`cp-swatch-${id}`);
@@ -99,13 +111,35 @@ window._cpPick = function (id, hex) {
d.classList.toggle('active', dHex.toLowerCase() === hex.toLowerCase());
});
pop.style.display = 'none';
+ const card = pop.closest('.card, .template-card');
+ if (card) card.classList.remove('cp-elevated');
}
// Fire callback
if (_callbacks[id]) _callbacks[id](hex);
};
+window._cpReset = function (id, resetColor) {
+ // Reset swatch to neutral color
+ const swatch = document.getElementById(`cp-swatch-${id}`);
+ if (swatch) swatch.style.background = resetColor;
+ // Clear active dots and close popover
+ const pop = document.getElementById(`cp-pop-${id}`);
+ if (pop) {
+ pop.querySelectorAll('.color-picker-dot').forEach(d => d.classList.remove('active'));
+ pop.style.display = 'none';
+ const card = pop.closest('.card, .template-card');
+ if (card) card.classList.remove('cp-elevated');
+ }
+ // Fire callback with empty string to signal removal
+ if (_callbacks[id]) _callbacks[id]('');
+};
+
export function closeAllColorPickers() {
- document.querySelectorAll('.color-picker-popover').forEach(p => p.style.display = 'none');
+ document.querySelectorAll('.color-picker-popover').forEach(p => {
+ p.style.display = 'none';
+ const card = p.closest('.card, .template-card');
+ if (card) card.classList.remove('cp-elevated');
+ });
}
// Close on outside click
diff --git a/server/src/wled_controller/static/js/features/automations.js b/server/src/wled_controller/static/js/features/automations.js
index 7dd9bf2..d6f9f11 100644
--- a/server/src/wled_controller/static/js/features/automations.js
+++ b/server/src/wled_controller/static/js/features/automations.js
@@ -10,6 +10,7 @@ import { Modal } from '../core/modal.js';
import { CardSection } from '../core/card-sections.js';
import { updateTabBadge } from './tabs.js';
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js';
+import { wrapCard } from '../core/card-colors.js';
import { csScenes, createSceneCard } from './scene-presets.js';
class AutomationEditorModal extends Modal {
@@ -152,11 +153,13 @@ function createAutomationCard(automation, sceneMap = new Map()) {
lastActivityMeta = `${ICON_CLOCK} ${ts.toLocaleString()}`;
}
- return `
-
-
-
-
+ return wrapCard({
+ dataAttr: 'data-automation-id',
+ id: automation.id,
+ classes: !automation.enabled ? 'automation-status-disabled' : '',
+ removeOnclick: `deleteAutomation('${automation.id}', '${escapeHtml(automation.name)}')`,
+ removeTitle: t('common.delete'),
+ content: `
`;
+ `,
+ });
}
export async function openAutomationEditor(automationId) {
diff --git a/server/src/wled_controller/static/js/features/color-strips.js b/server/src/wled_controller/static/js/features/color-strips.js
index 60967bb..b6bb195 100644
--- a/server/src/wled_controller/static/js/features/color-strips.js
+++ b/server/src/wled_controller/static/js/features/color-strips.js
@@ -13,6 +13,7 @@ import {
ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM,
ICON_LINK, ICON_SPARKLES, ICON_FAST_FORWARD, ICON_ACTIVITY,
} from '../core/icons.js';
+import { wrapCard } from '../core/card-colors.js';
class CSSEditorModal extends Modal {
constructor() {
@@ -707,9 +708,12 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
? `
`
: '';
- return `
-
-
+ return wrapCard({
+ dataAttr: 'data-css-id',
+ id: source.id,
+ removeOnclick: `deleteColorStrip('${source.id}')`,
+ removeTitle: t('common.delete'),
+ content: `
-
- `;
+ ${calibrationBtn}`,
+ });
}
/* ── Editor open/close ────────────────────────────────────────── */
diff --git a/server/src/wled_controller/static/js/features/devices.js b/server/src/wled_controller/static/js/features/devices.js
index 2948159..f873892 100644
--- a/server/src/wled_controller/static/js/features/devices.js
+++ b/server/src/wled_controller/static/js/features/devices.js
@@ -10,6 +10,7 @@ import { t } from '../core/i18n.js';
import { showToast, showConfirm } from '../core/ui.js';
import { Modal } from '../core/modal.js';
import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_LED, ICON_WEB, ICON_PLUG } from '../core/icons.js';
+import { wrapCard } from '../core/card-colors.js';
class DeviceSettingsModal extends Modal {
constructor() { super('device-settings-modal'); }
@@ -77,12 +78,13 @@ export function createDeviceCard(device) {
const ledCount = state.device_led_count || device.led_count;
- return `
-
-
- ${(device.capabilities || []).includes('power_control') ? `` : ''}
-
-
+ return wrapCard({
+ dataAttr: 'data-device-id',
+ id: device.id,
+ topButtons: (device.capabilities || []).includes('power_control') ? `
` : '',
+ removeOnclick: `removeDevice('${device.id}')`,
+ removeTitle: t('device.button.remove'),
+ content: `
- `;
+
` : ''}`,
+ actions: `
+
`,
+ });
}
export async function turnOffDevice(deviceId) {
diff --git a/server/src/wled_controller/static/js/features/kc-targets.js b/server/src/wled_controller/static/js/features/kc-targets.js
index 2c0c65e..11bfc20 100644
--- a/server/src/wled_controller/static/js/features/kc-targets.js
+++ b/server/src/wled_controller/static/js/features/kc-targets.js
@@ -19,6 +19,7 @@ import {
ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_START, ICON_STOP, ICON_PAUSE,
ICON_LINK_SOURCE, ICON_PATTERN_TEMPLATE, ICON_FPS, ICON_PALETTE,
} from '../core/icons.js';
+import { wrapCard } from '../core/card-colors.js';
class KCEditorModal extends Modal {
constructor() {
@@ -118,12 +119,13 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
swatchesHtml = `
${t('kc.colors.none')}`;
}
- return `
-
-
-
-
-
+ return wrapCard({
+ dataAttr: 'data-kc-target-id',
+ id: target.id,
+ topButtons: `
`,
+ removeOnclick: `deleteKCTarget('${target.id}')`,
+ removeTitle: t('common.delete'),
+ content: `
- `;
+ `,
+ });
}
// ===== KEY COLORS TEST =====
diff --git a/server/src/wled_controller/static/js/features/pattern-templates.js b/server/src/wled_controller/static/js/features/pattern-templates.js
index c6cb969..6726d74 100644
--- a/server/src/wled_controller/static/js/features/pattern-templates.js
+++ b/server/src/wled_controller/static/js/features/pattern-templates.js
@@ -19,6 +19,7 @@ import { t } from '../core/i18n.js';
import { showToast, showConfirm } from '../core/ui.js';
import { Modal } from '../core/modal.js';
import { getPictureSourceIcon, ICON_PATTERN_TEMPLATE, ICON_CLONE, ICON_EDIT } from '../core/icons.js';
+import { wrapCard } from '../core/card-colors.js';
class PatternTemplateModal extends Modal {
constructor() {
@@ -52,22 +53,24 @@ const patternModal = new PatternTemplateModal();
export function createPatternTemplateCard(pt) {
const rectCount = (pt.rectangles || []).length;
const desc = pt.description ? `
${escapeHtml(pt.description)}
` : '';
- return `
-
-
+ return wrapCard({
+ type: 'template-card',
+ dataAttr: 'data-pattern-template-id',
+ id: pt.id,
+ removeOnclick: `deletePatternTemplate('${pt.id}')`,
+ removeTitle: t('common.delete'),
+ content: `
${desc}
▭ ${rectCount} rect${rectCount !== 1 ? 's' : ''}
-
-
+
`,
+ actions: `
-
-
-
- `;
+
`,
+ });
}
export async function showPatternTemplateEditor(templateId = null, cloneData = null) {
diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js
index fbb315d..16b25e0 100644
--- a/server/src/wled_controller/static/js/features/streams.js
+++ b/server/src/wled_controller/static/js/features/streams.js
@@ -45,6 +45,7 @@ import {
ICON_AUDIO_TEMPLATE, ICON_MONITOR, ICON_WRENCH, ICON_RADIO,
ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_HELP,
} from '../core/icons.js';
+import { wrapCard } from '../core/card-colors.js';
// ── Card section instances ──
const csRawStreams = new CardSection('raw-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('raw')", keyAttr: 'data-stream-id' });
@@ -1184,29 +1185,35 @@ function renderPictureSourcesList(streams) {
`;
}
- return `
-
-
+ return wrapCard({
+ type: 'template-card',
+ dataAttr: 'data-stream-id',
+ id: stream.id,
+ removeOnclick: `deleteStream('${stream.id}')`,
+ removeTitle: t('common.delete'),
+ content: `
${detailsHtml}
- ${stream.description ? `
${escapeHtml(stream.description)}
` : ''}
-
+ ${stream.description ? `
${escapeHtml(stream.description)}
` : ''}`,
+ actions: `
-
-
-
- `;
+ `,
+ });
};
const renderCaptureTemplateCard = (template) => {
const engineIcon = getEngineIcon(template.engine_type);
const configEntries = Object.entries(template.engine_config);
- return `
-
-
+ return wrapCard({
+ type: 'template-card',
+ dataAttr: 'data-template-id',
+ id: template.id,
+ removeOnclick: `deleteTemplate('${template.id}')`,
+ removeTitle: t('common.delete'),
+ content: `
@@ -1227,14 +1234,12 @@ function renderPictureSourcesList(streams) {
`).join('')}
- ` : ''}
-
+ ` : ''}`,
+ actions: `
-
-
-
- `;
+ `,
+ });
};
const renderPPTemplateCard = (tmpl) => {
@@ -1243,21 +1248,23 @@ function renderPictureSourcesList(streams) {
const filterNames = tmpl.filters.map(fi => `${escapeHtml(_getFilterName(fi.filter_id))}`);
filterChainHtml = `${filterNames.join('→')}
`;
}
- return `
-
-
+ return wrapCard({
+ type: 'template-card',
+ dataAttr: 'data-pp-template-id',
+ id: tmpl.id,
+ removeOnclick: `deletePPTemplate('${tmpl.id}')`,
+ removeTitle: t('common.delete'),
+ content: `
${tmpl.description ? `
${escapeHtml(tmpl.description)}
` : ''}
- ${filterChainHtml}
-
+ ${filterChainHtml}`,
+ actions: `
-
-
-
- `;
+ `,
+ });
};
const rawStreams = streams.filter(s => s.stream_type === 'raw');
@@ -1305,28 +1312,34 @@ function renderPictureSourcesList(streams) {
propsHtml = `${devLabel} #${devIdx}${tplBadge}`;
}
- return `
-
-
+ return wrapCard({
+ type: 'template-card',
+ dataAttr: 'data-id',
+ id: src.id,
+ removeOnclick: `deleteAudioSource('${src.id}')`,
+ removeTitle: t('common.delete'),
+ content: `
${propsHtml}
- ${src.description ? `
${escapeHtml(src.description)}
` : ''}
-
+ ${src.description ? `
${escapeHtml(src.description)}
` : ''}`,
+ actions: `
-
-
-
- `;
+ `,
+ });
};
const renderAudioTemplateCard = (template) => {
const configEntries = Object.entries(template.engine_config || {});
- return `
-
-
+ return wrapCard({
+ type: 'template-card',
+ dataAttr: 'data-audio-template-id',
+ id: template.id,
+ removeOnclick: `deleteAudioTemplate('${template.id}')`,
+ removeTitle: t('common.delete'),
+ content: `
@@ -1347,14 +1360,12 @@ function renderPictureSourcesList(streams) {
`).join('')}
- ` : ''}
-
+ ` : ''}`,
+ actions: `
-
-
-
- `;
+ `,
+ });
};
const panels = tabs.map(tab => {
diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js
index a923d8d..647729e 100644
--- a/server/src/wled_controller/static/js/features/targets.js
+++ b/server/src/wled_controller/static/js/features/targets.js
@@ -23,6 +23,7 @@ import {
ICON_LED, ICON_FPS, ICON_OVERLAY, ICON_LED_PREVIEW,
ICON_GLOBE, ICON_RADIO, ICON_PLUG, ICON_FILM, ICON_SUN_DIM, ICON_TARGET_ICON, ICON_HELP,
} from '../core/icons.js';
+import { wrapCard } from '../core/card-colors.js';
import { CardSection } from '../core/card-sections.js';
import { updateSubTabHash, updateTabBadge } from './tabs.js';
@@ -850,12 +851,13 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
healthTitle = devOnline ? t('device.health.online') : t('device.health.offline');
}
- return `
-
-
-
-
-
+ return wrapCard({
+ dataAttr: 'data-target-id',
+ id: target.id,
+ topButtons: `
`,
+ removeOnclick: `deleteTarget('${target.id}')`,
+ removeTitle: t('common.delete'),
+ content: `
- `;
+ `) : ''}`,
+ });
}
async function _targetAction(action) {
diff --git a/server/src/wled_controller/static/js/features/value-sources.js b/server/src/wled_controller/static/js/features/value-sources.js
index 721ceae..3fd395b 100644
--- a/server/src/wled_controller/static/js/features/value-sources.js
+++ b/server/src/wled_controller/static/js/features/value-sources.js
@@ -21,6 +21,7 @@ import {
ICON_LED_PREVIEW, ICON_ACTIVITY, ICON_TIMER, ICON_MOVE_VERTICAL,
ICON_MUSIC, ICON_TRENDING_UP, ICON_MAP_PIN, ICON_MONITOR, ICON_REFRESH,
} from '../core/icons.js';
+import { wrapCard } from '../core/card-colors.js';
import { loadPictureSources } from './streams.js';
export { getValueSourceIcon };
@@ -522,21 +523,23 @@ export function createValueSourceCard(src) {
`;
}
- return `
-
-
+ return wrapCard({
+ type: 'template-card',
+ dataAttr: 'data-id',
+ id: src.id,
+ removeOnclick: `deleteValueSource('${src.id}')`,
+ removeTitle: t('common.delete'),
+ content: `
${propsHtml}
- ${src.description ? `
${escapeHtml(src.description)}
` : ''}
-
+ ${src.description ? `
${escapeHtml(src.description)}
` : ''}`,
+ actions: `
-
-
-
- `;
+
`,
+ });
}
// ── Helpers ───────────────────────────────────────────────────
diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json
index 4b5bba5..8fbd09a 100644
--- a/server/src/wled_controller/static/locales/en.json
+++ b/server/src/wled_controller/static/locales/en.json
@@ -5,6 +5,7 @@
"theme.toggle": "Toggle theme",
"accent.title": "Accent color",
"accent.custom": "Custom",
+ "accent.reset": "Reset",
"locale.change": "Change language",
"auth.login": "Login",
"auth.logout": "Logout",
diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json
index 426cce1..548ae29 100644
--- a/server/src/wled_controller/static/locales/ru.json
+++ b/server/src/wled_controller/static/locales/ru.json
@@ -5,6 +5,7 @@
"theme.toggle": "Переключить тему",
"accent.title": "Цвет акцента",
"accent.custom": "Свой",
+ "accent.reset": "Сброс",
"locale.change": "Изменить язык",
"auth.login": "Войти",
"auth.logout": "Выйти",
diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json
index 273998e..c1134ce 100644
--- a/server/src/wled_controller/static/locales/zh.json
+++ b/server/src/wled_controller/static/locales/zh.json
@@ -5,6 +5,7 @@
"theme.toggle": "切换主题",
"accent.title": "主题色",
"accent.custom": "自定义",
+ "accent.reset": "重置",
"locale.change": "切换语言",
"auth.login": "登录",
"auth.logout": "退出",