fix: device card header layout — URL badge overflow and hide button gap
Lint & Test / test (push) Successful in 1m24s
Lint & Test / test (push) Successful in 1m24s
Move URL badge from card-title to card-subtitle row to prevent overlap with top-right action buttons. Widen card-header padding-right to 60px for 2-button clearance. Reorder hide button to first position in top-actions so power and trash stay visually adjacent.
This commit is contained in:
@@ -378,10 +378,39 @@ body.cs-drag-active .card-drag-handle {
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-top-actions .card-remove-btn {
|
.card-top-actions .card-remove-btn,
|
||||||
|
.card-top-actions .card-hide-btn {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-hide-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.card:hover .card-hide-btn,
|
||||||
|
.template-card:hover .card-hide-btn {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.card-hide-btn:hover {
|
||||||
|
opacity: 1 !important;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.card-hide-btn .icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hidden card dimmed style */
|
||||||
|
.cs-card-hidden {
|
||||||
|
opacity: 0.55;
|
||||||
|
border-style: dashed !important;
|
||||||
|
}
|
||||||
|
|
||||||
.card-actions .color-picker-wrapper,
|
.card-actions .color-picker-wrapper,
|
||||||
.template-card-actions .color-picker-wrapper {
|
.template-card-actions .color-picker-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -478,7 +507,7 @@ body.cs-drag-active .card-drag-handle {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
padding-right: 30px;
|
padding-right: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
@@ -1181,6 +1210,41 @@ ul.section-tip li {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Hidden cards toggle ──────────────────────────────────── */
|
||||||
|
|
||||||
|
.cs-hidden-toggle {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color 0.2s, background 0.2s, border-color 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.cs-hidden-toggle .icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
.cs-hidden-toggle:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.cs-hidden-toggle.active {
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: var(--primary-contrast, #fff);
|
||||||
|
}
|
||||||
|
.cs-hidden-badge {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Bulk selection ────────────────────────────────────────── */
|
/* ── Bulk selection ────────────────────────────────────────── */
|
||||||
|
|
||||||
/* Toggle button in section header */
|
/* Toggle button in section header */
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
import { t } from './i18n.ts';
|
import { t } from './i18n.ts';
|
||||||
import { showBulkToolbar, hideBulkToolbar, updateBulkToolbar } from './bulk-toolbar.ts';
|
import { showBulkToolbar, hideBulkToolbar, updateBulkToolbar } from './bulk-toolbar.ts';
|
||||||
import { ICON_LIST_CHECKS } from './icons.ts';
|
import { ICON_LIST_CHECKS, ICON_EYE, ICON_EYE_OFF } from './icons.ts';
|
||||||
|
|
||||||
export interface BulkAction {
|
export interface BulkAction {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -52,11 +52,44 @@ export interface CardSectionOpts {
|
|||||||
|
|
||||||
const STORAGE_KEY = 'sections_collapsed';
|
const STORAGE_KEY = 'sections_collapsed';
|
||||||
const ORDER_PREFIX = 'card_order_';
|
const ORDER_PREFIX = 'card_order_';
|
||||||
|
const HIDDEN_KEY = 'hidden_cards';
|
||||||
const DRAG_THRESHOLD = 5;
|
const DRAG_THRESHOLD = 5;
|
||||||
const SCROLL_EDGE = 60;
|
const SCROLL_EDGE = 60;
|
||||||
const SCROLL_SPEED = 12;
|
const SCROLL_SPEED = 12;
|
||||||
const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
|
|
||||||
|
function _getHiddenMap(): Record<string, string[]> {
|
||||||
|
try { return JSON.parse(localStorage.getItem(HIDDEN_KEY) as string) || {}; }
|
||||||
|
catch { return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function _saveHiddenMap(map: Record<string, string[]>): void {
|
||||||
|
localStorage.setItem(HIDDEN_KEY, JSON.stringify(map));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get hidden IDs for a section. */
|
||||||
|
export function getHiddenIds(sectionKey: string): string[] {
|
||||||
|
return _getHiddenMap()[sectionKey] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hide a card in a section. */
|
||||||
|
export function hideCard(sectionKey: string, id: string): void {
|
||||||
|
const map = _getHiddenMap();
|
||||||
|
const ids = map[sectionKey] || [];
|
||||||
|
if (!ids.includes(id)) {
|
||||||
|
map[sectionKey] = [...ids, id];
|
||||||
|
_saveHiddenMap(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unhide a card in a section. */
|
||||||
|
export function unhideCard(sectionKey: string, id: string): void {
|
||||||
|
const map = _getHiddenMap();
|
||||||
|
const ids = map[sectionKey] || [];
|
||||||
|
map[sectionKey] = ids.filter(x => x !== id);
|
||||||
|
_saveHiddenMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
function _getCollapsedMap(): Record<string, boolean> {
|
function _getCollapsedMap(): Record<string, boolean> {
|
||||||
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) as string) || {}; }
|
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) as string) || {}; }
|
||||||
catch { return {}; }
|
catch { return {}; }
|
||||||
@@ -80,6 +113,15 @@ export function renderSkeletonCards(count = 3, gridClass = 'devices-grid') {
|
|||||||
return `<div class="${gridClass}">${html}</div>`;
|
return `<div class="${gridClass}">${html}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Registry of all CardSection instances for global access. */
|
||||||
|
const _sectionRegistry = new Map<string, CardSection>();
|
||||||
|
|
||||||
|
/** Toggle hidden state of a card from any context (e.g. inline onclick). */
|
||||||
|
export function toggleCardHidden(sectionKey: string, id: string): void {
|
||||||
|
const section = _sectionRegistry.get(sectionKey);
|
||||||
|
if (section) section.toggleHidden(id);
|
||||||
|
}
|
||||||
|
|
||||||
export class CardSection {
|
export class CardSection {
|
||||||
|
|
||||||
sectionKey: string;
|
sectionKey: string;
|
||||||
@@ -101,6 +143,7 @@ export class CardSection {
|
|||||||
_escHandler: ((e: KeyboardEvent) => void) | null;
|
_escHandler: ((e: KeyboardEvent) => void) | null;
|
||||||
_pendingReconcile: CardItem[] | null;
|
_pendingReconcile: CardItem[] | null;
|
||||||
_animated: boolean;
|
_animated: boolean;
|
||||||
|
_showHidden: boolean;
|
||||||
|
|
||||||
constructor(sectionKey: string, { titleKey, gridClass, addCardOnclick, keyAttr, headerExtra, collapsible, emptyKey, bulkActions }: CardSectionOpts) {
|
constructor(sectionKey: string, { titleKey, gridClass, addCardOnclick, keyAttr, headerExtra, collapsible, emptyKey, bulkActions }: CardSectionOpts) {
|
||||||
this.sectionKey = sectionKey;
|
this.sectionKey = sectionKey;
|
||||||
@@ -123,6 +166,8 @@ export class CardSection {
|
|||||||
this._escHandler = null;
|
this._escHandler = null;
|
||||||
this._pendingReconcile = null;
|
this._pendingReconcile = null;
|
||||||
this._animated = false;
|
this._animated = false;
|
||||||
|
this._showHidden = false;
|
||||||
|
_sectionRegistry.set(sectionKey, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True if this section's DOM element exists (i.e. not the first render). */
|
/** True if this section's DOM element exists (i.e. not the first render). */
|
||||||
@@ -134,8 +179,18 @@ export class CardSection {
|
|||||||
render(items: CardItem[]) {
|
render(items: CardItem[]) {
|
||||||
this._lastItems = items;
|
this._lastItems = items;
|
||||||
this._dragBound = false; // DOM will be recreated → need to re-init drag
|
this._dragBound = false; // DOM will be recreated → need to re-init drag
|
||||||
const count = items.length;
|
|
||||||
const cardsHtml = items.map(i => i.html).join('');
|
const hiddenIds = new Set(getHiddenIds(this.sectionKey));
|
||||||
|
const visibleItems = this._showHidden ? items : items.filter(i => !hiddenIds.has(i.key));
|
||||||
|
const hiddenCount = items.length - visibleItems.length + (this._showHidden ? 0 : 0);
|
||||||
|
const totalHidden = items.filter(i => hiddenIds.has(i.key)).length;
|
||||||
|
const count = visibleItems.length;
|
||||||
|
const cardsHtml = visibleItems.map(i => {
|
||||||
|
if (hiddenIds.has(i.key)) {
|
||||||
|
return i.html.replace(/class="(card|template-card)/, 'class="$1 cs-card-hidden');
|
||||||
|
}
|
||||||
|
return i.html;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
const isCollapsed = this.collapsible && !!_getCollapsedMap()[this.sectionKey];
|
const isCollapsed = this.collapsible && !!_getCollapsedMap()[this.sectionKey];
|
||||||
const chevronStyle = isCollapsed ? '' : ' style="transform:rotate(90deg)"';
|
const chevronStyle = isCollapsed ? '' : ' style="transform:rotate(90deg)"';
|
||||||
@@ -146,7 +201,9 @@ export class CardSection {
|
|||||||
? `<div class="template-card add-template-card cs-add-card" data-cs-add="${this.sectionKey}" onclick="${this.addCardOnclick}"><div class="add-template-icon">+</div></div>`
|
? `<div class="template-card add-template-card cs-add-card" data-cs-add="${this.sectionKey}" onclick="${this.addCardOnclick}"><div class="add-template-icon">+</div></div>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const emptyState = '';
|
const hiddenToggle = totalHidden > 0
|
||||||
|
? `<button type="button" class="cs-hidden-toggle${this._showHidden ? ' active' : ''}" data-cs-hidden-toggle="${this.sectionKey}" title="${t('section.show_hidden')}">${this._showHidden ? ICON_EYE : ICON_EYE_OFF} <span class="cs-hidden-badge">${totalHidden}</span></button>`
|
||||||
|
: '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="subtab-section${collapsedClass}" data-card-section="${this.sectionKey}">
|
<div class="subtab-section${collapsedClass}" data-card-section="${this.sectionKey}">
|
||||||
@@ -155,6 +212,7 @@ export class CardSection {
|
|||||||
<span class="cs-title" data-i18n="${this.titleKey}">${t(this.titleKey)}</span>
|
<span class="cs-title" data-i18n="${this.titleKey}">${t(this.titleKey)}</span>
|
||||||
<span class="cs-count">${count}</span>
|
<span class="cs-count">${count}</span>
|
||||||
${this.headerExtra ? `<span class="cs-header-extra">${this.headerExtra}</span>` : ''}
|
${this.headerExtra ? `<span class="cs-header-extra">${this.headerExtra}</span>` : ''}
|
||||||
|
${hiddenToggle}
|
||||||
${this.bulkActions ? `<button type="button" class="cs-bulk-toggle" data-cs-bulk="${this.sectionKey}" title="${t('bulk.select')}">${ICON_LIST_CHECKS}</button>` : ''}
|
${this.bulkActions ? `<button type="button" class="cs-bulk-toggle" data-cs-bulk="${this.sectionKey}" title="${t('bulk.select')}">${ICON_LIST_CHECKS}</button>` : ''}
|
||||||
<div class="cs-filter-wrap">
|
<div class="cs-filter-wrap">
|
||||||
<input type="text" class="cs-filter" data-cs-filter="${this.sectionKey}"
|
<input type="text" class="cs-filter" data-cs-filter="${this.sectionKey}"
|
||||||
@@ -163,7 +221,7 @@ export class CardSection {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cs-content ${this.gridClass}" data-cs-content="${this.sectionKey}"${contentDisplay}>
|
<div class="cs-content ${this.gridClass}" data-cs-content="${this.sectionKey}"${contentDisplay}>
|
||||||
${emptyState}${cardsHtml}
|
${cardsHtml}
|
||||||
${addCard}
|
${addCard}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -232,6 +290,20 @@ export class CardSection {
|
|||||||
updateResetVisibility();
|
updateResetVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hidden cards toggle button
|
||||||
|
const hiddenToggleBtn = document.querySelector(`[data-cs-hidden-toggle="${this.sectionKey}"]`);
|
||||||
|
if (hiddenToggleBtn) {
|
||||||
|
hiddenToggleBtn.addEventListener('mousedown', (e) => e.stopPropagation());
|
||||||
|
hiddenToggleBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this._showHidden = !this._showHidden;
|
||||||
|
// Re-render by triggering reconcile with last items
|
||||||
|
if (this._lastItems) this.reconcile(this._lastItems);
|
||||||
|
// Update toggle button visual
|
||||||
|
this._updateHiddenToggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Bulk selection toggle button
|
// Bulk selection toggle button
|
||||||
if (this.bulkActions) {
|
if (this.bulkActions) {
|
||||||
const bulkBtn = document.querySelector(`[data-cs-bulk="${this.sectionKey}"]`);
|
const bulkBtn = document.querySelector(`[data-cs-bulk="${this.sectionKey}"]`);
|
||||||
@@ -282,8 +354,9 @@ export class CardSection {
|
|||||||
// Tag card elements with their source HTML for future reconciliation
|
// Tag card elements with their source HTML for future reconciliation
|
||||||
this._tagCards(content);
|
this._tagCards(content);
|
||||||
|
|
||||||
// Inject drag handles and initialize drag-and-drop
|
// Inject hide buttons and drag handles, initialize drag-and-drop
|
||||||
if (this.keyAttr) {
|
if (this.keyAttr) {
|
||||||
|
this._injectHideButtons(content);
|
||||||
this._injectDragHandles(content);
|
this._injectDragHandles(content);
|
||||||
this._initDrag(content);
|
this._initDrag(content);
|
||||||
}
|
}
|
||||||
@@ -308,15 +381,30 @@ export class CardSection {
|
|||||||
|
|
||||||
this._lastItems = items;
|
this._lastItems = items;
|
||||||
|
|
||||||
|
// Filter hidden items
|
||||||
|
const hiddenIds = new Set(getHiddenIds(this.sectionKey));
|
||||||
|
const visibleItems = this._showHidden ? items : items.filter(i => !hiddenIds.has(i.key));
|
||||||
|
|
||||||
// Update count badge (will be refined by _applyFilter if a filter is active)
|
// Update count badge (will be refined by _applyFilter if a filter is active)
|
||||||
const countEl = document.querySelector(`[data-cs-toggle="${this.sectionKey}"] .cs-count`);
|
const countEl = document.querySelector(`[data-cs-toggle="${this.sectionKey}"] .cs-count`);
|
||||||
if (countEl && !this._filterValue) countEl.textContent = String(items.length);
|
if (countEl && !this._filterValue) countEl.textContent = String(visibleItems.length);
|
||||||
|
|
||||||
|
// Update hidden toggle button
|
||||||
|
this._updateHiddenToggle();
|
||||||
|
|
||||||
// Remove any stale empty-state element from DOM
|
// Remove any stale empty-state element from DOM
|
||||||
const staleEmpty = content.querySelector(`[data-cs-empty="${this.sectionKey}"]`);
|
const staleEmpty = content.querySelector(`[data-cs-empty="${this.sectionKey}"]`);
|
||||||
if (staleEmpty) staleEmpty.remove();
|
if (staleEmpty) staleEmpty.remove();
|
||||||
|
|
||||||
const newMap = new Map(items.map(i => [i.key, i.html]));
|
// Mark hidden cards with CSS class in HTML
|
||||||
|
const processedItems = visibleItems.map(i => {
|
||||||
|
if (hiddenIds.has(i.key)) {
|
||||||
|
return { key: i.key, html: i.html.replace(/class="(card|template-card)/, 'class="$1 cs-card-hidden') };
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
});
|
||||||
|
|
||||||
|
const newMap = new Map(processedItems.map(i => [i.key, i.html]));
|
||||||
const addCard = content.querySelector('.cs-add-card');
|
const addCard = content.querySelector('.cs-add-card');
|
||||||
const added = new Set<string>();
|
const added = new Set<string>();
|
||||||
const replaced = new Set<string>();
|
const replaced = new Set<string>();
|
||||||
@@ -348,7 +436,7 @@ export class CardSection {
|
|||||||
const existingKeys = new Set([...content.querySelectorAll(`[${this.keyAttr}]`)].map(
|
const existingKeys = new Set([...content.querySelectorAll(`[${this.keyAttr}]`)].map(
|
||||||
el => el.getAttribute(this.keyAttr)
|
el => el.getAttribute(this.keyAttr)
|
||||||
));
|
));
|
||||||
for (const { key, html } of items) {
|
for (const { key, html } of processedItems) {
|
||||||
if (!existingKeys.has(key)) {
|
if (!existingKeys.has(key)) {
|
||||||
const tmp = document.createElement('div');
|
const tmp = document.createElement('div');
|
||||||
tmp.innerHTML = html;
|
tmp.innerHTML = html;
|
||||||
@@ -375,8 +463,9 @@ export class CardSection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-inject drag handles on new/replaced cards
|
// Re-inject hide buttons and drag handles on new/replaced cards
|
||||||
if (this.keyAttr && (added.size > 0 || replaced.size > 0)) {
|
if (this.keyAttr && (added.size > 0 || replaced.size > 0)) {
|
||||||
|
this._injectHideButtons(content);
|
||||||
this._injectDragHandles(content);
|
this._injectDragHandles(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,8 +789,79 @@ export class CardSection {
|
|||||||
if (countEl) countEl.textContent = `${visible}/${total}`;
|
if (countEl) countEl.textContent = `${visible}/${total}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Update the hidden toggle button's visual state. */
|
||||||
|
_updateHiddenToggle() {
|
||||||
|
const btn = document.querySelector(`[data-cs-hidden-toggle="${this.sectionKey}"]`) as HTMLElement | null;
|
||||||
|
const totalHidden = this._lastItems ? this._lastItems.filter(i => getHiddenIds(this.sectionKey).includes(i.key)).length : 0;
|
||||||
|
|
||||||
|
if (totalHidden === 0 && btn) {
|
||||||
|
btn.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalHidden > 0 && !btn) {
|
||||||
|
// Insert toggle button into header
|
||||||
|
const bulkBtn = document.querySelector(`[data-cs-bulk="${this.sectionKey}"]`);
|
||||||
|
const filterWrap = document.querySelector(`[data-cs-toggle="${this.sectionKey}"] .cs-filter-wrap`);
|
||||||
|
const insertBefore = bulkBtn || filterWrap;
|
||||||
|
if (insertBefore) {
|
||||||
|
const newBtn = document.createElement('button');
|
||||||
|
newBtn.type = 'button';
|
||||||
|
newBtn.className = `cs-hidden-toggle${this._showHidden ? ' active' : ''}`;
|
||||||
|
newBtn.dataset.csHiddenToggle = this.sectionKey;
|
||||||
|
newBtn.title = t('section.show_hidden');
|
||||||
|
newBtn.innerHTML = `${this._showHidden ? ICON_EYE : ICON_EYE_OFF} <span class="cs-hidden-badge">${totalHidden}</span>`;
|
||||||
|
newBtn.addEventListener('mousedown', (e) => e.stopPropagation());
|
||||||
|
newBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this._showHidden = !this._showHidden;
|
||||||
|
if (this._lastItems) this.reconcile(this._lastItems);
|
||||||
|
this._updateHiddenToggle();
|
||||||
|
});
|
||||||
|
insertBefore.parentNode!.insertBefore(newBtn, insertBefore);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.toggle('active', this._showHidden);
|
||||||
|
btn.innerHTML = `${this._showHidden ? ICON_EYE : ICON_EYE_OFF} <span class="cs-hidden-badge">${totalHidden}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toggle hidden state for a card and re-render. */
|
||||||
|
toggleHidden(id: string) {
|
||||||
|
const hidden = getHiddenIds(this.sectionKey);
|
||||||
|
if (hidden.includes(id)) {
|
||||||
|
unhideCard(this.sectionKey, id);
|
||||||
|
} else {
|
||||||
|
hideCard(this.sectionKey, id);
|
||||||
|
}
|
||||||
|
if (this._lastItems) this.reconcile(this._lastItems);
|
||||||
|
}
|
||||||
|
|
||||||
// ── drag-and-drop reordering ──
|
// ── drag-and-drop reordering ──
|
||||||
|
|
||||||
|
_injectHideButtons(content: HTMLElement) {
|
||||||
|
if (!this.keyAttr) return;
|
||||||
|
content.querySelectorAll(`[${this.keyAttr}]`).forEach(card => {
|
||||||
|
if (card.querySelector('.card-hide-btn')) return;
|
||||||
|
const topActions = card.querySelector('.card-top-actions');
|
||||||
|
if (!topActions) return;
|
||||||
|
const key = card.getAttribute(this.keyAttr)!;
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'card-hide-btn';
|
||||||
|
btn.title = 'Hide';
|
||||||
|
btn.innerHTML = ICON_EYE_OFF;
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.toggleHidden(key);
|
||||||
|
});
|
||||||
|
// Insert as first child so order is [hide] [power] [trash]
|
||||||
|
topActions.insertBefore(btn, topActions.firstChild);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_injectDragHandles(content: HTMLElement) {
|
_injectDragHandles(content: HTMLElement) {
|
||||||
const cards = content.querySelectorAll(`[${this.keyAttr}]`);
|
const cards = content.querySelectorAll(`[${this.keyAttr}]`);
|
||||||
cards.forEach(card => {
|
cards.forEach(card => {
|
||||||
|
|||||||
@@ -158,12 +158,12 @@ export function createDeviceCard(device: Device & { state?: any }) {
|
|||||||
<div class="card-title" title="${escapeHtml(device.name || device.id)}">
|
<div class="card-title" title="${escapeHtml(device.name || device.id)}">
|
||||||
<span class="health-dot ${healthClass}" title="${healthTitle}" role="status" aria-label="${healthTitle}"></span>
|
<span class="health-dot ${healthClass}" title="${healthTitle}" role="status" aria-label="${healthTitle}"></span>
|
||||||
<span class="card-title-text">${device.name || device.id}</span>
|
<span class="card-title-text">${device.name || device.id}</span>
|
||||||
${device.url && device.url.startsWith('http') ? `<a class="device-url-badge" href="${device.url}" target="_blank" rel="noopener" title="${t('device.button.webui')}"><span class="device-url-text">${escapeHtml(device.url.replace(/^https?:\/\//, ''))}</span><span class="device-url-icon">${ICON_WEB}</span></a>` : (device.url && !device.url.startsWith('mock://') && !device.url.startsWith('ws://') && !device.url.startsWith('openrgb://') && !device.url.startsWith('http') ? `<span class="device-url-badge"><span class="device-url-text">${escapeHtml(device.url)}</span></span>` : '')}
|
|
||||||
${healthLabel}
|
${healthLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-subtitle">
|
<div class="card-subtitle">
|
||||||
<span class="card-meta device-type-badge">${(device.device_type || 'wled').toUpperCase()}</span>
|
<span class="card-meta device-type-badge">${(device.device_type || 'wled').toUpperCase()}</span>
|
||||||
|
${device.url && device.url.startsWith('http') ? `<a class="device-url-badge" href="${device.url}" target="_blank" rel="noopener" title="${t('device.button.webui')}"><span class="device-url-text">${escapeHtml(device.url.replace(/^https?:\/\//, ''))}</span><span class="device-url-icon">${ICON_WEB}</span></a>` : (device.url && !device.url.startsWith('mock://') && !device.url.startsWith('ws://') && !device.url.startsWith('openrgb://') && !device.url.startsWith('http') ? `<span class="device-url-badge"><span class="device-url-text">${escapeHtml(device.url)}</span></span>` : '')}
|
||||||
${openrgbZones.length
|
${openrgbZones.length
|
||||||
? openrgbZones.map((z: any) => `<span class="card-meta zone-badge" data-zone-name="${escapeHtml(z)}">${ICON_LED} ${escapeHtml(z)}</span>`).join('')
|
? openrgbZones.map((z: any) => `<span class="card-meta zone-badge" data-zone-name="${escapeHtml(z)}">${ICON_LED} ${escapeHtml(z)}</span>`).join('')
|
||||||
: (ledCount ? `<span class="card-meta" title="${t('device.led_count')}">${ICON_LED} ${ledCount}</span>` : '')}
|
: (ledCount ? `<span class="card-meta" title="${t('device.led_count')}">${ICON_LED} ${ledCount}</span>` : '')}
|
||||||
@@ -200,7 +200,7 @@ export async function turnOffDevice(deviceId: any) {
|
|||||||
try {
|
try {
|
||||||
const setResp = await fetchWithAuth(`/devices/${deviceId}/power`, {
|
const setResp = await fetchWithAuth(`/devices/${deviceId}/power`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ on: false })
|
body: JSON.stringify({ power: false })
|
||||||
});
|
});
|
||||||
if (setResp.ok) {
|
if (setResp.ok) {
|
||||||
showToast(t('device.power.off_success'), 'success');
|
showToast(t('device.power.off_success'), 'success');
|
||||||
|
|||||||
Reference in New Issue
Block a user