Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41ddecf5a7 | ||
|
|
b06c37affa | ||
|
|
b4912f5879 |
@@ -748,6 +748,25 @@ function elementById(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"];
|
const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"];
|
||||||
|
function anchorMenu(button, menu) {
|
||||||
|
const rect = button.getBoundingClientRect();
|
||||||
|
if (menu.parentElement !== document.body) document.body.appendChild(menu);
|
||||||
|
menu.style.position = "fixed";
|
||||||
|
menu.style.zIndex = "9700";
|
||||||
|
menu.style.top = "0px";
|
||||||
|
menu.style.bottom = "auto";
|
||||||
|
menu.style.right = "auto";
|
||||||
|
const width = menu.offsetWidth || 200;
|
||||||
|
const height = menu.offsetHeight || 0;
|
||||||
|
menu.style.left = `${String(Math.round(Math.min(Math.max(8, rect.right - width), window.innerWidth - width - 8)))}px`;
|
||||||
|
const below = rect.bottom + 8;
|
||||||
|
if (below + height <= window.innerHeight - 8) {
|
||||||
|
menu.style.top = `${String(Math.round(below))}px`;
|
||||||
|
} else {
|
||||||
|
menu.style.top = "auto";
|
||||||
|
menu.style.bottom = `${String(Math.round(window.innerHeight - rect.top + 8))}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
function installTopBarOverflow() {
|
function installTopBarOverflow() {
|
||||||
const actions = document.querySelector(".top-bar-actions");
|
const actions = document.querySelector(".top-bar-actions");
|
||||||
if (!actions || document.getElementById("top-bar-more")) return;
|
if (!actions || document.getElementById("top-bar-more")) return;
|
||||||
@@ -777,9 +796,14 @@ function elementById(id) {
|
|||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
const open = menu.classList.toggle("is-open");
|
const open = menu.classList.toggle("is-open");
|
||||||
button.setAttribute("aria-expanded", String(open));
|
button.setAttribute("aria-expanded", String(open));
|
||||||
|
if (open) anchorMenu(button, menu);
|
||||||
});
|
});
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
if (!(event.target instanceof Node) || !wrap.contains(event.target)) closeMenu();
|
if (!(event.target instanceof Node)) {
|
||||||
|
closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!wrap.contains(event.target) && !menu.contains(event.target)) closeMenu();
|
||||||
});
|
});
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Escape") closeMenu();
|
if (event.key === "Escape") closeMenu();
|
||||||
@@ -819,7 +843,8 @@ function elementById(id) {
|
|||||||
more.id = "mobile-more-btn";
|
more.id = "mobile-more-btn";
|
||||||
more.className = "tab mobile-more-btn";
|
more.className = "tab mobile-more-btn";
|
||||||
more.type = "button";
|
more.type = "button";
|
||||||
more.innerHTML = '<span class="tab-more-icon" aria-hidden="true">•••</span><span class="tab-label">More</span>';
|
more.innerHTML = '<span class="tab-more-icon" aria-hidden="true">•••</span><span class="tab-label">Tools</span>';
|
||||||
|
more.setAttribute("aria-label", "Tools and settings");
|
||||||
more.setAttribute("aria-haspopup", "menu");
|
more.setAttribute("aria-haspopup", "menu");
|
||||||
more.setAttribute("aria-expanded", "false");
|
more.setAttribute("aria-expanded", "false");
|
||||||
const menu = document.createElement("div");
|
const menu = document.createElement("div");
|
||||||
@@ -851,7 +876,10 @@ function elementById(id) {
|
|||||||
more.addEventListener("click", () => {
|
more.addEventListener("click", () => {
|
||||||
const open = menu.classList.toggle("is-open");
|
const open = menu.classList.toggle("is-open");
|
||||||
more.setAttribute("aria-expanded", String(open));
|
more.setAttribute("aria-expanded", String(open));
|
||||||
if (open) menu.querySelector('[role="menuitem"]')?.focus();
|
if (open) {
|
||||||
|
anchorMenu(more, menu);
|
||||||
|
menu.querySelector('[role="menuitem"]')?.focus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
if (!(event.target instanceof Node) || !menu.contains(event.target) && !more.contains(event.target)) closeMore();
|
if (!(event.target instanceof Node) || !menu.contains(event.target) && !more.contains(event.target)) closeMore();
|
||||||
|
|||||||
@@ -1531,31 +1531,17 @@ small { color: var(--text-muted); }
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
/* Scroll rather than wrap: eight labels wrapped into a ragged block (four
|
/* Never wraps and never scrolls: the occasional destinations live behind
|
||||||
* rows at 1100px) and made the header's height depend on the viewport. */
|
* More, so four tabs plus More always fit. overflow-x here would clip the
|
||||||
|
* dropdown this element anchors. */
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
overflow-x: auto;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
/* Fade the trailing edge so a tab scrolled out of view reads as "there is more"
|
|
||||||
* rather than as a label cut mid-word. A mask rather than a colour-matched
|
|
||||||
* cover gradient: the card is transparent, so a cover would have to track the
|
|
||||||
* page background across both themes and all nine styles. Only the trailing
|
|
||||||
* edge is faded, so the first tab stays crisp when the strip is at rest. */
|
|
||||||
.tab-bar-nav {
|
|
||||||
-webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent 100%);
|
|
||||||
mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent 100%);
|
|
||||||
}
|
|
||||||
.tab-bar-nav::-webkit-scrollbar { height: 3px; }
|
|
||||||
.tab-bar-nav::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 3px;
|
|
||||||
background: color-mix(in srgb, var(--border-light) 70%, transparent);
|
|
||||||
}
|
}
|
||||||
.tab-bar-nav .tab { flex: 0 0 auto; }
|
.tab-bar-nav .tab { flex: 0 0 auto; }
|
||||||
/* Icons before scrolling: every tab already carries one, and four icons plus
|
/* Icons before scrolling: every tab already carries one, and four icons plus
|
||||||
* More always fit, so the strip never has to hide a destination. */
|
* More always fit, so the strip never has to hide a destination. */
|
||||||
@media (max-width: 1180px) and (min-width: 701px) {
|
@media (max-width: 1180px) and (min-width: 701px) {
|
||||||
.tab-bar-nav .tab .tab-label { display: none; }
|
.tab-bar-nav .tab .tab-label { display: none; }
|
||||||
|
.tab-bar-nav .tab .tab-icon, .tab-bar-nav .tab .tab-more-icon { display: block; }
|
||||||
.tab-bar-nav .tab { padding: 0.5rem 0.6rem; }
|
.tab-bar-nav .tab { padding: 0.5rem 0.6rem; }
|
||||||
}
|
}
|
||||||
.tab {
|
.tab {
|
||||||
@@ -1579,6 +1565,7 @@ small { color: var(--text-muted); }
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.tab-label { display: block; }
|
.tab-label { display: block; }
|
||||||
|
.tab-more-icon { display: none; }
|
||||||
.about-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; }
|
.about-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; }
|
||||||
.about-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0; overflow: hidden; }
|
.about-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0; overflow: hidden; }
|
||||||
.about-card-title { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0.75rem; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-heading); border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--card-bg) 50%, var(--bg)); }
|
.about-card-title { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0.75rem; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-heading); border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--card-bg) 50%, var(--bg)); }
|
||||||
@@ -3292,7 +3279,7 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
box-shadow: inset 0 1px 0 color-mix(in srgb, #ffffff 8%, transparent);
|
box-shadow: inset 0 1px 0 color-mix(in srgb, #ffffff 8%, transparent);
|
||||||
}
|
}
|
||||||
.tab-icon { display: block; }
|
.tab-icon, .tab-more-icon { display: block; }
|
||||||
/* Shorten long tab labels to keep bottom nav compact */
|
/* Shorten long tab labels to keep bottom nav compact */
|
||||||
.tab[data-tab="bookmarks"] .tab-label { font-size: 0.6rem; }
|
.tab[data-tab="bookmarks"] .tab-label { font-size: 0.6rem; }
|
||||||
.tab[data-tab="digital-modes"] .tab-label { font-size: 0.6rem; }
|
.tab[data-tab="digital-modes"] .tab-label { font-size: 0.6rem; }
|
||||||
|
|||||||
@@ -326,6 +326,39 @@ function elementById<T extends HTMLElement>(id: string): T {
|
|||||||
// Audio, record and the rig picker are the operating controls and stay.
|
// Audio, record and the rig picker are the operating controls and stay.
|
||||||
const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"];
|
const overflowOrder = [".operator-layout-picker", ".header-style-pick", "#theme-toggle"];
|
||||||
|
|
||||||
|
// Anchored at paint time in fixed coordinates. An absolutely positioned
|
||||||
|
// dropdown is clipped by any scrolling ancestor and trapped inside the
|
||||||
|
// stacking context its ancestors create, so it can end up squashed inside
|
||||||
|
// the bar or painted underneath page content. Fixed positioning answers to
|
||||||
|
// the viewport instead, and the menu flips above its button near the bottom
|
||||||
|
// edge.
|
||||||
|
function anchorMenu(button: HTMLElement, menu: HTMLElement) {
|
||||||
|
const rect = button.getBoundingClientRect();
|
||||||
|
// Reparent to the body. Fixed positioning escapes clipping but not
|
||||||
|
// stacking: the header sets `z-index: 2`, which makes it a stacking
|
||||||
|
// context, so any z-index the menu carries is confined below every panel
|
||||||
|
// that paints above level 2 — the spectrum overlays reach 9600. Only
|
||||||
|
// leaving that subtree lets the menu sit above the page.
|
||||||
|
if (menu.parentElement !== document.body) document.body.appendChild(menu);
|
||||||
|
menu.style.position = "fixed";
|
||||||
|
menu.style.zIndex = "9700";
|
||||||
|
menu.style.top = "0px";
|
||||||
|
menu.style.bottom = "auto";
|
||||||
|
// The stylesheet pins these menus to the right of their anchor; leaving
|
||||||
|
// that in place while setting a left would stretch them across the bar.
|
||||||
|
menu.style.right = "auto";
|
||||||
|
const width = menu.offsetWidth || 200;
|
||||||
|
const height = menu.offsetHeight || 0;
|
||||||
|
menu.style.left = `${String(Math.round(Math.min(Math.max(8, rect.right - width), window.innerWidth - width - 8)))}px`;
|
||||||
|
const below = rect.bottom + 8;
|
||||||
|
if (below + height <= window.innerHeight - 8) {
|
||||||
|
menu.style.top = `${String(Math.round(below))}px`;
|
||||||
|
} else {
|
||||||
|
menu.style.top = "auto";
|
||||||
|
menu.style.bottom = `${String(Math.round(window.innerHeight - rect.top + 8))}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function installTopBarOverflow() {
|
function installTopBarOverflow() {
|
||||||
const actions = document.querySelector<HTMLElement>(".top-bar-actions");
|
const actions = document.querySelector<HTMLElement>(".top-bar-actions");
|
||||||
if (!actions || document.getElementById("top-bar-more")) return;
|
if (!actions || document.getElementById("top-bar-more")) return;
|
||||||
@@ -356,9 +389,11 @@ function elementById<T extends HTMLElement>(id: string): T {
|
|||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
const open = menu.classList.toggle("is-open");
|
const open = menu.classList.toggle("is-open");
|
||||||
button.setAttribute("aria-expanded", String(open));
|
button.setAttribute("aria-expanded", String(open));
|
||||||
|
if (open) anchorMenu(button, menu);
|
||||||
});
|
});
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
if (!(event.target instanceof Node) || !wrap.contains(event.target)) closeMenu();
|
if (!(event.target instanceof Node)) { closeMenu(); return; }
|
||||||
|
if (!wrap.contains(event.target) && !menu.contains(event.target)) closeMenu();
|
||||||
});
|
});
|
||||||
document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeMenu(); });
|
document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeMenu(); });
|
||||||
|
|
||||||
@@ -413,7 +448,8 @@ function elementById<T extends HTMLElement>(id: string): T {
|
|||||||
more.id = "mobile-more-btn";
|
more.id = "mobile-more-btn";
|
||||||
more.className = "tab mobile-more-btn";
|
more.className = "tab mobile-more-btn";
|
||||||
more.type = "button";
|
more.type = "button";
|
||||||
more.innerHTML = '<span class="tab-more-icon" aria-hidden="true">•••</span><span class="tab-label">More</span>';
|
more.innerHTML = '<span class="tab-more-icon" aria-hidden="true">•••</span><span class="tab-label">Tools</span>';
|
||||||
|
more.setAttribute("aria-label", "Tools and settings");
|
||||||
more.setAttribute("aria-haspopup", "menu");
|
more.setAttribute("aria-haspopup", "menu");
|
||||||
more.setAttribute("aria-expanded", "false");
|
more.setAttribute("aria-expanded", "false");
|
||||||
const menu = document.createElement("div");
|
const menu = document.createElement("div");
|
||||||
@@ -445,7 +481,10 @@ function elementById<T extends HTMLElement>(id: string): T {
|
|||||||
more.addEventListener("click", () => {
|
more.addEventListener("click", () => {
|
||||||
const open = menu.classList.toggle("is-open");
|
const open = menu.classList.toggle("is-open");
|
||||||
more.setAttribute("aria-expanded", String(open));
|
more.setAttribute("aria-expanded", String(open));
|
||||||
if (open) menu.querySelector<HTMLElement>('[role="menuitem"]')?.focus();
|
if (open) {
|
||||||
|
anchorMenu(more, menu);
|
||||||
|
menu.querySelector<HTMLElement>('[role="menuitem"]')?.focus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
if (!(event.target instanceof Node) || (!menu.contains(event.target) && !more.contains(event.target))) closeMore();
|
if (!(event.target instanceof Node) || (!menu.contains(event.target) && !more.contains(event.target))) closeMore();
|
||||||
|
|||||||
Reference in New Issue
Block a user