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 TuiSegment from "./TuiSegment.astro";
|
||||||
import TuiToggle from "./TuiToggle.astro";
|
import TuiToggle from "./TuiToggle.astro";
|
||||||
import TuiButton from "./TuiButton.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">
|
<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">
|
<div class="mobile-controls-header">
|
||||||
<span class="mobile-controls-title">CONTROLS</span>
|
<span class="mobile-controls-title">CONTROLS</span>
|
||||||
<ChevronDown class="mobile-toggle-icon" size={24} />
|
<ChevronDown class="mobile-toggle-icon" size={24} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-panel-inner">
|
<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 -->
|
<!-- Sliders Section -->
|
||||||
<div class="panel-section sliders-section">
|
<div class="panel-section sliders-section">
|
||||||
<div class="section-header">ADJUSTMENTS</div>
|
<div class="section-header">ADJUSTMENTS</div>
|
||||||
@@ -183,6 +198,7 @@ import { ChevronDown } from "@lucide/astro";
|
|||||||
<div
|
<div
|
||||||
class="tui-color-btn"
|
class="tui-color-btn"
|
||||||
title="Monochrome Tint Color"
|
title="Monochrome Tint Color"
|
||||||
|
id="btn-color-tint"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
@@ -195,7 +211,11 @@ import { ChevronDown } from "@lucide/astro";
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Background Color Picker -->
|
<!-- Background Color Picker -->
|
||||||
<div class="tui-color-btn" title="Background Color">
|
<div
|
||||||
|
class="tui-color-btn"
|
||||||
|
title="Background Color"
|
||||||
|
id="btn-color-bg"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
id="input-bg-color"
|
id="input-bg-color"
|
||||||
@@ -333,8 +353,8 @@ import { ChevronDown } from "@lucide/astro";
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Keyboard shortcuts hint -->
|
<!-- KEYBOARD HINTS (Desktop Only) -->
|
||||||
<div class="shortcuts-hint">
|
<div class="shortcuts-hint desktop-only">
|
||||||
<span><kbd>N</kbd> Next</span>
|
<span><kbd>N</kbd> Next</span>
|
||||||
<span><kbd>R</kbd> Reset</span>
|
<span><kbd>R</kbd> Reset</span>
|
||||||
<span><kbd>I</kbd> Invert</span>
|
<span><kbd>I</kbd> Invert</span>
|
||||||
@@ -343,37 +363,331 @@ import { ChevronDown } from "@lucide/astro";
|
|||||||
<span><kbd>E</kbd> Edges</span>
|
<span><kbd>E</kbd> Edges</span>
|
||||||
<span><kbd>S</kbd> Charset</span>
|
<span><kbd>S</kbd> Charset</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const controlPanel = document.getElementById("tui-controls");
|
import {
|
||||||
const toggleBtn = controlPanel?.querySelector(".mobile-controls-header");
|
SlidersHorizontal,
|
||||||
|
Wand2,
|
||||||
|
Download,
|
||||||
|
RotateCcw,
|
||||||
|
SkipForward,
|
||||||
|
Layers,
|
||||||
|
} from "@lucide/astro";
|
||||||
|
|
||||||
if (controlPanel && toggleBtn) {
|
// --- MOBILE COMPACT MODE LOGIC ---
|
||||||
// Default to collapsed on mobile
|
|
||||||
if (window.innerWidth <= 1200) {
|
// Configuration for mapping controls to tabs
|
||||||
controlPanel.classList.add("collapsed");
|
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Main Container matching Sidebar visual style */
|
/* DESKTOP STYLES (Existing) */
|
||||||
.control-panel {
|
.control-panel {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: #000; /* Matching Sidebar */
|
background: #000;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
padding: 2rem 3rem; /* Matching Sidebar padding style */
|
padding: 2rem 3rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
z-index: 20;
|
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;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +695,7 @@ import { ChevronDown } from "@lucide/astro";
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
flex-wrap: nowrap; /* Prevent wrapping on large screens */
|
flex-wrap: nowrap;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,7 +737,7 @@ import { ChevronDown } from "@lucide/astro";
|
|||||||
|
|
||||||
/* Headers */
|
/* Headers */
|
||||||
.section-header {
|
.section-header {
|
||||||
font-size: 0.75rem; /* Matched to sidebar subtitle size approx */
|
font-size: 0.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
@@ -437,7 +751,7 @@ import { ChevronDown } from "@lucide/astro";
|
|||||||
.sliders-grid {
|
.sliders-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
gap: 1rem 1.5rem; /* More airy gap */
|
gap: 1rem 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggles-row {
|
.toggles-row {
|
||||||
@@ -483,7 +797,7 @@ import { ChevronDown } from "@lucide/astro";
|
|||||||
color: rgba(255, 255, 255, 0.4);
|
color: rgba(255, 255, 255, 0.4);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 32px; /* Match button height */
|
height: 32px;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,88 +834,15 @@ import { ChevronDown } from "@lucide/astro";
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Helper Classes */
|
||||||
|
.mobile-only {
|
||||||
|
display: none !important; /* Force hidden on desktop */
|
||||||
|
}
|
||||||
.mobile-controls-header {
|
.mobile-controls-header {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
/* Color Btn Styles (Shared) */
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tui-color-btn {
|
.tui-color-btn {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -616,16 +857,14 @@ import { ChevronDown } from "@lucide/astro";
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
height: 29px; /* Match toggle/button height approx */
|
height: 29px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tui-color-btn:hover {
|
.tui-color-btn:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: rgba(255, 255, 255, 0.3);
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tui-color-btn input[type="color"] {
|
.tui-color-btn input[type="color"] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -638,7 +877,6 @@ import { ChevronDown } from "@lucide/astro";
|
|||||||
border: none;
|
border: none;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-swatch {
|
.color-swatch {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
@@ -647,9 +885,167 @@ import { ChevronDown } from "@lucide/astro";
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tui-color-btn .label {
|
.tui-color-btn .label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.5px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -89,6 +89,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showTooltip = (target: Element, e: MouseEvent) => {
|
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 title = target.getAttribute("data-tooltip-title");
|
||||||
const desc = target.getAttribute("data-tooltip-desc");
|
const desc = target.getAttribute("data-tooltip-desc");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user