feat: implement a responsive mobile control panel with tabbed navigation and dynamic control staging, and refactor the desktop layout for mobile responsiveness.
This commit is contained in:
@@ -3,17 +3,32 @@ import TuiSlider from "./TuiSlider.astro";
|
||||
import TuiSegment from "./TuiSegment.astro";
|
||||
import TuiToggle from "./TuiToggle.astro";
|
||||
import TuiButton from "./TuiButton.astro";
|
||||
import { ChevronDown } from "@lucide/astro";
|
||||
import {
|
||||
ChevronDown,
|
||||
SlidersHorizontal,
|
||||
Wand2,
|
||||
Download,
|
||||
RotateCcw,
|
||||
SkipForward,
|
||||
Layers,
|
||||
} from "@lucide/astro";
|
||||
---
|
||||
|
||||
<footer id="tui-controls" class="control-panel">
|
||||
<!-- Desktop Header (Hidden on Mobile) -->
|
||||
<div class="desktop-header">
|
||||
<!-- Optional: keeps the old structure if needed, or purely CSS hidden -->
|
||||
</div>
|
||||
|
||||
<!-- Mobile Header (Hidden on Desktop) -->
|
||||
<div class="mobile-controls-header">
|
||||
<span class="mobile-controls-title">CONTROLS</span>
|
||||
<ChevronDown class="mobile-toggle-icon" size={24} />
|
||||
</div>
|
||||
|
||||
<div class="control-panel-inner">
|
||||
<div class="control-panel-content">
|
||||
<!-- DESKTOP LAYOUT (Hidden on Mobile via CSS) -->
|
||||
<div class="control-panel-content desktop-only">
|
||||
<!-- Sliders Section -->
|
||||
<div class="panel-section sliders-section">
|
||||
<div class="section-header">ADJUSTMENTS</div>
|
||||
@@ -183,6 +198,7 @@ import { ChevronDown } from "@lucide/astro";
|
||||
<div
|
||||
class="tui-color-btn"
|
||||
title="Monochrome Tint Color"
|
||||
id="btn-color-tint"
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
@@ -195,7 +211,11 @@ import { ChevronDown } from "@lucide/astro";
|
||||
</div>
|
||||
|
||||
<!-- Background Color Picker -->
|
||||
<div class="tui-color-btn" title="Background Color">
|
||||
<div
|
||||
class="tui-color-btn"
|
||||
title="Background Color"
|
||||
id="btn-color-bg"
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
id="input-bg-color"
|
||||
@@ -333,8 +353,8 @@ import { ChevronDown } from "@lucide/astro";
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keyboard shortcuts hint -->
|
||||
<div class="shortcuts-hint">
|
||||
<!-- KEYBOARD HINTS (Desktop Only) -->
|
||||
<div class="shortcuts-hint desktop-only">
|
||||
<span><kbd>N</kbd> Next</span>
|
||||
<span><kbd>R</kbd> Reset</span>
|
||||
<span><kbd>I</kbd> Invert</span>
|
||||
@@ -343,37 +363,331 @@ import { ChevronDown } from "@lucide/astro";
|
||||
<span><kbd>E</kbd> Edges</span>
|
||||
<span><kbd>S</kbd> Charset</span>
|
||||
</div>
|
||||
|
||||
<!-- MOBILE NAV CONTAINER (Hidden on Desktop) -->
|
||||
<div class="mobile-nav-container mobile-only">
|
||||
<!-- 1. ACTIVE VIEW (Where the selected control appears) -->
|
||||
<div class="mobile-active-view" id="mobile-control-stage">
|
||||
<div class="mobile-placeholder">Select a Control</div>
|
||||
<!-- Logic will move elements here -->
|
||||
</div>
|
||||
|
||||
<!-- 2. PARAMETER LIST (Horizontal Scroll) -->
|
||||
<div class="mobile-param-list" id="mobile-param-list">
|
||||
<!-- Populated via JS based on Active Tab -->
|
||||
</div>
|
||||
|
||||
<!-- 3. TABS (Categories) -->
|
||||
<div class="mobile-tabs">
|
||||
<button class="mobile-tab-btn active" data-tab="adjust">
|
||||
<SlidersHorizontal size={18} />
|
||||
<span>ADJUST</span>
|
||||
</button>
|
||||
<button class="mobile-tab-btn" data-tab="effects">
|
||||
<Wand2 size={18} />
|
||||
<span>EFFECTS</span>
|
||||
</button>
|
||||
<button class="mobile-tab-btn" data-tab="export">
|
||||
<Download size={18} />
|
||||
<span>EXPORT</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 4. FOOTER (High Freq Actions) -->
|
||||
<div class="mobile-footer">
|
||||
<button class="mobile-footer-btn" id="mobile-btn-reset">
|
||||
<RotateCcw size={16} /> RESET
|
||||
</button>
|
||||
<div class="mobile-queue-badge">
|
||||
<Layers size={14} />
|
||||
<span id="mobile-val-queue">0</span>
|
||||
</div>
|
||||
<button class="mobile-footer-btn primary" id="mobile-btn-next">
|
||||
NEXT <SkipForward size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const controlPanel = document.getElementById("tui-controls");
|
||||
const toggleBtn = controlPanel?.querySelector(".mobile-controls-header");
|
||||
import {
|
||||
SlidersHorizontal,
|
||||
Wand2,
|
||||
Download,
|
||||
RotateCcw,
|
||||
SkipForward,
|
||||
Layers,
|
||||
} from "@lucide/astro";
|
||||
|
||||
if (controlPanel && toggleBtn) {
|
||||
// Default to collapsed on mobile
|
||||
if (window.innerWidth <= 1200) {
|
||||
controlPanel.classList.add("collapsed");
|
||||
// --- MOBILE COMPACT MODE LOGIC ---
|
||||
|
||||
// Configuration for mapping controls to tabs
|
||||
const TABS: Record<string, string[]> = {
|
||||
adjust: [
|
||||
"exposure",
|
||||
"contrast",
|
||||
"saturation",
|
||||
"gamma",
|
||||
"shadows",
|
||||
"highlights",
|
||||
"sharpen",
|
||||
"resolution",
|
||||
"dither",
|
||||
"scanlines",
|
||||
"vignette",
|
||||
"overlayStrength",
|
||||
"edgeThreshold", // Actually belongs slightly to effects but fits slider format
|
||||
],
|
||||
effects: [
|
||||
"toggle-color",
|
||||
"btn-color-tint",
|
||||
"btn-color-bg",
|
||||
"toggle-denoise",
|
||||
"segment-invert",
|
||||
"segment-edge",
|
||||
"segment-charset",
|
||||
],
|
||||
export: [
|
||||
"btn-import",
|
||||
"btn-save-png",
|
||||
"btn-copy-text",
|
||||
"btn-copy-html",
|
||||
],
|
||||
};
|
||||
|
||||
// Dictionary to store original parents to restore them if needed resizing back to desktop
|
||||
const originalParents = new Map<string, Element | null>();
|
||||
|
||||
// Store current mode to prevent redundant operations
|
||||
let isMobileMode = false;
|
||||
|
||||
function initMobileMode() {
|
||||
// Init happens once, but state changes on resize
|
||||
const stage = document.getElementById("mobile-control-stage");
|
||||
const list = document.getElementById("mobile-param-list");
|
||||
const tabs = document.querySelectorAll(".mobile-tab-btn");
|
||||
|
||||
// Helper to get element by ID (handling some component specific wrappings if needed)
|
||||
const getEl = (id: string) => {
|
||||
// Try direct ID first
|
||||
let el = document.getElementById(id);
|
||||
if (el) {
|
||||
// For TuiSlider, the top level div has the ID in data-slider-id but not id attribute itself sometimes?
|
||||
// Wait, TuiSlider: <div class="tui-slider" data-slider-id={id}> ... <input id={id}>
|
||||
// We want the container.
|
||||
const container =
|
||||
el.closest(".tui-slider") ||
|
||||
el.closest(".tui-toggle") ||
|
||||
el.closest(".tui-segment") ||
|
||||
el.closest(".tui-btn") ||
|
||||
el.closest(".tui-color-btn") ||
|
||||
el;
|
||||
return container;
|
||||
}
|
||||
// Fallback for sliders where ID is on input
|
||||
const input = document.getElementById(id);
|
||||
if (input) {
|
||||
return (
|
||||
input.closest(".tui-slider") ||
|
||||
input.closest(".tui-color-btn")
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderTab = (tabName: string) => {
|
||||
if (!list) return;
|
||||
list.innerHTML = ""; // Clear list
|
||||
|
||||
const items = TABS[tabName];
|
||||
if (!items) return;
|
||||
|
||||
items.forEach((id) => {
|
||||
const el = getEl(id);
|
||||
if (!el) return;
|
||||
|
||||
// Create a Nav Pill
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "mobile-list-item";
|
||||
btn.id = `nav-pill-${id}`; // Help with tracking
|
||||
|
||||
// Get Label Text
|
||||
// Try various selectors based on component type
|
||||
let labelText = id;
|
||||
const labelEl = el.querySelector(
|
||||
".tui-slider-label .full, .tui-toggle-label .full, .tui-segment-label .full, .tui-btn-label .full, .label",
|
||||
);
|
||||
if (labelEl && labelEl.textContent)
|
||||
labelText = labelEl.textContent;
|
||||
|
||||
// Abbr fallback
|
||||
const abbrEl = el.querySelector(".abbr");
|
||||
if (
|
||||
(!labelEl || !labelEl.textContent) &&
|
||||
abbrEl &&
|
||||
abbrEl.textContent
|
||||
)
|
||||
labelText = abbrEl.textContent;
|
||||
|
||||
btn.textContent = labelText;
|
||||
btn.onclick = () => {
|
||||
// Highlight active logic
|
||||
document
|
||||
.querySelectorAll(".mobile-list-item")
|
||||
.forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
|
||||
// Move component to stage
|
||||
if (stage) {
|
||||
// Clean stage first - return any existing child to its home
|
||||
const currentInStage = stage.firstElementChild;
|
||||
if (
|
||||
currentInStage &&
|
||||
currentInStage.className !== "mobile-placeholder" &&
|
||||
currentInStage !== el
|
||||
) {
|
||||
// Return to original home
|
||||
// The ID might be on the element itself or a child (input)
|
||||
// We use data-origin-id tagged during move
|
||||
const originId = (currentInStage as HTMLElement)
|
||||
.dataset.originTabId;
|
||||
if (originId) {
|
||||
const orig = originalParents.get(originId);
|
||||
if (orig) orig.appendChild(currentInStage);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove placeholder if present
|
||||
const placeholder = stage.querySelector(
|
||||
".mobile-placeholder",
|
||||
);
|
||||
if (placeholder) placeholder.remove();
|
||||
|
||||
// Prepare element for move
|
||||
if (!originalParents.has(id)) {
|
||||
// Store parent for restoration
|
||||
originalParents.set(id, el.parentElement);
|
||||
}
|
||||
|
||||
(el as HTMLElement).dataset.originTabId = id; // Tag it
|
||||
stage.appendChild(el);
|
||||
}
|
||||
};
|
||||
list.appendChild(btn);
|
||||
});
|
||||
|
||||
// Auto-select first item?
|
||||
if (list.firstElementChild) {
|
||||
(list.firstElementChild as HTMLElement).click();
|
||||
}
|
||||
};
|
||||
|
||||
// --- State Management ---
|
||||
const checkMode = () => {
|
||||
// Match CSS breakpoint
|
||||
const isMobile = window.innerWidth <= 1024;
|
||||
|
||||
if (isMobile && !isMobileMode) {
|
||||
// Entering Mobile
|
||||
isMobileMode = true;
|
||||
console.log("📱 Entering Mobile Mode");
|
||||
|
||||
// Init tabs listeners if not already
|
||||
tabs.forEach((tab) => {
|
||||
// Check if already bound
|
||||
if ((tab as HTMLElement).dataset.hasListener) return;
|
||||
|
||||
tab.addEventListener("click", () => {
|
||||
const t = tab as HTMLElement;
|
||||
tabs.forEach((x) => x.classList.remove("active"));
|
||||
t.classList.add("active");
|
||||
if (t.dataset.tab) renderTab(t.dataset.tab);
|
||||
});
|
||||
(tab as HTMLElement).dataset.hasListener = "true";
|
||||
});
|
||||
|
||||
// Render default tab
|
||||
// Find active tab or default to adjust
|
||||
const activeTab = document.querySelector(
|
||||
".mobile-tab-btn.active",
|
||||
) as HTMLElement;
|
||||
renderTab(activeTab?.dataset.tab || "adjust");
|
||||
} else if (!isMobile && isMobileMode) {
|
||||
// Exiting Mobile - RESTORE EVERYTHING
|
||||
isMobileMode = false;
|
||||
console.log("🖥️ Entering Desktop Mode");
|
||||
|
||||
// 1. Move everything back from stage
|
||||
if (stage) {
|
||||
Array.from(stage.children).forEach((child) => {
|
||||
if (child.className === "mobile-placeholder") return;
|
||||
const originId = (child as HTMLElement).dataset
|
||||
.originTabId;
|
||||
if (originId) {
|
||||
const orig = originalParents.get(originId);
|
||||
if (orig) orig.appendChild(child);
|
||||
}
|
||||
});
|
||||
// Clear stage
|
||||
stage.innerHTML =
|
||||
'<div class="mobile-placeholder">Select a Control</div>';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for resize
|
||||
window.addEventListener("resize", checkMode);
|
||||
|
||||
// Initial check
|
||||
checkMode();
|
||||
|
||||
// --- FOOTER PROXY BUTTONS ---
|
||||
const bindFooterProxy = (proxyId: string, originalId: string) => {
|
||||
const proxy = document.getElementById(proxyId);
|
||||
const original = document.getElementById(originalId);
|
||||
if (proxy && original) {
|
||||
if (proxy.dataset.bound) return;
|
||||
proxy.addEventListener("click", () => original.click());
|
||||
proxy.dataset.bound = "true";
|
||||
}
|
||||
};
|
||||
bindFooterProxy("mobile-btn-reset", "btn-reset");
|
||||
bindFooterProxy("mobile-btn-next", "btn-next");
|
||||
|
||||
// Queue Sync
|
||||
const qVal = document.getElementById("val-queue");
|
||||
const mQVal = document.getElementById("mobile-val-queue");
|
||||
if (qVal && mQVal) {
|
||||
// Observer to sync text
|
||||
const obs = new MutationObserver(() => {
|
||||
if (qVal.textContent) mQVal.textContent = qVal.textContent;
|
||||
});
|
||||
obs.observe(qVal, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
});
|
||||
if (qVal.textContent) mQVal.textContent = qVal.textContent;
|
||||
}
|
||||
|
||||
toggleBtn.addEventListener("click", () => {
|
||||
controlPanel.classList.toggle("collapsed");
|
||||
});
|
||||
}
|
||||
|
||||
// Run on load
|
||||
document.addEventListener("DOMContentLoaded", initMobileMode);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Main Container matching Sidebar visual style */
|
||||
/* DESKTOP STYLES (Existing) */
|
||||
.control-panel {
|
||||
flex-shrink: 0;
|
||||
background: #000; /* Matching Sidebar */
|
||||
background: #000;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 2rem 3rem; /* Matching Sidebar padding style */
|
||||
padding: 2rem 3rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
z-index: 20;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.5); /* Inverted shadow */
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.5);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -381,7 +695,7 @@ import { ChevronDown } from "@lucide/astro";
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 2rem;
|
||||
flex-wrap: nowrap; /* Prevent wrapping on large screens */
|
||||
flex-wrap: nowrap;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
@@ -423,7 +737,7 @@ import { ChevronDown } from "@lucide/astro";
|
||||
|
||||
/* Headers */
|
||||
.section-header {
|
||||
font-size: 0.75rem; /* Matched to sidebar subtitle size approx */
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
opacity: 0.5;
|
||||
letter-spacing: 1px;
|
||||
@@ -437,7 +751,7 @@ import { ChevronDown } from "@lucide/astro";
|
||||
.sliders-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 1rem 1.5rem; /* More airy gap */
|
||||
gap: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.toggles-row {
|
||||
@@ -483,7 +797,7 @@ import { ChevronDown } from "@lucide/astro";
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
height: 32px; /* Match button height */
|
||||
height: 32px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
@@ -520,88 +834,15 @@ import { ChevronDown } from "@lucide/astro";
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Helper Classes */
|
||||
.mobile-only {
|
||||
display: none !important; /* Force hidden on desktop */
|
||||
}
|
||||
.mobile-controls-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1200px) {
|
||||
.control-panel {
|
||||
padding: 0; /* Let inner content handle padding or just reset */
|
||||
}
|
||||
|
||||
.mobile-controls-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
cursor: pointer;
|
||||
background: #000;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.mobile-controls-title {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.mobile-toggle-icon {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.control-panel.collapsed .mobile-toggle-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.control-panel-inner {
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
transition:
|
||||
max-height 0.4s ease,
|
||||
opacity 0.4s ease,
|
||||
padding 0.4s ease;
|
||||
max-height: 70dvh;
|
||||
opacity: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.control-panel.collapsed .control-panel-inner {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
padding: 0 1.5rem;
|
||||
pointer-events: none;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.control-panel-content {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.panel-divider-v {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sliders-section,
|
||||
.effects-section,
|
||||
.actions-section {
|
||||
flex: 1 1 100%; /* Stack on smaller screens */
|
||||
}
|
||||
|
||||
.sliders-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||
}
|
||||
|
||||
.shortcuts-hint {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Color Btn Styles (Shared) */
|
||||
.tui-color-btn {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
@@ -616,16 +857,14 @@ import { ChevronDown } from "@lucide/astro";
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s;
|
||||
height: 29px; /* Match toggle/button height approx */
|
||||
height: 29px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tui-color-btn:hover {
|
||||
color: #fff;
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.tui-color-btn input[type="color"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -638,7 +877,6 @@ import { ChevronDown } from "@lucide/astro";
|
||||
border: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
@@ -647,9 +885,167 @@ import { ChevronDown } from "@lucide/astro";
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.tui-color-btn .label {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* --- MOBILE STYLES --- */
|
||||
@media (max-width: 1024px) {
|
||||
.control-panel {
|
||||
padding: 0;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
border-top: 1px solid #222;
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.mobile-nav-container {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 240px; /* Fixed compact height */
|
||||
background: #000;
|
||||
}
|
||||
|
||||
/* 1. Active View */
|
||||
.mobile-active-view {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid #222;
|
||||
overflow: hidden;
|
||||
background: #080808;
|
||||
}
|
||||
|
||||
.mobile-placeholder {
|
||||
color: #444;
|
||||
font-size: 0.8rem;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* When components are moved here, they need to stretch */
|
||||
.mobile-active-view > :global(*) {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
/* 2. Param List */
|
||||
.mobile-param-list {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
background: #000;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
/* Hide scrollbar */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
.mobile-param-list::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-param-list :global(.mobile-list-item) {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
color: #fff;
|
||||
opacity: 0.8;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
border-radius: 2px;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mobile-param-list :global(.mobile-list-item.active) {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--accent-color),
|
||||
transparent 90%
|
||||
);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 3. Tabs */
|
||||
.mobile-tabs {
|
||||
display: flex;
|
||||
border-top: 1px solid #222;
|
||||
height: 50px;
|
||||
}
|
||||
.mobile-tab-btn {
|
||||
flex: 1;
|
||||
background: #000;
|
||||
border: none;
|
||||
color: #555;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
font-size: 9px;
|
||||
font-family: var(--font-mono);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.mobile-tab-btn span {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.mobile-tab-btn.active {
|
||||
color: var(--accent-color);
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
/* 4. Footer */
|
||||
.mobile-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
height: 44px;
|
||||
background: #050505;
|
||||
border-top: 1px solid #1a1a1a;
|
||||
}
|
||||
.mobile-footer-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
opacity: 0.7;
|
||||
padding: 8px;
|
||||
}
|
||||
.mobile-footer-btn.primary {
|
||||
color: var(--accent-color);
|
||||
font-weight: 700;
|
||||
opacity: 1;
|
||||
}
|
||||
.mobile-queue-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #444;
|
||||
font-size: 10px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
};
|
||||
|
||||
const showTooltip = (target: Element, e: MouseEvent) => {
|
||||
// Only show on devices with hover capability (mouse)
|
||||
if (!window.matchMedia("(hover: hover)").matches) return;
|
||||
|
||||
const title = target.getAttribute("data-tooltip-title");
|
||||
const desc = target.getAttribute("data-tooltip-desc");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user