mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-07-06 01:41:27 +02:00
fix: resolve mobile UI issues on members page and node detail
- Replace nested <a> tags in member cards with @click handlers to fix invalid HTML causing missing badge styles and double-navigation race conditions (NS_BINDING_ABORT) on mobile - Add e.preventDefault() to @click handlers to prevent parent <a> activation - Bump mobile dropdown z-index from z-[1] to z-50 so menu items render above QR code wrapper (z-20) on node detail hero section - Blur active element after SPA navigation to dismiss DaisyUI :focus-within dropdown state - Fix dashboard channel messages to show short date+time format - Add overflow-x-auto to custom pages and node detail panels for mobile overflow - Fix tag editor: hide type column on mobile, truncate key/value cells, show key in modal title instead of disabled input field - Fix delete tag dialog using innerHTML for <strong> in i18n strings - Fix tag add form not resetting after successful submit - Add break-word and overflow-x-auto to prose tables in app.css
This commit is contained in:
@@ -256,10 +256,17 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.prose {
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.prose table {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
border-collapse: collapse;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.prose th,
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function render(container, params, router) {
|
||||
litRender(html`
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body prose prose-lg max-w-none">
|
||||
<div class="card-body prose prose-lg max-w-none overflow-x-auto">
|
||||
${unsafeHTML(page.content_html)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@ function formatTimeOnly(isoString) {
|
||||
|
||||
function formatTimeShort(isoString) {
|
||||
return formatDateTime(isoString, {
|
||||
month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { apiGet } from '../api.js';
|
||||
import { html, litRender, nothing, t, errorAlert, getConfig } from '../components.js';
|
||||
import { iconAntenna, iconUsers } from '../icons.js';
|
||||
|
||||
function renderProfileTile(profile) {
|
||||
function renderProfileTile(profile, router) {
|
||||
const callsignBadge = profile.callsign
|
||||
? html`<span class="badge badge-neutral badge-sm">${profile.callsign}</span>`
|
||||
: nothing;
|
||||
@@ -20,7 +20,12 @@ function renderProfileTile(profile) {
|
||||
const nodeBadges = profile.adopted_nodes && profile.adopted_nodes.length > 0
|
||||
? html`<div class="flex flex-wrap gap-1 mt-2">${profile.adopted_nodes.map(node => {
|
||||
const label = node.name || node.public_key.slice(0, 12) + '...';
|
||||
return html`<a href="/nodes/${node.public_key}" class="badge badge-outline badge-sm hover:badge-ghost transition-colors">${label}</a>`;
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
router.navigate('/nodes/' + node.public_key);
|
||||
};
|
||||
return html`<span class="badge badge-secondary badge-sm cursor-pointer" @click=${handleClick}>${label}</span>`;
|
||||
})}</div>`
|
||||
: nothing;
|
||||
|
||||
@@ -29,7 +34,7 @@ function renderProfileTile(profile) {
|
||||
: nothing;
|
||||
|
||||
const urlLink = profile.url
|
||||
? html`<a href="${profile.url}" target="_blank" rel="noopener noreferrer" class="link link-primary text-xs mt-1 inline-block truncate">${profile.url}</a>`
|
||||
? html`<span class="link link-accent text-xs mt-1 inline-block truncate cursor-pointer" @click=${(e) => { e.preventDefault(); e.stopPropagation(); window.open(profile.url, '_blank', 'noopener,noreferrer'); }}>${profile.url}</span>`
|
||||
: nothing;
|
||||
|
||||
return html`<a href="/profile/${profile.id}" class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow">
|
||||
@@ -47,14 +52,14 @@ function renderProfileTile(profile) {
|
||||
</a>`;
|
||||
}
|
||||
|
||||
function renderGroup(title, profiles, icon) {
|
||||
function renderGroup(title, profiles, icon, router) {
|
||||
if (profiles.length === 0) return nothing;
|
||||
return html`
|
||||
<h2 class="text-2xl font-bold mt-8 mb-4 flex items-center gap-2">
|
||||
${icon}${title}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
${profiles.sort((a, b) => (a.name || '').localeCompare(b.name || '')).map(renderProfileTile)}
|
||||
${profiles.sort((a, b) => (a.name || '').localeCompare(b.name || '')).map(p => renderProfileTile(p, router))}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -92,8 +97,8 @@ export async function render(container, params, router) {
|
||||
<span class="badge badge-lg">${t('common.count_entity', { count: profiles.length, entity: t('entities.members').toLowerCase() })}</span>
|
||||
</div>
|
||||
|
||||
${renderGroup(t('members_page.operators'), operators, html`<span class="text-primary">${iconAntenna('h-6 w-6')}</span>`)}
|
||||
${renderGroup(t('members_page.members'), members, html`<span class="text-secondary">${iconUsers('h-6 w-6')}</span>`)}
|
||||
${renderGroup(t('members_page.operators'), operators, html`<span class="text-primary">${iconAntenna('h-6 w-6')}</span>`, router)}
|
||||
${renderGroup(t('members_page.members'), members, html`<span class="text-secondary">${iconUsers('h-6 w-6')}</span>`, router)}
|
||||
`, container);
|
||||
|
||||
} catch (e) {
|
||||
|
||||
@@ -44,12 +44,9 @@ function renderEditTagModal() {
|
||||
return html`
|
||||
<dialog id="tagEditModal" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg">${t('common.edit_entity', { entity: t('entities.tag') })}</h3>
|
||||
<h3 class="font-bold text-lg">${t('common.edit_entity', { entity: t('entities.tag') })}: <span id="tagEditKeyDisplay" class="font-mono text-base font-normal"></span></h3>
|
||||
<form id="tag-edit-form" class="py-4">
|
||||
<div class="form-control mb-4">
|
||||
<label class="label"><span class="label-text">${t('common.key')}</span></label>
|
||||
<input type="text" id="tagEditKey" class="input input-bordered" disabled>
|
||||
</div>
|
||||
<input type="hidden" id="tagEditKey">
|
||||
<div class="form-control mb-4">
|
||||
<label class="label"><span class="label-text">${t('common.value')}</span></label>
|
||||
<input type="text" id="tagEditValue" class="input input-bordered">
|
||||
@@ -154,11 +151,11 @@ export async function render(container, params, router) {
|
||||
? html`<span class="opacity-50">-</span>`
|
||||
: recvName
|
||||
? html`<a href="/nodes/${adv.observed_by}" class="link link-hover">
|
||||
<div class="font-medium text-sm">${recvName}</div>
|
||||
<div class="text-xs font-mono opacity-70">${adv.observed_by.slice(0, 16)}...</div>
|
||||
<div class="font-medium text-sm truncate max-w-[8rem]">${recvName}</div>
|
||||
<div class="text-xs font-mono opacity-70 hidden sm:block">${adv.observed_by.slice(0, 16)}...</div>
|
||||
</a>`
|
||||
: html`<a href="/nodes/${adv.observed_by}" class="link link-hover">
|
||||
<span class="font-mono text-xs">${adv.observed_by.slice(0, 16)}...</span>
|
||||
<span class="font-mono text-xs">${adv.observed_by.slice(0, 12)}...</span>
|
||||
</a>`;
|
||||
return html`<tr>
|
||||
<td class="text-xs whitespace-nowrap">${formatDateTime(adv.received_at)}</td>
|
||||
@@ -184,15 +181,15 @@ export async function render(container, params, router) {
|
||||
<tr>
|
||||
<th>${t('common.key')}</th>
|
||||
<th>${t('common.value')}</th>
|
||||
<th>${t('common.type')}</th>
|
||||
<th class="w-20">${t('common.actions')}</th>
|
||||
<th class="hidden sm:table-cell">${t('common.type')}</th>
|
||||
<th class="w-16">${t('common.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${tags.map(tag => html`<tr>
|
||||
<td class="font-mono">${tag.key}</td>
|
||||
<td>${tag.value || ''}</td>
|
||||
<td class="opacity-70">${tag.value_type || 'string'}</td>
|
||||
<td class="font-mono min-w-0 truncate max-w-[8rem]">${tag.key}</td>
|
||||
<td class="min-w-0 truncate max-w-[12rem]">${tag.value || ''}</td>
|
||||
<td class="hidden sm:table-cell opacity-70">${tag.value_type || 'string'}</td>
|
||||
<td>
|
||||
<div class="flex gap-1">
|
||||
<button class="btn btn-xs btn-ghost tag-edit-btn" data-key=${tag.key} data-value=${tag.value || ''} data-type=${tag.value_type || 'string'}>${iconEdit('h-4 w-4')}</button>
|
||||
@@ -454,6 +451,7 @@ ${canEditTags ? renderEditTagModal() : nothing}`, container);
|
||||
try {
|
||||
await apiPost('/api/v1/nodes/' + node.public_key + '/tags', { key, value, value_type: valueType });
|
||||
showFlash('success', t('common.entity_added_success', { entity: t('entities.tag') }));
|
||||
addForm.reset();
|
||||
router.navigate('/nodes/' + node.public_key, true);
|
||||
} catch (err) {
|
||||
showFlash('error', err.message);
|
||||
@@ -466,11 +464,13 @@ ${canEditTags ? renderEditTagModal() : nothing}`, container);
|
||||
btn.addEventListener('click', () => {
|
||||
const modal = container.querySelector('#tagEditModal');
|
||||
const keyInput = container.querySelector('#tagEditKey');
|
||||
const keyDisplay = container.querySelector('#tagEditKeyDisplay');
|
||||
const valueInput = container.querySelector('#tagEditValue');
|
||||
const typeSelect = container.querySelector('#tagEditType');
|
||||
const errorLabel = container.querySelector('#tagEditError');
|
||||
|
||||
keyInput.value = btn.dataset.key;
|
||||
if (keyDisplay) keyDisplay.textContent = btn.dataset.key;
|
||||
valueInput.value = btn.dataset.value;
|
||||
typeSelect.value = btn.dataset.type;
|
||||
if (errorLabel) errorLabel.innerHTML = '';
|
||||
@@ -519,7 +519,7 @@ ${canEditTags ? renderEditTagModal() : nothing}`, container);
|
||||
btn.addEventListener('click', () => {
|
||||
const modal = container.querySelector('#tagDeleteModal');
|
||||
const msg = container.querySelector('#tag-delete-msg');
|
||||
msg.textContent = t('common.delete_entity_confirm', { entity: t('entities.tag'), name: btn.dataset.key });
|
||||
msg.innerHTML = t('common.delete_entity_confirm', { entity: t('entities.tag'), name: btn.dataset.key });
|
||||
modal._tagKey = btn.dataset.key;
|
||||
modal.showModal();
|
||||
}, { signal });
|
||||
|
||||
@@ -122,6 +122,9 @@ export class Router {
|
||||
if (loader) loader.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Reset focus to dismiss any open dropdown after navigation
|
||||
document.activeElement?.blur();
|
||||
|
||||
// Scroll to top on navigation
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" /></svg>
|
||||
</div>
|
||||
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-50 p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<li><a href="/" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg> {{ t('entities.home') }}</a></li>
|
||||
{% if features.dashboard %}
|
||||
<li><a href="/dashboard" data-nav-link><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 nav-icon-dashboard" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg> {{ t('entities.dashboard') }}</a></li>
|
||||
|
||||
Reference in New Issue
Block a user