diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..5431954 --- /dev/null +++ b/static/index.html @@ -0,0 +1,14 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <title>RGB Slider Tabs</title> + <link rel="stylesheet" href="styles.css" /> + </head> + <body> + <div class="tabs"></div> + <div class="tab-content"></div> + + <script type="module" src="main.js"></script> + </body> +</html> diff --git a/static/main.js b/static/main.js new file mode 100644 index 0000000..6dce033 --- /dev/null +++ b/static/main.js @@ -0,0 +1,81 @@ +import "./rgb-slider.js"; + +const ws = new WebSocket("ws://localhost:8000/ws"); + +ws.onopen = () => { + console.log("WebSocket connection established"); +}; + +ws.onclose = () => { + console.log("WebSocket connection closed"); +}; + +ws.onerror = (error) => { + console.error("WebSocket error:", error); +}; + +// Number of sliders (tabs) you want to create +const numTabs = 3; + +// Select the container for tabs and content +const tabsContainer = document.querySelector(".tabs"); +const tabContentContainer = document.querySelector(".tab-content"); + +// Create tabs dynamically +for (let i = 1; i <= numTabs; i++) { + // Create the tab button + const tabButton = document.createElement("button"); + tabButton.classList.add("tab"); + tabButton.id = `tab${i}`; + tabButton.textContent = `Tab ${i}`; + + // Add the tab button to the container + tabsContainer.appendChild(tabButton); + + // Create the corresponding tab content (RGB slider) + const tabContent = document.createElement("div"); + tabContent.classList.add("tab-pane"); + tabContent.id = `content${i}`; + const slider = document.createElement("rgb-slider"); + slider.id = i; + tabContent.appendChild(slider); + + // Add the tab content to the container + tabContentContainer.appendChild(tabContent); + + // Listen for color change on each RGB slider + slider.addEventListener("color-change", (e) => { + const { r, g, b } = e.detail; + console.log(`Color changed in tab ${i}:`, e.detail); + // Send RGB data to WebSocket server + if (ws.readyState === WebSocket.OPEN) { + const colorData = { r, g, b }; + ws.send(JSON.stringify(colorData)); + } + }); +} + +// Function to switch tabs +function switchTab(tabId) { + const tabs = document.querySelectorAll(".tab"); + const tabContents = document.querySelectorAll(".tab-pane"); + + tabs.forEach((tab) => tab.classList.remove("active")); + tabContents.forEach((content) => content.classList.remove("active")); + + // Activate the clicked tab and corresponding content + document.getElementById(tabId).classList.add("active"); + document + .getElementById("content" + tabId.replace("tab", "")) + .classList.add("active"); +} + +// Add event listeners to tabs +tabsContainer.addEventListener("click", (e) => { + if (e.target.classList.contains("tab")) { + switchTab(e.target.id); + } +}); + +// Initially set the first tab as active +switchTab("tab1"); diff --git a/static/rgb-slider.js b/static/rgb-slider.js new file mode 100644 index 0000000..0c986df --- /dev/null +++ b/static/rgb-slider.js @@ -0,0 +1,195 @@ +// rgb-slider.js + +export class RGBSlider extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({ mode: "open" }); + + shadow.innerHTML = ` + <style> + .container { + display: flex; + flex-direction: column; + align-items: center; + padding: 1em; + border: 1px solid #ccc; + border-radius: 8px; + width: 50%; + + font-family: sans-serif; + box-sizing: border-box; + } + + .preview { + width: 50%; + height: 60px; + border-radius: 6px; + border: 1px solid #000; + background-color: rgb(0, 0, 0); + margin-bottom: 1em; + } + + .sliders { + display: flex; + gap: 50px; + justify-content: center; + margin-bottom: 1em; + } + + .slider-group { + display: flex; + flex-direction: column-reverse; + align-items: center; + } + + .slider-group input[type="range"] { + writing-mode: vertical-lr; + direction: rtl; + + width: 10px; + height: 120px; + } + + .slider-group label { + margin-top: 8px; + font-size: 0.8em; + } + + .rgb-inputs { + display: flex; + gap: 8px; + } + + .rgb-inputs input { + width: 6ch; + padding: 2px; + font-family: monospace; + text-align: right; + } + + .rgb-inputs label { + font-size: 0.8em; + text-align: center; + } + + .rgb-input-group { + display: flex; + flex-direction: column; + align-items: center; + } + + /* Mobile styles */ + @media (max-width: 600px) { + .preview { + height: 80px; + } + + .slider-group input[type="range"] { + height: 180px; + width: 14px; + } + + .rgb-inputs input { + font-size: 1em; + padding: 4px; + width: 7ch; + } + + .slider-group label, + .rgb-inputs label { + font-size: 1em; + } + + .container { + padding: 1.5em; + } + } + </style> + + + <div class="container"> + <div class="preview" id="preview"></div> + + <div class="sliders"> + <div class="slider-group"> + <input type="range" min="0" max="255" value="0" id="r"> + <label>R</label> + </div> + <div class="slider-group"> + <input type="range" min="0" max="255" value="0" id="g"> + <label>G</label> + </div> + <div class="slider-group"> + <input type="range" min="0" max="255" value="0" id="b"> + <label>B</label> + </div> + </div> + + <div class="rgb-inputs"> + <div class="rgb-input-group"> + <label for="rInput">R</label> + <input type="number" min="0" max="255" id="rInput" value="0"> + </div> + <div class="rgb-input-group"> + <label for="gInput">G</label> + <input type="number" min="0" max="255" id="gInput" value="0"> + </div> + <div class="rgb-input-group"> + <label for="bInput">B</label> + <input type="number" min="0" max="255" id="bInput" value="0"> + </div> + </div> + </div> + `; + + const get = (id) => shadow.querySelector(id); + this.r = get("#r"); + this.g = get("#g"); + this.b = get("#b"); + this.rInput = get("#rInput"); + this.gInput = get("#gInput"); + this.bInput = get("#bInput"); + this.preview = get("#preview"); + + const updateColor = (r, g, b) => { + this.preview.style.backgroundColor = `rgb(${r}, ${g}, ${b})`; + this.rInput.value = r; + this.gInput.value = g; + this.bInput.value = b; + this.dispatchEvent( + new CustomEvent("color-change", { + detail: { r, g, b }, + bubbles: true, + composed: true, + }), + ); + }; + + const syncFromSliders = () => { + const r = +this.r.value; + const g = +this.g.value; + const b = +this.b.value; + updateColor(r, g, b); + }; + + const syncFromInputs = () => { + const r = Math.min(255, Math.max(0, +this.rInput.value)); + const g = Math.min(255, Math.max(0, +this.gInput.value)); + const b = Math.min(255, Math.max(0, +this.bInput.value)); + this.r.value = r; + this.g.value = g; + this.b.value = b; + updateColor(r, g, b); + }; + + this.r.addEventListener("input", syncFromSliders); + this.g.addEventListener("input", syncFromSliders); + this.b.addEventListener("input", syncFromSliders); + + this.rInput.addEventListener("change", syncFromInputs); + this.gInput.addEventListener("change", syncFromInputs); + this.bInput.addEventListener("change", syncFromInputs); + } +} + +customElements.define("rgb-slider", RGBSlider); diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..e3ecf1c --- /dev/null +++ b/static/styles.css @@ -0,0 +1,37 @@ +/* General tab styles */ +.tabs { + display: flex; + justify-content: center; + margin-bottom: 20px; +} + +.tab { + padding: 10px 20px; + margin: 0 10px; + cursor: pointer; + background-color: #f1f1f1; + border: 1px solid #ccc; + border-radius: 4px; + transition: background-color 0.3s ease; +} + +.tab:hover { + background-color: #ddd; +} + +.tab.active { + background-color: #ccc; +} + +.tab-content { + display: flex; + justify-content: center; +} + +.tab-pane { + display: none; +} + +.tab-pane.active { + display: block; +}