Add device preview toggle (mobile/tablet/desktop) and open-in-new-tab

This commit is contained in:
Walter Jekat 2025-12-05 12:17:39 +01:00
parent e9ebb99204
commit 9b7eb16ac4
1 changed files with 101 additions and 7 deletions

View File

@ -112,6 +112,7 @@
color: #9ca3af; color: #9ca3af;
margin-left: auto; margin-left: auto;
} }
.preview { .preview {
width: 45%; width: 45%;
min-width: 320px; min-width: 320px;
@ -125,16 +126,75 @@
font-size: 0.85rem; font-size: 0.85rem;
color: #9ca3af; color: #9ca3af;
background: #020617; background: #020617;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.preview-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.preview-controls {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.8rem;
}
.device-btn {
padding: 0.15rem 0.5rem;
border-radius: 999px;
border: 1px solid #4b5563;
background: #020617;
color: #e5e7eb;
cursor: pointer;
font-size: 0.75rem;
}
.device-btn.active {
background: #4b5563;
color: #f9fafb;
}
.preview-open {
padding: 0.15rem 0.45rem;
border-radius: 999px;
border: 1px solid #4b5563;
text-decoration: none;
color: #e5e7eb;
font-size: 0.75rem;
}
.preview-inner {
flex: 1;
display: flex;
justify-content: center;
align-items: stretch;
padding: 0.5rem;
box-sizing: border-box;
background: #000;
overflow: auto;
} }
.preview iframe { .preview iframe {
flex: 1;
width: 100%;
border: none; border: none;
height: 100%;
max-height: 100%;
background: #000; background: #000;
} }
/* Device width presets for iframe */
.device-mobile #preview-frame {
width: 390px; /* typical phone */
}
.device-tablet #preview-frame {
width: 820px; /* typical tablet */
}
.device-desktop #preview-frame {
width: 1200px; /* typical desktop layout */
max-width: 100%;
}
</style> </style>
</head> </head>
<body> <body class="device-desktop">
<aside class="sidebar"> <aside class="sidebar">
<h1>Pages</h1> <h1>Pages</h1>
<ul class="page-list" id="page-list"></ul> <ul class="page-list" id="page-list"></ul>
@ -151,9 +211,19 @@
</section> </section>
<section class="preview"> <section class="preview">
<div class="preview-header"> <div class="preview-header">
<span class="preview-title">
Preview: <span id="preview-label"></span> Preview: <span id="preview-label"></span>
</span>
<div class="preview-controls">
<button class="device-btn" data-mode="mobile">📱</button>
<button class="device-btn" data-mode="tablet">📱📱</button>
<button class="device-btn active" data-mode="desktop">🖥</button>
<a id="open-preview" class="preview-open" href="#" target="_blank" rel="noopener noreferrer"></a>
</div> </div>
</div>
<div class="preview-inner">
<iframe id="preview-frame" src="about:blank"></iframe> <iframe id="preview-frame" src="about:blank"></iframe>
</div>
</section> </section>
</main> </main>
@ -168,9 +238,26 @@
const jsonEditorEl = document.getElementById('json-editor'); const jsonEditorEl = document.getElementById('json-editor');
const saveBtnEl = document.getElementById('save-btn'); const saveBtnEl = document.getElementById('save-btn');
const statusEl = document.getElementById('status'); const statusEl = document.getElementById('status');
const deviceButtons = document.querySelectorAll('.device-btn');
const openPreviewEl = document.getElementById('open-preview');
let currentSlug = null; let currentSlug = null;
function setDeviceMode(mode) {
document.body.classList.remove('device-mobile', 'device-tablet', 'device-desktop');
document.body.classList.add('device-' + mode);
deviceButtons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
}
deviceButtons.forEach(btn => {
btn.addEventListener('click', () => {
const mode = btn.dataset.mode;
setDeviceMode(mode);
});
});
async function loadPages() { async function loadPages() {
const res = await fetch(API_BASE + '/api/pages'); const res = await fetch(API_BASE + '/api/pages');
const pages = await res.json(); const pages = await res.json();
@ -205,7 +292,9 @@
saveBtnEl.disabled = false; saveBtnEl.disabled = false;
statusEl.textContent = 'Loaded'; statusEl.textContent = 'Loaded';
previewFrameEl.src = API_BASE + '/preview/' + encodeURIComponent(slug) + '/'; const url = API_BASE + '/preview/' + encodeURIComponent(slug) + '/';
previewFrameEl.src = url;
openPreviewEl.href = url;
} }
async function saveCurrent() { async function saveCurrent() {
@ -233,7 +322,9 @@
saveBtnEl.disabled = false; saveBtnEl.disabled = false;
// refresh preview // refresh preview
previewFrameEl.src = API_BASE + '/preview/' + encodeURIComponent(currentSlug) + '/'; const url = API_BASE + '/preview/' + encodeURIComponent(currentSlug) + '/';
previewFrameEl.src = url;
openPreviewEl.href = url;
} }
jsonEditorEl.addEventListener('input', () => { jsonEditorEl.addEventListener('input', () => {
@ -243,6 +334,9 @@
saveBtnEl.addEventListener('click', saveCurrent); saveBtnEl.addEventListener('click', saveCurrent);
// Default device mode = desktop
setDeviceMode('desktop');
loadPages().catch(err => { loadPages().catch(err => {
console.error(err); console.error(err);
statusEl.textContent = 'Error loading pages'; statusEl.textContent = 'Error loading pages';