diff --git a/server/pyproject.toml b/server/pyproject.toml index da52924..8f6116b 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "python-json-logger>=3.1.0", "python-dateutil>=2.9.0", "python-multipart>=0.0.12", + "jinja2>=3.1.0", "wmi>=1.5.1; sys_platform == 'win32'", "zeroconf>=0.131.0", "pyserial>=3.5", diff --git a/server/src/wled_controller/main.py b/server/src/wled_controller/main.py index 9f9a5b9..d5ee020 100644 --- a/server/src/wled_controller/main.py +++ b/server/src/wled_controller/main.py @@ -6,8 +6,10 @@ from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, FileResponse +from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from starlette.requests import Request from wled_controller import __version__ from wled_controller.api import router @@ -262,6 +264,10 @@ if static_path.exists(): else: logger.warning(f"Static files directory not found: {static_path}") +# Jinja2 templates +templates_path = Path(__file__).parent / "templates" +templates = Jinja2Templates(directory=str(templates_path)) + @app.exception_handler(Exception) async def global_exception_handler(request, exc): @@ -279,20 +285,9 @@ async def global_exception_handler(request, exc): @app.get("/") -async def root(): +async def root(request: Request): """Serve the web UI dashboard.""" - static_path = Path(__file__).parent / "static" / "index.html" - if static_path.exists(): - return FileResponse(static_path) - - # Fallback to JSON if static files not found - return { - "name": "LED Grab", - "version": __version__, - "docs": "/docs", - "health": "/health", - "api": "/api/v1", - } + return templates.TemplateResponse(request, "index.html") if __name__ == "__main__": diff --git a/server/src/wled_controller/static/css/base.css b/server/src/wled_controller/static/css/base.css new file mode 100644 index 0000000..8070b3a --- /dev/null +++ b/server/src/wled_controller/static/css/base.css @@ -0,0 +1,61 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #4CAF50; + --danger-color: #f44336; + --warning-color: #ff9800; + --info-color: #2196F3; +} + +/* Dark theme (default) */ +[data-theme="dark"] { + --bg-color: #1a1a1a; + --card-bg: #2d2d2d; + --text-color: #e0e0e0; + --border-color: #404040; + --display-badge-bg: rgba(0, 0, 0, 0.4); + color-scheme: dark; +} + +/* Light theme */ +[data-theme="light"] { + --bg-color: #f5f5f5; + --card-bg: #ffffff; + --text-color: #333333; + --border-color: #e0e0e0; + --display-badge-bg: rgba(255, 255, 255, 0.85); + color-scheme: light; +} + +/* Default to dark theme */ +body { + background: var(--bg-color); + color: var(--text-color); +} + +html { + background: var(--bg-color); + overflow-y: scroll; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: var(--bg-color); + color: var(--text-color); + line-height: 1.6; +} + +body.modal-open { + position: fixed; + width: 100%; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} diff --git a/server/src/wled_controller/static/css/calibration.css b/server/src/wled_controller/static/css/calibration.css new file mode 100644 index 0000000..ba68c05 --- /dev/null +++ b/server/src/wled_controller/static/css/calibration.css @@ -0,0 +1,423 @@ +/* Interactive Calibration Preview Edges */ +.calibration-preview { + position: relative; + width: 100%; + aspect-ratio: 16 / 9; + margin: 40px auto 40px; + background: var(--card-bg); + border: 2px solid var(--border-color); + border-radius: 8px; + overflow: visible; +} + +#calibration-preview-canvas { + position: absolute; + top: -40px; + left: -40px; + width: calc(100% + 80px); + height: calc(100% + 80px); + pointer-events: none; + z-index: 3; +} + +.preview-screen { + position: absolute; + top: 37px; + left: 57px; + right: 57px; + bottom: 37px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 4px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + color: white; + font-size: 14px; +} + +.preview-screen-border-width { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; +} + +.preview-screen-border-width label { + white-space: nowrap; +} + +.preview-screen-border-width input { + width: 52px; + padding: 2px 4px; + font-size: 12px; + border: 1px solid rgba(255, 255, 255, 0.4); + border-radius: 3px; + background: rgba(255, 255, 255, 0.15); + color: white; + text-align: center; +} + +.preview-screen-border-width input:focus { + outline: none; + border-color: rgba(255, 255, 255, 0.7); + background: rgba(255, 255, 255, 0.25); +} + +.preview-screen-total { + font-size: 16px; + font-weight: 600; + opacity: 0.9; + transition: color 0.2s; + cursor: pointer; + user-select: none; +} + +.preview-screen-total:hover { + opacity: 1; +} + +.preview-screen-total.mismatch { + color: #FFC107; +} + +.inputs-dimmed .edge-led-input { + opacity: 0.2; + pointer-events: none; +} + +.preview-screen-controls { + display: flex; + align-items: center; + gap: 6px; +} + +.preview-edge { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + color: var(--text-secondary); + background: rgba(128, 128, 128, 0.15); + transition: background 0.2s; + z-index: 2; + user-select: none; +} + +/* Edge test toggle zones — positioned outside the container border */ +.edge-toggle { + position: absolute; + cursor: pointer; + z-index: 1; + background: rgba(128, 128, 128, 0.1); + border: 1px solid rgba(128, 128, 128, 0.35); + border-radius: 3px; + transition: background 0.2s, box-shadow 0.2s; +} + +.edge-toggle:hover { + background: rgba(128, 128, 128, 0.25); +} + +.preview-edge.edge-disabled { + opacity: 0.25; + pointer-events: none; +} + +.preview-edge.edge-disabled .edge-led-input { + pointer-events: auto; + opacity: 1; +} + +.edge-toggle.edge-disabled { + opacity: 0.15; + pointer-events: none; + cursor: default; +} + +.toggle-top { + top: -16px; + left: 56px; + right: 56px; + height: 16px; +} + +.toggle-bottom { + bottom: -16px; + left: 56px; + right: 56px; + height: 16px; +} + +.toggle-left { + left: -16px; + top: 36px; + bottom: 36px; + width: 16px; +} + +.toggle-right { + right: -16px; + top: 36px; + bottom: 36px; + width: 16px; +} + +.edge-top { + top: 0; + left: 56px; + right: 56px; + height: 36px; + border-radius: 6px 6px 0 0; + flex-direction: row; + gap: 8px; +} + +.edge-bottom { + bottom: 0; + left: 56px; + right: 56px; + height: 36px; + border-radius: 0 0 6px 6px; + flex-direction: row; + gap: 8px; +} + +.edge-left { + left: 0; + top: 36px; + bottom: 36px; + width: 56px; + flex-direction: column; + border-radius: 6px 0 0 6px; + gap: 4px; +} + +.edge-right { + right: 0; + top: 36px; + bottom: 36px; + width: 56px; + flex-direction: column; + border-radius: 0 6px 6px 0; + gap: 4px; +} + +.edge-led-input { + width: 46px; + padding: 2px 2px; + font-size: 12px; + box-sizing: border-box; + max-height: 100%; + text-align: center; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 3px; + color: inherit; +} + +.edge-led-input:focus { + border-color: var(--primary-color); + outline: none; +} + +.edge-top .edge-led-input, +.edge-bottom .edge-led-input { + width: 56px; +} + +/* Hide spinner arrows on edge inputs to save space */ +.edge-led-input::-webkit-outer-spin-button, +.edge-led-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} +.edge-led-input[type=number] { + -moz-appearance: textfield; +} + +/* Edge span bars */ +.edge-span-bar { + position: absolute; + background: rgba(76, 175, 80, 0.3); + border: 1px solid rgba(76, 175, 80, 0.5); + border-radius: 2px; + cursor: grab; + transition: background 0.15s; +} + +.edge-span-bar:hover { + background: rgba(76, 175, 80, 0.45); +} + +.edge-span-bar:active { + cursor: grabbing; +} + +/* Horizontal edges: bar spans left-right */ +.edge-top .edge-span-bar, +.edge-bottom .edge-span-bar { + top: 0; + bottom: 0; +} + +/* Vertical edges: bar spans top-bottom */ +.edge-left .edge-span-bar, +.edge-right .edge-span-bar { + left: 0; + right: 0; +} + +/* Resize handles — large transparent hit area with narrow visible strip */ +.edge-span-handle { + position: absolute; + background: transparent; + z-index: 3; + opacity: 0; + transition: opacity 0.15s; +} + +.edge-span-handle::after { + content: ''; + position: absolute; + background: rgba(255, 255, 255, 0.75); + border: 1px solid rgba(76, 175, 80, 0.7); + border-radius: 2px; +} + +.edge-span-bar:hover .edge-span-handle { + opacity: 1; +} + +/* Horizontal handles */ +.edge-top .edge-span-handle, +.edge-bottom .edge-span-handle { + top: 0; + bottom: 0; + width: 16px; + cursor: ew-resize; +} + +.edge-top .edge-span-handle::after, +.edge-bottom .edge-span-handle::after { + top: 3px; + bottom: 3px; + left: 6px; + width: 4px; +} + +.edge-top .edge-span-handle-start, +.edge-bottom .edge-span-handle-start { + left: -8px; +} + +.edge-top .edge-span-handle-end, +.edge-bottom .edge-span-handle-end { + right: -8px; +} + +/* Vertical handles */ +.edge-left .edge-span-handle, +.edge-right .edge-span-handle { + left: 0; + right: 0; + height: 16px; + cursor: ns-resize; +} + +.edge-left .edge-span-handle::after, +.edge-right .edge-span-handle::after { + left: 3px; + right: 3px; + top: 6px; + height: 4px; +} + +.edge-left .edge-span-handle-start, +.edge-right .edge-span-handle-start { + top: -8px; +} + +.edge-left .edge-span-handle-end, +.edge-right .edge-span-handle-end { + bottom: -8px; +} + +/* Ensure LED input is above span bar */ +.edge-led-input { + position: relative; + z-index: 2; +} + +/* Corner start-position buttons */ +.preview-corner { + position: absolute; + width: 56px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + line-height: 1; + color: rgba(128, 128, 128, 0.4); + cursor: pointer; + z-index: 5; + transition: color 0.2s, transform 0.2s, text-shadow 0.2s; + user-select: none; +} + +.preview-corner:hover { + color: rgba(76, 175, 80, 0.6); +} + +.preview-corner.active:hover { + transform: none; +} + +.preview-corner.active { + color: #4CAF50; + text-shadow: 0 0 8px rgba(76, 175, 80, 0.6); + font-size: 22px; +} + +.corner-top-left { top: 0; left: 0; } +.corner-top-right { top: 0; right: 0; } +.corner-bottom-left { bottom: 0; left: 0; } +.corner-bottom-right { bottom: 0; right: 0; } + +/* Direction toggle inside screen */ +.direction-toggle { + display: flex; + align-items: center; + gap: 4px; + height: 26px; + padding: 0 10px; + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 12px; + color: white; + font-family: inherit; + font-size: 12px; + box-sizing: border-box; + cursor: pointer; + transition: background 0.2s; + user-select: none; +} + +.direction-toggle:hover { + background: rgba(255, 255, 255, 0.25); +} + +.direction-toggle #direction-icon { + font-size: 14px; +} + +.preview-hint { + text-align: center; + font-size: 0.8rem; + color: var(--text-secondary); + margin-top: 8px; +} diff --git a/server/src/wled_controller/static/css/cards.css b/server/src/wled_controller/static/css/cards.css new file mode 100644 index 0000000..b810201 --- /dev/null +++ b/server/src/wled_controller/static/css/cards.css @@ -0,0 +1,592 @@ +section { + margin-bottom: 40px; +} + +.displays-grid, +.devices-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px; +} + +.devices-grid > .loading, +.devices-grid > .loading-spinner { + grid-column: 1 / -1; +} + +.add-device-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + border: 2px dashed var(--border-color); + background: transparent; + min-height: 160px; + transition: border-color 0.2s, background 0.2s; +} + +.add-device-card:hover { + border-color: var(--primary-color); + background: rgba(33, 150, 243, 0.05); +} + +.add-device-icon { + font-size: 2.5rem; + font-weight: 300; + color: var(--text-secondary); + line-height: 1; + transition: color 0.2s; +} + +.add-device-card:hover .add-device-icon { + color: var(--primary-color); +} + +.add-device-label { + font-size: 0.85rem; + color: var(--text-secondary); + margin-top: 8px; +} + +.card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 12px 20px 20px; + position: relative; + transition: transform 0.2s, box-shadow 0.2s; + display: flex; + flex-direction: column; +} + +.card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.card-tutorial-btn { + position: absolute; + bottom: 10px; + right: 10px; + background: none; + border: 1.5px solid var(--border-color); + color: var(--text-muted, #777); + font-size: 0.7rem; + font-weight: bold; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 50%; + transition: color 0.2s, background 0.2s, border-color 0.2s; + padding: 0; +} + +.card-tutorial-btn:hover { + border-color: var(--primary-color); + color: var(--primary-color); +} + + +.card-top-actions { + position: absolute; + top: 8px; + right: 8px; + display: flex; + align-items: center; + gap: 2px; +} + +.card-top-actions .card-remove-btn { + position: static; +} + +.card-power-btn { + background: none; + border: none; + color: #777; + font-size: 1rem; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 4px; + transition: color 0.2s, background 0.2s; +} + +.card-power-btn:hover { + color: var(--primary-color); + background: rgba(76, 175, 80, 0.1); +} + +.card-remove-btn { + position: absolute; + top: 10px; + right: 10px; + background: none; + border: none; + color: #777; + font-size: 1rem; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 4px; + transition: color 0.2s, background 0.2s; +} + +.card-remove-btn:hover { + color: var(--danger-color); + background: rgba(244, 67, 54, 0.1); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + padding-right: 30px; +} + +.card-title { + font-size: 1.2rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.device-url-badge { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 0.7rem; + font-weight: 400; + color: var(--text-secondary); + background: var(--border-color); + padding: 2px 8px; + border-radius: 10px; + letter-spacing: 0.03em; + font-family: monospace; + text-decoration: none; + transition: background 0.2s; +} + +.device-url-badge:hover { + background: var(--text-muted); +} + +.device-url-icon { + font-size: 0.6rem; +} + +.card-subtitle { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 15px; + flex-wrap: wrap; +} + +.card-meta { + font-size: 0.8rem; + color: #999; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.device-type-badge { + font-size: 10px; + font-weight: 700; + padding: 1px 6px; + border-radius: 3px; + background: rgba(255, 255, 255, 0.15); + letter-spacing: 0.5px; +} + +/* Device discovery */ +.discovery-section { + margin-bottom: 12px; +} +.btn-block { + width: 100%; +} +.discovery-list { + max-height: 200px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 8px; +} +.discovery-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--card-bg); + cursor: pointer; + transition: opacity 0.15s; +} +.discovery-item:not(.discovery-item--added):hover { + opacity: 0.8; +} +.discovery-item--added { + opacity: 0.5; + cursor: default; +} +.discovery-item-info { + display: flex; + flex-direction: column; + gap: 2px; +} +.discovery-badge { + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; + background: var(--border-color); + white-space: nowrap; +} +.discovery-type-badge { + font-size: 10px; + padding: 1px 5px; + border-radius: 3px; + background: var(--primary-color); + color: #fff; + font-weight: 600; + vertical-align: middle; + margin-right: 2px; +} +.modal-divider { + border: none; + border-top: 1px solid var(--border-color); + margin: 12px 0; +} +.discovery-loading { + display: flex; + justify-content: center; + padding: 12px; +} +.discovery-spinner { + width: 20px; + height: 20px; + border: 2px solid var(--border-color); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.channel-indicator { + display: inline-flex; + gap: 2px; + align-items: center; +} + +.channel-indicator .ch { + width: 8px; + height: 10px; + border-radius: 1px; +} + +.display-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 15px; +} + +.display-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.display-index { + font-size: 1.3rem; + font-weight: 700; + color: var(--info-color); +} + +.primary-star { + color: var(--primary-color); + font-size: 1.2rem; +} + +/* Display Layout Visualization */ +.layout-container { + position: relative; + background: transparent; +} + +.layout-display { + position: absolute; + border: 3px solid; + border-radius: 8px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + transition: box-shadow 0.2s, border-color 0.2s; +} + +.layout-display:hover { + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); + z-index: 10; +} + +.layout-display.primary { + border-color: var(--primary-color); + background: linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(76, 175, 80, 0.05)); +} + +.layout-display.secondary { + border-color: var(--border-color); + background: linear-gradient(135deg, rgba(128, 128, 128, 0.1), rgba(128, 128, 128, 0.05)); +} + +.layout-position-label { + position: absolute; + top: 4px; + left: 6px; + font-size: 0.7rem; + color: var(--text-secondary); +} + +.layout-index-label { + position: absolute; + bottom: 6px; + left: 6px; + font-size: 0.85rem; + font-weight: 600; + color: var(--text-color); + background: var(--display-badge-bg); + padding: 1px 6px; + border-radius: 4px; + letter-spacing: 0.5px; +} + +.layout-display-label { + text-align: center; + padding: 5px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.layout-display-label strong { + font-size: 0.9rem; + color: var(--text-color); + font-weight: 600; +} + +.layout-display-label small { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.primary-indicator { + position: absolute; + top: 2px; + right: 4px; + color: var(--primary-color); + font-size: 1.5rem; + line-height: 1; + text-shadow: 0 0 4px rgba(0, 0, 0, 0.4); +} + + +/* Card brightness slider */ +.brightness-control { + padding: 0; + margin-bottom: 12px; +} + +.brightness-slider { + width: 100%; +} + +.brightness-loading .brightness-slider { + opacity: 0.3; + pointer-events: none; +} + +/* Static color picker — inline in card-subtitle */ +.static-color-control { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.static-color-picker { + width: 22px; + height: 18px; + padding: 0; + border: 1px solid var(--border-color); + border-radius: 3px; + cursor: pointer; + background: none; + vertical-align: middle; +} + +.btn-clear-color { + background: none; + border: none; + color: #777; + font-size: 0.75rem; + cursor: pointer; + padding: 0 2px; + line-height: 1; + border-radius: 3px; + transition: color 0.2s; +} + +.btn-clear-color:hover { + color: var(--danger-color); +} + +.section-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.section-header h2 { + margin-bottom: 0; +} + +.section-tip { + font-size: 0.82rem; + color: var(--text-secondary); + margin: 0 0 15px 0; + line-height: 1.5; + padding: 8px 12px; + background: rgba(33, 150, 243, 0.08); + border-left: 3px solid var(--info-color, #2196F3); + border-radius: 0 6px 6px 0; +} + +.section-tip a { + color: var(--info-color, #2196F3); + text-decoration: underline; +} + +ul.section-tip { + list-style: disc; + padding-left: 28px; +} + +ul.section-tip li { + margin: 2px 0; +} + +.metrics-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px 12px; + margin-top: 8px; +} + +.metric { + display: flex; + justify-content: space-between; + align-items: center; + padding: 3px 8px; + background: var(--bg-color); + border-radius: 4px; +} + +.metric-value { + font-size: 0.9rem; + font-weight: 700; + color: var(--primary-color); +} + +.metric-label { + font-size: 0.8rem; + color: #999; +} + +/* Timing breakdown bar */ +.timing-breakdown { + margin-top: 8px; + padding: 6px 8px; + background: var(--bg-color); + border-radius: 4px; +} + +.timing-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.timing-total { + font-size: 0.8rem; + color: var(--primary-color); +} + +.timing-bar { + display: flex; + height: 8px; + border-radius: 4px; + overflow: hidden; + margin: 4px 0; + gap: 1px; +} + +.timing-seg { + min-width: 2px; + transition: flex 0.3s ease; +} + +.timing-extract { background: #4CAF50; } +.timing-map { background: #FF9800; } +.timing-smooth { background: #2196F3; } +.timing-send { background: #E91E63; } + +.timing-legend { + display: flex; + flex-wrap: wrap; + gap: 8px; + font-size: 0.75rem; + color: #999; + margin-top: 4px; +} + +.timing-legend-item { + display: flex; + align-items: center; + gap: 3px; +} + +.timing-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 2px; +} + +.timing-dot.timing-extract { background: #4CAF50; } +.timing-dot.timing-map { background: #FF9800; } +.timing-dot.timing-smooth { background: #2196F3; } +.timing-dot.timing-send { background: #E91E63; } + +@media (max-width: 768px) { + .displays-grid, + .devices-grid { + grid-template-columns: 1fr; + } +} diff --git a/server/src/wled_controller/static/css/components.css b/server/src/wled_controller/static/css/components.css new file mode 100644 index 0000000..be15d1e --- /dev/null +++ b/server/src/wled_controller/static/css/components.css @@ -0,0 +1,356 @@ +.badge { + padding: 4px 12px; + border-radius: 12px; + font-size: 0.85rem; + font-weight: 600; +} + +.badge.processing { + background: var(--primary-color); + color: white; +} + +.badge.idle { + background: var(--warning-color); + color: white; +} + +.badge.error { + background: var(--danger-color); + color: white; +} + +.card-content { + margin-bottom: 15px; +} + +.info-row { + display: flex; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid var(--border-color); +} + +.info-row:last-child { + border-bottom: none; +} + +.info-label { + color: #999; +} + +.info-value { + font-weight: 600; +} + +.card-actions { + display: flex; + gap: 8px; + margin-top: auto; + padding-top: 12px; + border-top: 1px solid var(--border-color); + align-items: center; +} + +.btn { + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 600; + transition: opacity 0.2s; + flex: 1 1 auto; + min-width: 100px; +} + +.btn:hover { + opacity: 0.9; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: var(--primary-color); + color: white; +} + +.btn-danger { + background: var(--danger-color); + color: white; +} + +.btn-secondary { + background: var(--border-color); + color: var(--text-color); +} + +.btn-icon { + min-width: auto; + padding: 8px 12px; + font-size: 1.2rem; + flex: 0 0 auto; +} + +.btn-icon:hover { + transform: scale(1.1); + opacity: 1; +} + +.form-group { + margin-bottom: 15px; +} + +.settings-toggle-group { + display: flex; + flex-direction: column; +} + +.settings-toggle { + position: relative; + display: inline-block; + width: 34px; + height: 18px; + cursor: pointer; + margin-top: 4px; +} + +.settings-toggle input { + opacity: 0; + width: 0; + height: 0; +} + +.settings-toggle-slider { + position: absolute; + inset: 0; + background: var(--border-color); + border-radius: 9px; + transition: background 0.2s; +} + +.settings-toggle-slider::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 14px; + height: 14px; + background: white; + border-radius: 50%; + transition: transform 0.2s; +} + +.settings-toggle input:checked + .settings-toggle-slider { + background: var(--primary-color); +} + +.settings-toggle input:checked + .settings-toggle-slider::after { + transform: translateX(16px); +} + +label { + display: block; + margin-bottom: 5px; + color: #999; + font-weight: 500; +} + +input[type="text"], +input[type="url"], +input[type="number"], +input[type="password"], +select { + width: 100%; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--bg-color); + color: var(--text-color); + font-size: 1rem; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + transition: border-color 0.2s, box-shadow 0.2s, opacity 0.2s; +} + +input[type="number"]:disabled, +input[type="password"]:disabled, +select:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +input[type="range"] { + width: 100%; + margin: 8px 0; +} + +/* Better password field appearance */ +input[type="password"] { + letter-spacing: 0.15em; +} + +input:focus, +select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); +} + +/* Remove browser autofill styling */ +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus { + -webkit-box-shadow: 0 0 0 1000px var(--bg-color) inset; + -webkit-text-fill-color: var(--text-color); + transition: background-color 5000s ease-in-out 0s; +} + +.loading { + text-align: center; + padding: 40px; + color: #999; +} + +.loading-spinner { + display: flex; + justify-content: center; + align-items: center; + padding: 40px; +} + +.loading-spinner::after { + content: ''; + width: 28px; + height: 28px; + border: 3px solid var(--border-color); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Full-page overlay spinner */ +.overlay-spinner { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 9999; + backdrop-filter: blur(4px); +} + +.overlay-spinner .progress-container { + position: relative; + width: 120px; + height: 120px; +} + +.overlay-spinner .progress-ring { + transform: rotate(-90deg); +} + +.overlay-spinner .progress-ring-circle { + transition: stroke-dashoffset 0.1s linear; + stroke: var(--primary-color); + stroke-width: 4; + fill: transparent; +} + +.overlay-spinner .progress-ring-bg { + stroke: rgba(255, 255, 255, 0.1); + stroke-width: 4; + fill: transparent; +} + +.overlay-spinner .progress-content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} + +.overlay-spinner .progress-percentage { + color: white; + font-size: 28px; + font-weight: 600; +} + +.overlay-spinner .spinner-text { + margin-top: 24px; + color: white; + font-size: 16px; + font-weight: 500; +} + +.overlay-spinner-close { + position: absolute; + top: 16px; + right: 16px; + background: none; + border: none; + color: rgba(255, 255, 255, 0.6); + font-size: 32px; + cursor: pointer; + line-height: 1; + padding: 4px 8px; + transition: color 0.15s; +} + +.overlay-spinner-close:hover { + color: white; +} + +.toast { + position: fixed; + bottom: 40px; + left: 50%; + transform: translateX(-50%) translateY(100px); + padding: 16px 24px; + border-radius: 8px; + color: white; + font-weight: 600; + font-size: 15px; + opacity: 0; + transition: opacity 0.3s, transform 0.3s; + z-index: 2001; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6); + min-width: 300px; + text-align: center; +} + +.toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); + animation: toastShake 0.5s ease-in-out; +} + +@keyframes toastShake { + 0%, 100% { transform: translateX(-50%) translateY(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-50%) translateY(-5px); } + 20%, 40%, 60%, 80% { transform: translateX(-50%) translateY(5px); } +} + +.toast.success { + background: var(--primary-color); +} + +.toast.error { + background: var(--danger-color); +} + +.toast.info { + background: var(--info-color); +} diff --git a/server/src/wled_controller/static/css/dashboard.css b/server/src/wled_controller/static/css/dashboard.css new file mode 100644 index 0000000..e1b9e32 --- /dev/null +++ b/server/src/wled_controller/static/css/dashboard.css @@ -0,0 +1,281 @@ +/* ── Dashboard ── */ + +.dashboard-section { + margin-bottom: 16px; +} + +.dashboard-section-header { + font-size: 0.8rem; + font-weight: 600; + margin-bottom: 6px; + color: var(--text-secondary); + display: flex; + align-items: center; + gap: 6px; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + user-select: none; +} + +.dashboard-section-chevron { + font-size: 0.6rem; + color: var(--text-secondary); + width: 10px; + display: inline-block; +} + +.dashboard-section-count { + background: var(--border-color); + color: var(--text-secondary); + border-radius: 10px; + padding: 0 6px; + font-size: 0.75rem; + font-weight: 600; +} + +.dashboard-subsection { + margin-bottom: 10px; + padding-left: 16px; +} +.dashboard-subsection .dashboard-section-header { + font-size: 0.72rem; +} + +.dashboard-stop-all { + margin-left: auto; + padding: 2px 8px; + font-size: 0.7rem; + white-space: nowrap; + flex: 0 0 auto; +} + +.dashboard-poll-wrap { + margin-left: auto; + display: flex; + align-items: center; + gap: 3px; +} + +.dashboard-poll-slider { + width: 48px; + height: 12px; + accent-color: var(--primary-color); + cursor: pointer; +} + +.dashboard-poll-value { + font-size: 0.6rem; + color: var(--text-secondary); + min-width: 18px; +} + +.dashboard-target { + display: grid; + grid-template-columns: 1fr auto auto; + align-items: center; + gap: 12px; + padding: 6px 12px; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 6px; + margin-bottom: 4px; +} + +.dashboard-target-info { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; + overflow: hidden; +} + +.dashboard-target-icon { + font-size: 1rem; + flex-shrink: 0; +} + +.dashboard-target-name { + font-size: 0.85rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: flex; + align-items: center; + gap: 4px; +} + +.dashboard-target-name .health-dot { + margin-right: 0; + flex-shrink: 0; +} + +.dashboard-target-subtitle { + font-size: 0.7rem; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.dashboard-target-metrics { + display: flex; + gap: 12px; + align-items: center; +} + +.dashboard-metric { + text-align: center; + min-width: 48px; +} + +.dashboard-metric-value { + font-size: 0.85rem; + font-weight: 700; + color: var(--primary-color); + line-height: 1.2; +} + +.dashboard-metric-label { + font-size: 0.6rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.dashboard-fps-metric { + display: flex; + align-items: center; + gap: 6px; + min-width: auto; +} + +.dashboard-fps-sparkline { + position: relative; + width: 100px; + height: 36px; +} + +.dashboard-fps-label { + display: flex; + flex-direction: column; + align-items: center; + min-width: 36px; + line-height: 1.1; +} + +.dashboard-fps-target { + font-weight: 400; + opacity: 0.5; + font-size: 0.75rem; +} + +.dashboard-target-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.dashboard-status-dot { + font-size: 1rem; + line-height: 1; +} + +.dashboard-status-dot.active { + color: #4CAF50; + animation: pulse 2s infinite; +} + +.dashboard-no-targets { + text-align: center; + padding: 32px 16px; + color: var(--text-secondary); + font-size: 0.9rem; +} + +.dashboard-badge-stopped { + padding: 2px 8px; + border-radius: 10px; + font-size: 0.7rem; + font-weight: 600; + background: var(--border-color); + color: var(--text-secondary); + flex-shrink: 0; +} + +.dashboard-badge-active { + padding: 2px 8px; + border-radius: 10px; + font-size: 0.7rem; + font-weight: 600; + background: var(--success-color, #28a745); + color: #fff; + flex-shrink: 0; +} + +.dashboard-profile .dashboard-target-metrics { + min-width: 48px; +} + +@media (max-width: 768px) { + .dashboard-target { + grid-template-columns: 1fr auto; + gap: 6px; + } + + .dashboard-target-metrics { + display: none; + } +} + +/* ===== PERFORMANCE CHARTS ===== */ + +.perf-charts-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 12px; +} + +.perf-chart-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 10px 12px; +} + +.perf-chart-wrap { + position: relative; + height: 60px; +} + +.perf-chart-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; +} + +.perf-chart-label { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.3px; + color: var(--text-secondary); +} + +.perf-chart-value { + font-size: 0.85rem; + font-weight: 700; +} + +.perf-chart-value.cpu { color: #2196F3; } +.perf-chart-value.ram { color: #4CAF50; } +.perf-chart-value.gpu { color: #FF9800; } + +.perf-chart-unavailable { + text-align: center; + padding: 20px 0; + color: var(--text-secondary); + font-size: 0.8rem; +} diff --git a/server/src/wled_controller/static/css/layout.css b/server/src/wled_controller/static/css/layout.css new file mode 100644 index 0000000..bbfcbfb --- /dev/null +++ b/server/src/wled_controller/static/css/layout.css @@ -0,0 +1,209 @@ +header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 0 10px; + margin-bottom: 10px; + position: relative; + z-index: 2100; +} + +.header-title { + display: flex; + align-items: center; + gap: 10px; +} + +h1 { + font-size: 2rem; + color: var(--primary-color); +} + +h2 { + margin-bottom: 20px; + color: var(--text-color); + font-size: 1.5rem; +} + +.server-info { + display: flex; + align-items: center; + gap: 15px; +} + +.header-link { + color: var(--text-secondary); + text-decoration: none; + font-size: 0.85rem; + font-weight: 500; + padding: 4px 8px; + border-radius: 4px; + transition: color 0.2s, background 0.2s; +} + +.header-link:hover { + color: var(--text-color); + background: var(--bg-secondary); +} + +#server-version { + font-size: 0.75rem; + font-weight: 400; + color: var(--text-secondary); + background: var(--border-color); + padding: 2px 8px; + border-radius: 10px; + letter-spacing: 0.03em; +} + +.status-badge { + font-size: 1.5rem; + animation: pulse 2s infinite; +} + +.status-badge.online { + color: var(--primary-color); +} + +.status-badge.offline { + color: var(--danger-color); +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* WLED device health indicator */ +.health-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 8px; + vertical-align: middle; + flex-shrink: 0; +} + +.health-dot.health-online { + background-color: #4CAF50; + box-shadow: 0 0 6px rgba(76, 175, 80, 0.6); +} + +.health-dot.health-offline { + background-color: var(--danger-color); + box-shadow: 0 0 6px rgba(244, 67, 54, 0.6); +} + +.health-dot.health-unknown { + background-color: #9E9E9E; + animation: pulse 2s infinite; +} + +.health-latency { + font-size: 0.7rem; + font-weight: 400; + color: #4CAF50; + margin-left: auto; + padding-left: 8px; + opacity: 0.85; +} + +.health-latency.offline { + color: var(--danger-color); +} + +/* Tabs */ +.tab-bar { + display: flex; + align-items: center; + gap: 4px; + border-bottom: 2px solid var(--border-color); + margin-bottom: 16px; +} + +.tab-btn { + background: none; + border: none; + padding: 10px 18px; + font-size: 1rem; + font-weight: 500; + color: var(--text-secondary); + cursor: pointer; + border-bottom: 2px solid transparent; + margin-bottom: -2px; + transition: color 0.2s, border-color 0.2s; +} + +.tab-btn:hover { + color: var(--text-color); +} + +.tab-btn.active { + color: var(--primary-color); + border-bottom-color: var(--primary-color); +} + +.tab-panel { + display: none; +} + +.tab-panel.active { + display: block; +} + +/* Theme Toggle */ +.theme-toggle { + background: var(--card-bg); + border: 1px solid var(--border-color); + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 1.2rem; + transition: transform 0.2s; + margin-left: 10px; +} + +.theme-toggle:hover { + transform: scale(1.1); +} + +/* Footer */ +.app-footer { + margin-top: 20px; + padding: 15px 0; + text-align: center; +} + +.footer-content { + color: var(--text-secondary); + font-size: 0.9rem; +} + +.footer-content p { + margin: 0; +} + + +.footer-content strong { + color: var(--text-color); +} + +.footer-content a { + color: var(--primary-color); + text-decoration: none; + transition: opacity 0.2s; +} + +.footer-content a:hover { + opacity: 0.8; + text-decoration: underline; +} + +@media (max-width: 768px) { + header { + flex-direction: column; + gap: 15px; + text-align: center; + } +} diff --git a/server/src/wled_controller/static/css/modal.css b/server/src/wled_controller/static/css/modal.css new file mode 100644 index 0000000..5156752 --- /dev/null +++ b/server/src/wled_controller/static/css/modal.css @@ -0,0 +1,450 @@ +/* Modal Styles */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + z-index: 2000; + align-items: center; + justify-content: center; + animation: fadeIn 0.2s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.modal-content { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + max-width: 500px; + width: 90%; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + animation: slideUp 0.3s ease-out; +} + +#template-modal .modal-content { + max-width: 500px !important; + width: 100% !important; +} + +#test-template-modal .modal-content { + max-width: 420px; +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.modal-header { + padding: 24px 24px 16px 24px; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; +} + +.modal-header h2 { + margin: 0; + font-size: 1.5rem; + color: var(--text-color); +} + +.modal-header-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.modal-header-btn { + background: none; + border: none; + font-size: 1.1rem; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 4px; + transition: background 0.2s; + flex-shrink: 0; +} +.modal-header-btn:hover { + background: rgba(128, 128, 128, 0.15); +} +.modal-header-btn:disabled { + opacity: 0.4; + cursor: default; +} + +.modal-close-btn { + background: none; + border: none; + color: #777; + font-size: 1.2rem; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 4px; + transition: color 0.2s, background 0.2s; + flex-shrink: 0; +} + +.modal-close-btn:hover { + color: var(--text-color); + background: rgba(128, 128, 128, 0.15); +} + +.modal-body { + padding: 24px; +} + +.modal-description { + color: #999; + margin-bottom: 20px; + line-height: 1.6; +} + +.password-input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.password-input-wrapper input { + flex: 1; + padding-right: 45px; +} + +.password-toggle { + position: absolute; + right: 8px; + background: none; + border: none; + cursor: pointer; + font-size: 1.2rem; + padding: 8px; + color: var(--text-color); + opacity: 0.6; + transition: opacity 0.2s; +} + +.password-toggle:hover { + opacity: 1; +} + +.label-row { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 5px; +} + +.label-row label { + margin-bottom: 0; +} + +.hint-toggle { + background: none; + border: 1px solid var(--border-color); + border-radius: 50%; + width: 18px; + height: 18px; + font-size: 0.7rem; + line-height: 1; + color: var(--text-secondary, #888); + cursor: pointer; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + opacity: 0.6; + transition: opacity 0.2s; + flex-shrink: 0; +} + +.hint-toggle:hover { + opacity: 1; +} + +.hint-toggle.active { + opacity: 1; + color: var(--primary-color, #4CAF50); + border-color: var(--primary-color, #4CAF50); +} + +.input-hint { + display: block; + margin: 0 0 6px 0; + color: #666; + font-size: 0.85rem; +} + +.fps-hint { + display: block; + margin-top: 4px; + font-size: 0.82rem; + color: var(--info-color, #2196F3); +} + +.slider-row { + display: flex; + align-items: center; + gap: 12px; +} + +.slider-row input[type="range"] { + flex: 1; +} + +.slider-value { + min-width: 28px; + text-align: center; + font-weight: 600; + font-size: 0.95rem; + color: var(--text-primary); +} + +.error-message { + background: rgba(244, 67, 54, 0.1); + border: 1px solid var(--danger-color); + color: var(--danger-color); + padding: 12px; + border-radius: 4px; + margin-top: 15px; + font-size: 0.9rem; +} + +.modal-footer { + padding: 16px 24px 24px 24px; + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.modal-footer .btn-icon { + min-width: 60px; + padding: 10px 20px; + font-size: 1.4rem; +} + +.btn-sm { + padding: 4px 10px; + font-size: 0.8rem; + min-width: auto; +} + +.btn-display-picker { + width: 100%; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--bg-color); + color: var(--text-color); + font-size: 1rem; + cursor: pointer; + text-align: left; + transition: border-color 0.2s, box-shadow 0.2s; + font-weight: 400; +} + +.btn-display-picker:hover { + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.15); +} + +.modal-content-wide { + width: fit-content; + min-width: 500px; + max-width: calc(100vw - 40px); + max-height: calc(100vh - 40px); + display: flex; + flex-direction: column; +} + +.modal-content-wide .modal-body { + overflow-y: auto; + scrollbar-gutter: stable; + flex: 1 1 auto; + min-height: 0; +} + +@media (max-width: 768px) { + .modal-content { + width: 95%; + margin: 20px; + } + + .modal-footer .btn { + min-width: 80px; + } +} + +/* Image Lightbox */ +.lightbox { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.92); + z-index: 10000; + justify-content: center; + align-items: center; + cursor: zoom-out; +} + +.lightbox.active { + display: flex; +} + +.lightbox-content { + position: relative; + max-width: 95%; + max-height: 95%; + cursor: default; +} + +.lightbox-content img { + max-width: 100%; + max-height: 90vh; + object-fit: contain; + border-radius: 4px; + display: block; +} + +.lightbox-close { + position: absolute; + top: 16px; + right: 16px; + background: rgba(255, 255, 255, 0.15); + border: none; + color: white; + font-size: 1.5rem; + width: 40px; + height: 40px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; + z-index: 1; +} + +.lightbox-close:hover { + background: rgba(255, 255, 255, 0.3); +} + +.lightbox-refresh-btn { + position: absolute; + top: 16px; + right: 64px; + background: rgba(255, 255, 255, 0.15); + border: none; + color: white; + font-size: 1.2rem; + width: 40px; + height: 40px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; + z-index: 1; +} + +.lightbox-refresh-btn:hover { + background: rgba(255, 255, 255, 0.3); +} + +.lightbox-refresh-btn.active { + background: var(--primary-color); +} + +.lightbox-stats { + position: absolute; + bottom: 8px; + left: 8px; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(8px); + color: white; + padding: 8px 14px; + border-radius: 6px; + font-size: 0.8rem; + display: flex; + gap: 16px; + flex-wrap: wrap; +} + +.lightbox-stats .stat-item { + display: flex; + gap: 4px; + align-items: center; +} + +.lightbox-stats .stat-item span { + opacity: 0.7; +} + +.lightbox-stats .stat-item strong { + font-weight: 600; +} + +/* Display Picker Lightbox */ +.display-picker-content { + max-width: 900px; + width: 90%; + text-align: center; +} + +.display-picker-title { + color: white; + font-size: 1.3rem; + margin-bottom: 20px; + font-weight: 500; +} + +.display-picker-canvas { + background: rgba(255, 255, 255, 0.05); + border: 2px dashed rgba(255, 255, 255, 0.15); + border-radius: 8px; + padding: 24px; + width: 100%; + box-sizing: border-box; +} + +.layout-display-pickable { + cursor: pointer !important; + border: 2px solid var(--border-color) !important; + background: linear-gradient(135deg, rgba(128, 128, 128, 0.08), rgba(128, 128, 128, 0.03)) !important; +} + +.layout-display-pickable:hover { + box-shadow: 0 0 20px rgba(76, 175, 80, 0.4); + border-color: var(--primary-color) !important; +} + +.layout-display-pickable.selected { + border-color: var(--primary-color) !important; + box-shadow: 0 0 16px rgba(76, 175, 80, 0.5); + background: rgba(76, 175, 80, 0.12) !important; +} diff --git a/server/src/wled_controller/static/css/patterns.css b/server/src/wled_controller/static/css/patterns.css new file mode 100644 index 0000000..7ddbb5a --- /dev/null +++ b/server/src/wled_controller/static/css/patterns.css @@ -0,0 +1,337 @@ +/* Static image stream styles */ +.image-preview-container { + text-align: center; + margin: 12px 0; + padding: 12px; + background: var(--bg-secondary); + border-radius: 8px; + border: 1px solid var(--border-color); +} +.stream-image-preview { + max-width: 100%; + max-height: 200px; + border-radius: 4px; + border: 1px solid var(--border-color); +} +.stream-image-info { + font-size: 0.75rem; + color: var(--text-muted); + margin-top: 6px; +} +.validation-status { + font-size: 0.8rem; + margin-top: 4px; + padding: 4px 8px; + border-radius: 4px; +} +.validation-status.success { + color: #4caf50; +} +.validation-status.error { + color: #f44336; +} +.validation-status.loading { + color: var(--text-muted); +} +.stream-card-props { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 8px; +} + +.stream-card-prop { + display: inline-block; + font-size: 0.75rem; + color: var(--text-secondary); + background: var(--border-color); + padding: 2px 8px; + border-radius: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 180px; + vertical-align: middle; +} + +.stream-card-prop-full { + max-width: 100%; + word-break: break-all; + white-space: normal; + font-size: 0.7rem; +} + +/* Key Colors target styles */ +.kc-rect-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 8px; +} + +.kc-rect-row { + display: flex; + align-items: center; + gap: 6px; + padding: 8px; + background: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: 6px; +} + +.kc-rect-row input[type="text"] { + flex: 2; + min-width: 0; +} + +.kc-rect-row input[type="number"] { + flex: 1; + min-width: 0; + width: 60px; +} + +.kc-rect-row .kc-rect-remove-btn { + background: none; + border: none; + color: #777; + font-size: 1rem; + cursor: pointer; + padding: 4px; + border-radius: 4px; + flex-shrink: 0; + transition: color 0.2s, background 0.2s; +} + +.kc-rect-row .kc-rect-remove-btn:hover { + color: var(--danger-color); + background: rgba(244, 67, 54, 0.1); +} + +.kc-rect-labels { + display: flex; + gap: 6px; + padding: 0 8px; + margin-bottom: 4px; + font-size: 0.7rem; + color: var(--text-secondary); + font-weight: 600; +} + +.kc-rect-labels span:first-child { + flex: 2; +} + +.kc-rect-labels span { + flex: 1; + text-align: center; +} + +.kc-rect-labels span:last-child { + width: 28px; + flex: 0 0 28px; +} + +.kc-add-rect-btn { + width: 100%; + font-size: 0.85rem; + padding: 6px 12px; +} + +.kc-rect-empty { + text-align: center; + color: var(--text-secondary); + font-size: 0.85rem; + padding: 12px; + font-style: italic; +} + +.kc-color-swatches { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 8px; +} + +.kc-swatch { + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; +} + +.kc-swatch-color { + width: 32px; + height: 32px; + border-radius: 6px; + border: 2px solid var(--border-color); + transition: background-color 0.3s; +} + +.kc-swatch-label { + font-size: 0.6rem; + color: var(--text-secondary); + max-width: 40px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: center; +} + +.kc-no-colors { + color: var(--text-secondary); + font-size: 0.8rem; + font-style: italic; + padding: 4px 0; +} + +/* Pattern Template Visual Editor */ +.pattern-canvas-container { + position: relative; + border-radius: 8px; + overflow: hidden; + background: var(--bg-color); + border: 1px solid var(--border-color); + margin-bottom: 12px; + resize: both; + width: 820px; + min-width: 400px; + max-width: 100%; + min-height: 200px; + height: 450px; + max-height: calc(100vh - 400px); +} + +#pattern-canvas { + width: 100%; + height: 100%; + display: block; + cursor: default; +} + +.pattern-canvas-toolbar { + display: flex; + gap: 2px; + padding: 4px; + align-items: center; + background: var(--bg-secondary); + border-radius: 6px; + margin-top: 4px; +} + +.pattern-canvas-toolbar .btn { + flex: 0 0 auto; + min-width: 32px; + width: 32px; + height: 32px; + padding: 0; + font-size: 1rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; +} + +.pattern-bg-row { + display: flex; + gap: 0.5rem; + align-items: center; + margin-bottom: 8px; +} + +.pattern-bg-row select { + flex: 1; +} + +.pattern-capture-btn { + flex: 0 0 auto; + min-width: 36px !important; + width: 36px; + height: 36px; + padding: 0 !important; + font-size: 1.1rem; + line-height: 36px; + text-align: center; +} + +.pattern-rect-list { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 8px; + max-height: 200px; + overflow-y: auto; +} + +.pattern-rect-row { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + background: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 0.85rem; + cursor: pointer; + transition: border-color 0.15s; +} + +.pattern-rect-row.selected { + border-color: var(--primary-color); + background: rgba(76, 175, 80, 0.08); +} + +.pattern-rect-row input[type="text"] { + flex: 2; + min-width: 0; + padding: 4px 6px; + font-size: 0.8rem; +} + +.pattern-rect-row input[type="number"] { + flex: 1; + min-width: 0; + width: 55px; + padding: 4px 6px; + font-size: 0.8rem; +} + +.pattern-rect-row .pattern-rect-remove-btn { + background: none; + border: none; + color: #777; + font-size: 0.9rem; + cursor: pointer; + padding: 2px 4px; + border-radius: 4px; + flex-shrink: 0; + transition: color 0.2s, background 0.2s; +} + +.pattern-rect-row .pattern-rect-remove-btn:hover { + color: var(--danger-color); + background: rgba(244, 67, 54, 0.1); +} + +.pattern-rect-labels { + display: flex; + gap: 6px; + padding: 0 8px; + margin-bottom: 2px; + font-size: 0.7rem; + color: var(--text-secondary); + font-weight: 600; +} + +.pattern-rect-labels span:first-child { + flex: 2; +} + +.pattern-rect-labels span { + flex: 1; + text-align: center; +} + +.pattern-rect-labels span:last-child { + width: 24px; + flex: 0 0 24px; +} diff --git a/server/src/wled_controller/static/css/profiles.css b/server/src/wled_controller/static/css/profiles.css new file mode 100644 index 0000000..491d190 --- /dev/null +++ b/server/src/wled_controller/static/css/profiles.css @@ -0,0 +1,185 @@ +/* ===== PROFILES ===== */ + +.badge-profile-active { + background: var(--success-color, #28a745); + color: #fff; +} + +.badge-profile-inactive { + background: var(--border-color); + color: var(--text-color); +} + +.badge-profile-disabled { + background: var(--border-color); + color: var(--text-muted); + opacity: 0.7; +} + +.profile-status-disabled { + opacity: 0.6; +} + +.profile-logic-label { + font-size: 0.7rem; + font-weight: 600; + color: var(--text-muted); + padding: 0 4px; +} + +/* Profile condition editor rows */ +.profile-condition-row { + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 10px; + margin-bottom: 8px; + background: var(--bg-secondary, var(--bg-color)); +} + +.condition-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.condition-type-label { + font-weight: 600; + font-size: 0.9rem; +} + +.btn-remove-condition { + background: none; + border: none; + color: var(--danger-color, #dc3545); + cursor: pointer; + font-size: 1rem; + padding: 2px 6px; +} + +.condition-fields { + display: flex; + flex-direction: column; + gap: 8px; +} + +.condition-field label { + display: block; + font-size: 0.85rem; + margin-bottom: 3px; + color: var(--text-muted); +} + +.condition-field select, +.condition-field textarea { + width: 100%; + padding: 6px 8px; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--bg-color); + color: var(--text-color); + font-size: 0.9rem; + font-family: inherit; +} + +.condition-apps { + resize: vertical; + min-height: 60px; +} + +.condition-apps-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.btn-browse-apps { + background: none; + border: 1px solid var(--border-color); + color: var(--text-color); + font-size: 0.75rem; + padding: 2px 8px; + border-radius: 4px; + cursor: pointer; + transition: border-color 0.2s, background 0.2s; +} + +.btn-browse-apps:hover { + border-color: var(--primary-color); + background: rgba(33, 150, 243, 0.1); +} + +.process-picker { + margin-top: 6px; + border: 1px solid var(--border-color); + border-radius: 4px; + overflow: hidden; +} + +.process-picker-search { + width: 100%; + padding: 6px 8px; + border: none; + border-bottom: 1px solid var(--border-color); + background: var(--bg-color); + color: var(--text-color); + font-size: 0.85rem; + font-family: inherit; + outline: none; + box-sizing: border-box; +} + +.process-picker-list { + max-height: 160px; + overflow-y: auto; +} + +.process-picker-item { + padding: 4px 8px; + font-size: 0.8rem; + cursor: pointer; + transition: background 0.1s; +} + +.process-picker-item:hover { + background: rgba(33, 150, 243, 0.15); +} + +.process-picker-item.added { + color: var(--text-muted); + cursor: default; + opacity: 0.6; +} + +.process-picker-loading { + padding: 8px; + font-size: 0.8rem; + color: var(--text-muted); + text-align: center; +} + +/* Profile target checklist */ +.profile-targets-checklist { + max-height: 200px; + overflow-y: auto; + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 6px; +} + +.profile-target-item { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 6px; + cursor: pointer; + border-radius: 3px; +} + +.profile-target-item:hover { + background: var(--bg-secondary, var(--bg-color)); +} + +.profile-target-item input[type="checkbox"] { + margin: 0; +} diff --git a/server/src/wled_controller/static/css/streams.css b/server/src/wled_controller/static/css/streams.css new file mode 100644 index 0000000..7b90a32 --- /dev/null +++ b/server/src/wled_controller/static/css/streams.css @@ -0,0 +1,611 @@ +/* =========================== + Capture Templates Styles + =========================== */ + +.templates-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 20px; +} + +.template-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; + transition: box-shadow 0.2s; + display: flex; + flex-direction: column; + position: relative; +} + +.template-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.add-template-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 180px; + cursor: pointer; + border: 2px dashed var(--border-color); + background: transparent; + transition: border-color 0.2s, background 0.2s; +} + +.add-template-card:hover { + border-color: var(--primary-color); + background: rgba(33, 150, 243, 0.05); + box-shadow: none; +} + +.add-template-icon { + font-size: 2.5rem; + font-weight: 300; + color: var(--text-secondary); + line-height: 1; + transition: color 0.2s; +} + +.add-template-card:hover .add-template-icon { + color: var(--primary-color); +} + +.add-template-label { + font-size: 0.85rem; + color: var(--text-secondary); + margin-top: 8px; + font-weight: 500; +} + +.template-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.template-card .template-card-header { + padding-right: 24px; +} + +.template-name { + font-size: 18px; + font-weight: bold; + color: var(--text-color); +} + +.badge { + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: bold; + text-transform: uppercase; +} + +.template-description { + color: var(--text-secondary); + font-size: 14px; + margin-bottom: 12px; + line-height: 1.4; +} + +.template-config { + font-size: 14px; + color: var(--text-secondary); + margin-bottom: 8px; +} + +.filter-chain { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 8px; +} + +.filter-chain-item { + font-size: 0.7rem; + background: var(--border-color); + color: var(--text-secondary); + padding: 2px 8px; + border-radius: 10px; +} + +.filter-chain-arrow { + font-size: 0.7rem; + color: var(--text-muted); +} + +.template-config-details { + margin: 12px 0; + font-size: 13px; +} + +.template-config-details summary { + cursor: pointer; + color: var(--primary-color); + font-weight: 500; + padding: 4px 0; +} + +.template-config-details summary:hover { + text-decoration: underline; +} + +.template-no-config { + margin: 12px 0; + font-size: 13px; + color: var(--primary-color); + font-weight: 500; + padding: 4px 0; +} + +.config-table { + width: 100%; + margin-top: 8px; + border-collapse: collapse; + font-size: 13px; +} + +.config-table td { + padding: 4px 8px; + border-bottom: 1px solid var(--border-color); +} + +.config-table tr:last-child td { + border-bottom: none; +} + +.config-key { + color: var(--text-secondary); + white-space: nowrap; + width: 1%; +} + +.config-value { + color: var(--text-primary); + font-family: monospace; +} + +/* Engine config grid (property name left, input right) */ +.config-grid { + display: grid; + grid-template-columns: auto 1fr; + gap: 8px 12px; + align-items: center; + margin-top: 12px; +} + +.config-grid-label { + font-size: 0.85rem; + color: var(--text-secondary); + white-space: nowrap; + text-align: right; +} + +.config-grid-value input, +.config-grid-value select { + width: 100%; + padding: 6px 10px; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--bg-color); + color: var(--text-color); + font-size: 0.85rem; +} + +.template-card-actions { + display: flex; + gap: 8px; + margin-top: auto; + padding-top: 12px; + border-top: 1px solid var(--border-color); + align-items: center; +} + +.template-card-actions .btn:not(.btn-icon) { + flex: 1; +} + +.template-card-actions .btn-icon { + flex-shrink: 0; +} + +.text-muted { + color: var(--text-secondary); + font-style: italic; + font-size: 13px; +} + +/* PP Filter List in Template Modal */ +.pp-filter-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 12px; +} + +.pp-filter-empty { + color: var(--text-secondary); + font-size: 13px; + text-align: center; + padding: 16px; + border: 1px dashed var(--border-color); + border-radius: 8px; +} + +.pp-filter-card { + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--bg-secondary); + padding: 10px 12px; +} + +.pp-filter-card-header { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + user-select: none; +} + +.pp-filter-card.expanded .pp-filter-card-header { + margin-bottom: 8px; +} + +.pp-filter-card-chevron { + font-size: 10px; + color: var(--text-secondary); + flex-shrink: 0; + width: 12px; +} + +.pp-filter-card-name { + font-weight: 600; + font-size: 14px; + color: var(--text-primary); +} + +.pp-filter-card-summary { + color: var(--text-secondary); + font-size: 12px; + margin-right: 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.pp-filter-card-actions { + display: flex; + gap: 4px; + flex-shrink: 0; + margin-left: auto; +} + +.btn-filter-action { + background: none; + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-secondary); + cursor: pointer; + width: 26px; + height: 26px; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + padding: 0; +} + +.btn-filter-action:hover:not(:disabled) { + background: var(--border-color); + color: var(--text-primary); +} + +.btn-filter-action:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.btn-filter-remove:hover:not(:disabled) { + background: rgba(239, 68, 68, 0.15); + border-color: rgba(239, 68, 68, 0.4); + color: #ef4444; +} + +.pp-filter-card-options { + display: flex; + flex-direction: column; + gap: 4px; +} + +.pp-filter-option { + display: flex; + flex-direction: column; + gap: 2px; +} + +.pp-filter-option label { + display: flex; + justify-content: space-between; + font-size: 12px; + color: var(--text-secondary); +} + +.pp-filter-option input[type="range"] { + width: 100%; +} + +.pp-filter-option-bool label { + justify-content: space-between; + gap: 8px; + align-items: center; + cursor: pointer; + padding: 4px 0; +} + +.pp-filter-option-bool input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + width: 34px; + min-width: 34px; + height: 18px; + background: var(--border-color); + border-radius: 9px; + position: relative; + cursor: pointer; + transition: background 0.2s; + order: 1; + margin: 0; +} + +.pp-filter-option-bool input[type="checkbox"]::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 14px; + height: 14px; + background: white; + border-radius: 50%; + transition: transform 0.2s; +} + +.pp-filter-option-bool input[type="checkbox"]:checked { + background: var(--primary-color); +} + +.pp-filter-option-bool input[type="checkbox"]:checked::after { + transform: translateX(16px); +} + +.pp-filter-option-bool span { + order: 0; +} + +.pp-add-filter-row { + display: flex; + gap: 8px; + align-items: center; + margin-bottom: 4px; +} + +.pp-add-filter-select { + flex: 1; + padding: 6px 10px; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--card-bg); + color: var(--text-primary); + font-size: 13px; +} + +.pp-add-filter-btn { + width: 34px; + height: 34px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--card-bg); + color: var(--text-primary); + font-size: 20px; + cursor: pointer; + padding: 0; + line-height: 1; +} + +.pp-add-filter-btn:hover { + background: var(--border-color); +} + +/* Template Test Section */ +.template-test-section { + background: var(--bg-secondary); + border-radius: 8px; + padding: 16px; +} + +.template-test-section h3 { + margin-top: 0; + margin-bottom: 12px; + font-size: 16px; +} + +.test-results-container { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; +} + +.test-preview-section, +.test-performance-section { + margin-bottom: 20px; +} + +.test-preview-section:last-child, +.test-performance-section:last-child { + margin-bottom: 0; +} + +.test-preview-section h4, +.test-performance-section h4 { + margin-top: 0; + margin-bottom: 12px; + font-size: 14px; + font-weight: 600; + color: var(--text-secondary); +} + +.test-preview-image { + border-radius: 4px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.test-preview-image img { + display: block; + width: 100%; + height: auto; +} + +.test-performance-stats { + display: flex; + flex-direction: column; + gap: 8px; +} + +.stat-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid var(--border-color); + font-size: 14px; +} + +.stat-item:last-child { + border-bottom: none; +} + +.stat-item span { + color: var(--text-secondary); + font-weight: 500; +} + +.stat-item strong { + color: var(--text-color); + font-weight: 600; + font-family: monospace; +} + +/* Empty state */ +.empty-state { + text-align: center; + padding: 40px 20px; + color: var(--text-secondary); + font-size: 16px; +} + +/* Stream type badges */ +.badge-raw { + background: #1976d2; + color: white; +} + +.badge-processed { + background: #7b1fa2; + color: white; +} + +/* Stream info panel in stream selector modal */ +.stream-info-panel { + padding: 4px 0 0 0; + font-size: 14px; + line-height: 1.6; +} + +/* Stream sub-tabs */ +.stream-tab-bar { + display: flex; + align-items: center; + gap: 4px; + border-bottom: 2px solid var(--border-color); + margin-bottom: 16px; +} + +.stream-tab-btn { + background: none; + border: none; + padding: 8px 14px; + font-size: 0.9rem; + font-weight: 500; + color: var(--text-secondary); + cursor: pointer; + border-bottom: 2px solid transparent; + margin-bottom: -2px; + transition: color 0.2s, border-color 0.2s; +} + +.stream-tab-btn:hover { + color: var(--text-color); +} + +.stream-tab-btn.active { + color: var(--primary-color); + border-bottom-color: var(--primary-color); +} + +.stream-tab-count { + background: var(--border-color); + color: var(--text-secondary); + font-size: 0.7rem; + font-weight: 600; + padding: 1px 6px; + border-radius: 8px; + margin-left: 4px; +} + +.stream-tab-btn.active .stream-tab-count { + background: var(--primary-color); + color: #fff; +} + +.stream-tab-panel { + display: none; +} + +.stream-tab-panel.active { + display: block; +} + +/* Sub-tab content sections */ +.subtab-section { + margin-bottom: 24px; +} + +.subtab-section:last-child { + margin-bottom: 0; +} + +.subtab-section-header { + font-size: 1rem; + font-weight: 600; + color: var(--text-secondary); + margin: 0 0 12px 0; + padding-bottom: 8px; + border-bottom: 1px solid var(--border-color); +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .templates-grid { + grid-template-columns: 1fr; + } +} diff --git a/server/src/wled_controller/static/css/tutorials.css b/server/src/wled_controller/static/css/tutorials.css new file mode 100644 index 0000000..a22db99 --- /dev/null +++ b/server/src/wled_controller/static/css/tutorials.css @@ -0,0 +1,197 @@ +/* Tutorial System */ +.tutorial-trigger-btn { + width: 28px; + height: 28px; + border-radius: 50%; + border: 2px solid var(--primary-color); + background: transparent; + color: var(--primary-color); + font-size: 1rem; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + margin-left: auto; + margin-right: 8px; + flex-shrink: 0; +} + +.tutorial-trigger-btn:hover { + background: var(--primary-color); + color: white; +} + +#calibration-modal .modal-body { + position: relative; +} + +.tutorial-overlay { + display: none; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 100; + pointer-events: none; +} + +.tutorial-overlay.active { + display: block; + pointer-events: auto; +} + +.tutorial-backdrop { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + transition: clip-path 0.3s ease; +} + +.tutorial-ring { + position: absolute; + border: 2px solid var(--primary-color); + border-radius: 6px; + pointer-events: none; + transition: all 0.3s ease; + animation: tutorial-pulse 2s infinite; +} + +@keyframes tutorial-pulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.6); } + 50% { box-shadow: 0 0 0 6px rgba(76, 175, 80, 0); } +} + +.tutorial-tooltip { + position: absolute; + width: 260px; + background: var(--card-bg); + border: 2px solid var(--primary-color); + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + z-index: 102; + pointer-events: auto; + animation: tutorial-tooltip-in 0.25s ease-out; +} + +@keyframes tutorial-tooltip-in { + from { opacity: 0; transform: translateY(-8px); } + to { opacity: 1; transform: translateY(0); } +} + +.tutorial-tooltip-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + border-bottom: 1px solid var(--border-color); +} + +.tutorial-step-counter { + font-size: 0.8rem; + font-weight: 600; + color: var(--primary-color); +} + +.tutorial-close-btn { + background: none; + border: none; + color: #777; + font-size: 1.3rem; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 4px; + transition: color 0.2s, background 0.2s; + padding: 0; + line-height: 1; +} + +.tutorial-close-btn:hover { + color: var(--text-color); + background: rgba(128, 128, 128, 0.15); +} + +.tutorial-tooltip-text { + margin: 0; + padding: 12px; + line-height: 1.5; + color: var(--text-color); + font-size: 0.9rem; +} + +.tutorial-tooltip-nav { + display: flex; + gap: 6px; + padding: 8px 12px; + border-top: 1px solid var(--border-color); +} + +.tutorial-prev-btn, +.tutorial-next-btn { + flex: 1; + padding: 6px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + font-weight: 600; + transition: opacity 0.2s; +} + +.tutorial-prev-btn { + background: var(--border-color); + color: var(--text-color); +} + +.tutorial-next-btn { + background: var(--primary-color); + color: white; +} + +.tutorial-prev-btn:hover:not(:disabled), +.tutorial-next-btn:hover:not(:disabled) { + opacity: 0.85; +} + +.tutorial-prev-btn:disabled, +.tutorial-next-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.tutorial-target { + position: relative; + z-index: 101 !important; +} + +/* Fixed (viewport-level) tutorial overlay for device cards */ +.tutorial-overlay-fixed { + position: fixed; + z-index: 10000; +} + +.tutorial-overlay-fixed .tutorial-backdrop { + position: fixed; +} + +.tutorial-overlay-fixed .tutorial-ring { + position: fixed; +} + +.tutorial-overlay-fixed .tutorial-tooltip { + position: absolute; + z-index: 10002; + animation: none; + opacity: 1; +} + +/* target z-index for fixed overlay is set inline via JS (target is outside overlay DOM) */ diff --git a/server/src/wled_controller/static/index.html b/server/src/wled_controller/static/index.html deleted file mode 100644 index 2b6284d..0000000 --- a/server/src/wled_controller/static/index.html +++ /dev/null @@ -1,1259 +0,0 @@ - - - - - - LED Grab - - - - - -
-
-
- -

LED Grab

- -
-
- API - - - - -
-
- -
-
- - - - -
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- - -
- - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - -
-

-
- - -
-
-
- - - - - - - - - - - diff --git a/server/src/wled_controller/static/style.css b/server/src/wled_controller/static/style.css deleted file mode 100644 index 092d459..0000000 --- a/server/src/wled_controller/static/style.css +++ /dev/null @@ -1,3712 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -:root { - --primary-color: #4CAF50; - --danger-color: #f44336; - --warning-color: #ff9800; - --info-color: #2196F3; -} - -/* Dark theme (default) */ -[data-theme="dark"] { - --bg-color: #1a1a1a; - --card-bg: #2d2d2d; - --text-color: #e0e0e0; - --border-color: #404040; - --display-badge-bg: rgba(0, 0, 0, 0.4); - color-scheme: dark; -} - -/* Light theme */ -[data-theme="light"] { - --bg-color: #f5f5f5; - --card-bg: #ffffff; - --text-color: #333333; - --border-color: #e0e0e0; - --display-badge-bg: rgba(255, 255, 255, 0.85); - color-scheme: light; -} - -/* Default to dark theme */ -body { - background: var(--bg-color); - color: var(--text-color); -} - -html { - background: var(--bg-color); - overflow-y: scroll; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; - background: var(--bg-color); - color: var(--text-color); - line-height: 1.6; -} - -body.modal-open { - position: fixed; - width: 100%; -} - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 20px; -} - -header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px 0 10px; - margin-bottom: 10px; - position: relative; - z-index: 2100; -} - -.header-title { - display: flex; - align-items: center; - gap: 10px; -} - -h1 { - font-size: 2rem; - color: var(--primary-color); -} - -h2 { - margin-bottom: 20px; - color: var(--text-color); - font-size: 1.5rem; -} - -.server-info { - display: flex; - align-items: center; - gap: 15px; -} - -.header-link { - color: var(--text-secondary); - text-decoration: none; - font-size: 0.85rem; - font-weight: 500; - padding: 4px 8px; - border-radius: 4px; - transition: color 0.2s, background 0.2s; -} - -.header-link:hover { - color: var(--text-color); - background: var(--bg-secondary); -} - -#server-version { - font-size: 0.75rem; - font-weight: 400; - color: var(--text-secondary); - background: var(--border-color); - padding: 2px 8px; - border-radius: 10px; - letter-spacing: 0.03em; -} - -.status-badge { - font-size: 1.5rem; - animation: pulse 2s infinite; -} - -.status-badge.online { - color: var(--primary-color); -} - -.status-badge.offline { - color: var(--danger-color); -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -/* WLED device health indicator */ -.health-dot { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 50%; - margin-right: 8px; - vertical-align: middle; - flex-shrink: 0; -} - -.health-dot.health-online { - background-color: #4CAF50; - box-shadow: 0 0 6px rgba(76, 175, 80, 0.6); -} - -.health-dot.health-offline { - background-color: var(--danger-color); - box-shadow: 0 0 6px rgba(244, 67, 54, 0.6); -} - -.health-dot.health-unknown { - background-color: #9E9E9E; - animation: pulse 2s infinite; -} - -.health-latency { - font-size: 0.7rem; - font-weight: 400; - color: #4CAF50; - margin-left: auto; - padding-left: 8px; - opacity: 0.85; -} - -.health-latency.offline { - color: var(--danger-color); -} - -section { - margin-bottom: 40px; -} - -.displays-grid, -.devices-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 20px; -} - -.devices-grid > .loading, -.devices-grid > .loading-spinner { - grid-column: 1 / -1; -} - -.add-device-card { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - cursor: pointer; - border: 2px dashed var(--border-color); - background: transparent; - min-height: 160px; - transition: border-color 0.2s, background 0.2s; -} - -.add-device-card:hover { - border-color: var(--primary-color); - background: rgba(33, 150, 243, 0.05); -} - -.add-device-icon { - font-size: 2.5rem; - font-weight: 300; - color: var(--text-secondary); - line-height: 1; - transition: color 0.2s; -} - -.add-device-card:hover .add-device-icon { - color: var(--primary-color); -} - -.add-device-label { - font-size: 0.85rem; - color: var(--text-secondary); - margin-top: 8px; -} - -.card { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 12px 20px 20px; - position: relative; - transition: transform 0.2s, box-shadow 0.2s; - display: flex; - flex-direction: column; -} - -.card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); -} - -.card-tutorial-btn { - position: absolute; - bottom: 10px; - right: 10px; - background: none; - border: 1.5px solid var(--border-color); - color: var(--text-muted, #777); - font-size: 0.7rem; - font-weight: bold; - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - border-radius: 50%; - transition: color 0.2s, background 0.2s, border-color 0.2s; - padding: 0; -} - -.card-tutorial-btn:hover { - border-color: var(--primary-color); - color: var(--primary-color); -} - - -.card-top-actions { - position: absolute; - top: 8px; - right: 8px; - display: flex; - align-items: center; - gap: 2px; -} - -.card-top-actions .card-remove-btn { - position: static; -} - -.card-power-btn { - background: none; - border: none; - color: #777; - font-size: 1rem; - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - border-radius: 4px; - transition: color 0.2s, background 0.2s; -} - -.card-power-btn:hover { - color: var(--primary-color); - background: rgba(76, 175, 80, 0.1); -} - -.card-remove-btn { - position: absolute; - top: 10px; - right: 10px; - background: none; - border: none; - color: #777; - font-size: 1rem; - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - border-radius: 4px; - transition: color 0.2s, background 0.2s; -} - -.card-remove-btn:hover { - color: var(--danger-color); - background: rgba(244, 67, 54, 0.1); -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; - padding-right: 30px; -} - -.card-title { - font-size: 1.2rem; - font-weight: 600; - display: flex; - align-items: center; - gap: 6px; - flex-wrap: wrap; -} - -.device-url-badge { - display: inline-flex; - align-items: center; - gap: 4px; - font-size: 0.7rem; - font-weight: 400; - color: var(--text-secondary); - background: var(--border-color); - padding: 2px 8px; - border-radius: 10px; - letter-spacing: 0.03em; - font-family: monospace; - text-decoration: none; - transition: background 0.2s; -} - -.device-url-badge:hover { - background: var(--text-muted); -} - -.device-url-icon { - font-size: 0.6rem; -} - -.card-subtitle { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 15px; - flex-wrap: wrap; -} - -.card-meta { - font-size: 0.8rem; - color: #999; - display: inline-flex; - align-items: center; - gap: 4px; -} - -.device-type-badge { - font-size: 10px; - font-weight: 700; - padding: 1px 6px; - border-radius: 3px; - background: rgba(255, 255, 255, 0.15); - letter-spacing: 0.5px; -} - -/* Device discovery */ -.discovery-section { - margin-bottom: 12px; -} -.btn-block { - width: 100%; -} -.discovery-list { - max-height: 200px; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 4px; - margin-top: 8px; -} -.discovery-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 12px; - border: 1px solid var(--border-color); - border-radius: 4px; - background: var(--card-bg); - cursor: pointer; - transition: opacity 0.15s; -} -.discovery-item:not(.discovery-item--added):hover { - opacity: 0.8; -} -.discovery-item--added { - opacity: 0.5; - cursor: default; -} -.discovery-item-info { - display: flex; - flex-direction: column; - gap: 2px; -} -.discovery-badge { - font-size: 11px; - padding: 2px 8px; - border-radius: 10px; - background: var(--border-color); - white-space: nowrap; -} -.discovery-type-badge { - font-size: 10px; - padding: 1px 5px; - border-radius: 3px; - background: var(--primary-color); - color: #fff; - font-weight: 600; - vertical-align: middle; - margin-right: 2px; -} -.modal-divider { - border: none; - border-top: 1px solid var(--border-color); - margin: 12px 0; -} -.discovery-loading { - display: flex; - justify-content: center; - padding: 12px; -} -.discovery-spinner { - width: 20px; - height: 20px; - border: 2px solid var(--border-color); - border-top-color: var(--primary-color); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} -@keyframes spin { - to { transform: rotate(360deg); } -} - -.channel-indicator { - display: inline-flex; - gap: 2px; - align-items: center; -} - -.channel-indicator .ch { - width: 8px; - height: 10px; - border-radius: 1px; -} - -.badge { - padding: 4px 12px; - border-radius: 12px; - font-size: 0.85rem; - font-weight: 600; -} - -.badge.processing { - background: var(--primary-color); - color: white; -} - -.badge.idle { - background: var(--warning-color); - color: white; -} - -.badge.error { - background: var(--danger-color); - color: white; -} - -.card-content { - margin-bottom: 15px; -} - -.info-row { - display: flex; - justify-content: space-between; - padding: 8px 0; - border-bottom: 1px solid var(--border-color); -} - -.info-row:last-child { - border-bottom: none; -} - -.info-label { - color: #999; -} - -.info-value { - font-weight: 600; -} - -.card-actions { - display: flex; - gap: 8px; - margin-top: auto; - padding-top: 12px; - border-top: 1px solid var(--border-color); - align-items: center; -} - -.btn { - padding: 8px 16px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 0.9rem; - font-weight: 600; - transition: opacity 0.2s; - flex: 1 1 auto; - min-width: 100px; -} - -.btn:hover { - opacity: 0.9; -} - -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.btn-primary { - background: var(--primary-color); - color: white; -} - -.btn-danger { - background: var(--danger-color); - color: white; -} - -.btn-secondary { - background: var(--border-color); - color: var(--text-color); -} - -.btn-icon { - min-width: auto; - padding: 8px 12px; - font-size: 1.2rem; - flex: 0 0 auto; -} - -.btn-icon:hover { - transform: scale(1.1); - opacity: 1; -} - -.display-card { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 15px; -} - -.display-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 15px; -} - -.display-index { - font-size: 1.3rem; - font-weight: 700; - color: var(--info-color); -} - -.primary-star { - color: var(--primary-color); - font-size: 1.2rem; -} - -/* Tabs */ -.tab-bar { - display: flex; - align-items: center; - gap: 4px; - border-bottom: 2px solid var(--border-color); - margin-bottom: 16px; -} - -.tab-btn { - background: none; - border: none; - padding: 10px 18px; - font-size: 1rem; - font-weight: 500; - color: var(--text-secondary); - cursor: pointer; - border-bottom: 2px solid transparent; - margin-bottom: -2px; - transition: color 0.2s, border-color 0.2s; -} - -.tab-btn:hover { - color: var(--text-color); -} - -.tab-btn.active { - color: var(--primary-color); - border-bottom-color: var(--primary-color); -} - -.tab-panel { - display: none; -} - -.tab-panel.active { - display: block; -} - -/* Display Layout Visualization */ -.layout-container { - position: relative; - background: transparent; -} - -.layout-display { - position: absolute; - border: 3px solid; - border-radius: 8px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); - transition: box-shadow 0.2s, border-color 0.2s; -} - -.layout-display:hover { - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); - z-index: 10; -} - -.layout-display.primary { - border-color: var(--primary-color); - background: linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(76, 175, 80, 0.05)); -} - -.layout-display.secondary { - border-color: var(--border-color); - background: linear-gradient(135deg, rgba(128, 128, 128, 0.1), rgba(128, 128, 128, 0.05)); -} - -.layout-position-label { - position: absolute; - top: 4px; - left: 6px; - font-size: 0.7rem; - color: var(--text-secondary); -} - -.layout-index-label { - position: absolute; - bottom: 6px; - left: 6px; - font-size: 0.85rem; - font-weight: 600; - color: var(--text-color); - background: var(--display-badge-bg); - padding: 1px 6px; - border-radius: 4px; - letter-spacing: 0.5px; -} - -.layout-display-label { - text-align: center; - padding: 5px; - display: flex; - flex-direction: column; - gap: 2px; -} - -.layout-display-label strong { - font-size: 0.9rem; - color: var(--text-color); - font-weight: 600; -} - -.layout-display-label small { - font-size: 0.75rem; - color: var(--text-secondary); -} - -.primary-indicator { - position: absolute; - top: 2px; - right: 4px; - color: var(--primary-color); - font-size: 1.5rem; - line-height: 1; - text-shadow: 0 0 4px rgba(0, 0, 0, 0.4); -} - - -/* Card brightness slider */ -.brightness-control { - padding: 0; - margin-bottom: 12px; -} - -.brightness-slider { - width: 100%; -} - -.brightness-loading .brightness-slider { - opacity: 0.3; - pointer-events: none; -} - -/* Static color picker — inline in card-subtitle */ -.static-color-control { - display: inline-flex; - align-items: center; - gap: 4px; -} - -.static-color-picker { - width: 22px; - height: 18px; - padding: 0; - border: 1px solid var(--border-color); - border-radius: 3px; - cursor: pointer; - background: none; - vertical-align: middle; -} - -.btn-clear-color { - background: none; - border: none; - color: #777; - font-size: 0.75rem; - cursor: pointer; - padding: 0 2px; - line-height: 1; - border-radius: 3px; - transition: color 0.2s; -} - -.btn-clear-color:hover { - color: var(--danger-color); -} - -.section-header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 8px; -} - -.section-header h2 { - margin-bottom: 0; -} - -.section-tip { - font-size: 0.82rem; - color: var(--text-secondary); - margin: 0 0 15px 0; - line-height: 1.5; - padding: 8px 12px; - background: rgba(33, 150, 243, 0.08); - border-left: 3px solid var(--info-color, #2196F3); - border-radius: 0 6px 6px 0; -} - -.section-tip a { - color: var(--info-color, #2196F3); - text-decoration: underline; -} - -ul.section-tip { - list-style: disc; - padding-left: 28px; -} - -ul.section-tip li { - margin: 2px 0; -} - -.form-group { - margin-bottom: 15px; -} - -.settings-toggle-group { - display: flex; - flex-direction: column; -} - -.settings-toggle { - position: relative; - display: inline-block; - width: 34px; - height: 18px; - cursor: pointer; - margin-top: 4px; -} - -.settings-toggle input { - opacity: 0; - width: 0; - height: 0; -} - -.settings-toggle-slider { - position: absolute; - inset: 0; - background: var(--border-color); - border-radius: 9px; - transition: background 0.2s; -} - -.settings-toggle-slider::after { - content: ''; - position: absolute; - top: 2px; - left: 2px; - width: 14px; - height: 14px; - background: white; - border-radius: 50%; - transition: transform 0.2s; -} - -.settings-toggle input:checked + .settings-toggle-slider { - background: var(--primary-color); -} - -.settings-toggle input:checked + .settings-toggle-slider::after { - transform: translateX(16px); -} - -label { - display: block; - margin-bottom: 5px; - color: #999; - font-weight: 500; -} - -input[type="text"], -input[type="url"], -input[type="number"], -input[type="password"], -select { - width: 100%; - padding: 10px; - border: 1px solid var(--border-color); - border-radius: 4px; - background: var(--bg-color); - color: var(--text-color); - font-size: 1rem; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; - transition: border-color 0.2s, box-shadow 0.2s, opacity 0.2s; -} - -input[type="number"]:disabled, -input[type="password"]:disabled, -select:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -input[type="range"] { - width: 100%; - margin: 8px 0; -} - -/* Better password field appearance */ -input[type="password"] { - letter-spacing: 0.15em; -} - -input:focus, -select:focus { - outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); -} - -/* Remove browser autofill styling */ -input:-webkit-autofill, -input:-webkit-autofill:hover, -input:-webkit-autofill:focus { - -webkit-box-shadow: 0 0 0 1000px var(--bg-color) inset; - -webkit-text-fill-color: var(--text-color); - transition: background-color 5000s ease-in-out 0s; -} - -.loading { - text-align: center; - padding: 40px; - color: #999; -} - -.loading-spinner { - display: flex; - justify-content: center; - align-items: center; - padding: 40px; -} - -.loading-spinner::after { - content: ''; - width: 28px; - height: 28px; - border: 3px solid var(--border-color); - border-top-color: var(--primary-color); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* Full-page overlay spinner */ -.overlay-spinner { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.7); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - z-index: 9999; - backdrop-filter: blur(4px); -} - -.overlay-spinner .progress-container { - position: relative; - width: 120px; - height: 120px; -} - -.overlay-spinner .progress-ring { - transform: rotate(-90deg); -} - -.overlay-spinner .progress-ring-circle { - transition: stroke-dashoffset 0.1s linear; - stroke: var(--primary-color); - stroke-width: 4; - fill: transparent; -} - -.overlay-spinner .progress-ring-bg { - stroke: rgba(255, 255, 255, 0.1); - stroke-width: 4; - fill: transparent; -} - -.overlay-spinner .progress-content { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - text-align: center; -} - -.overlay-spinner .progress-percentage { - color: white; - font-size: 28px; - font-weight: 600; -} - -.overlay-spinner .spinner-text { - margin-top: 24px; - color: white; - font-size: 16px; - font-weight: 500; -} - -.overlay-spinner-close { - position: absolute; - top: 16px; - right: 16px; - background: none; - border: none; - color: rgba(255, 255, 255, 0.6); - font-size: 32px; - cursor: pointer; - line-height: 1; - padding: 4px 8px; - transition: color 0.15s; -} - -.overlay-spinner-close:hover { - color: white; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.toast { - position: fixed; - bottom: 40px; - left: 50%; - transform: translateX(-50%) translateY(100px); - padding: 16px 24px; - border-radius: 8px; - color: white; - font-weight: 600; - font-size: 15px; - opacity: 0; - transition: opacity 0.3s, transform 0.3s; - z-index: 2001; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6); - min-width: 300px; - text-align: center; -} - -.toast.show { - opacity: 1; - transform: translateX(-50%) translateY(0); - animation: toastShake 0.5s ease-in-out; -} - -@keyframes toastShake { - 0%, 100% { transform: translateX(-50%) translateY(0); } - 10%, 30%, 50%, 70%, 90% { transform: translateX(-50%) translateY(-5px); } - 20%, 40%, 60%, 80% { transform: translateX(-50%) translateY(5px); } -} - -.toast.success { - background: var(--primary-color); -} - -.toast.error { - background: var(--danger-color); -} - -.toast.info { - background: var(--info-color); -} - -.metrics-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 4px 12px; - margin-top: 8px; -} - -.metric { - display: flex; - justify-content: space-between; - align-items: center; - padding: 3px 8px; - background: var(--bg-color); - border-radius: 4px; -} - -.metric-value { - font-size: 0.9rem; - font-weight: 700; - color: var(--primary-color); -} - -.metric-label { - font-size: 0.8rem; - color: #999; -} - -/* Timing breakdown bar */ -.timing-breakdown { - margin-top: 8px; - padding: 6px 8px; - background: var(--bg-color); - border-radius: 4px; -} - -.timing-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.timing-total { - font-size: 0.8rem; - color: var(--primary-color); -} - -.timing-bar { - display: flex; - height: 8px; - border-radius: 4px; - overflow: hidden; - margin: 4px 0; - gap: 1px; -} - -.timing-seg { - min-width: 2px; - transition: flex 0.3s ease; -} - -.timing-extract { background: #4CAF50; } -.timing-map { background: #FF9800; } -.timing-smooth { background: #2196F3; } -.timing-send { background: #E91E63; } - -.timing-legend { - display: flex; - flex-wrap: wrap; - gap: 8px; - font-size: 0.75rem; - color: #999; - margin-top: 4px; -} - -.timing-legend-item { - display: flex; - align-items: center; - gap: 3px; -} - -.timing-dot { - display: inline-block; - width: 8px; - height: 8px; - border-radius: 2px; -} - -.timing-dot.timing-extract { background: #4CAF50; } -.timing-dot.timing-map { background: #FF9800; } -.timing-dot.timing-smooth { background: #2196F3; } -.timing-dot.timing-send { background: #E91E63; } - -/* Modal Styles */ -.modal { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.8); - z-index: 2000; - align-items: center; - justify-content: center; - animation: fadeIn 0.2s ease-out; -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -.modal-content { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 12px; - max-width: 500px; - width: 90%; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); - animation: slideUp 0.3s ease-out; -} - -#template-modal .modal-content { - max-width: 500px !important; - width: 100% !important; -} - -#test-template-modal .modal-content { - max-width: 420px; -} - -@keyframes slideUp { - from { - transform: translateY(20px); - opacity: 0; - } - to { - transform: translateY(0); - opacity: 1; - } -} - -.modal-header { - padding: 24px 24px 16px 24px; - border-bottom: 1px solid var(--border-color); - display: flex; - align-items: center; - justify-content: space-between; -} - -.modal-header h2 { - margin: 0; - font-size: 1.5rem; - color: var(--text-color); -} - -.modal-header-actions { - display: flex; - align-items: center; - gap: 4px; -} - -.modal-header-btn { - background: none; - border: none; - font-size: 1.1rem; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - border-radius: 4px; - transition: background 0.2s; - flex-shrink: 0; -} -.modal-header-btn:hover { - background: rgba(128, 128, 128, 0.15); -} -.modal-header-btn:disabled { - opacity: 0.4; - cursor: default; -} - -.modal-close-btn { - background: none; - border: none; - color: #777; - font-size: 1.2rem; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - border-radius: 4px; - transition: color 0.2s, background 0.2s; - flex-shrink: 0; -} - -.modal-close-btn:hover { - color: var(--text-color); - background: rgba(128, 128, 128, 0.15); -} - -.modal-body { - padding: 24px; -} - -.modal-description { - color: #999; - margin-bottom: 20px; - line-height: 1.6; -} - -.password-input-wrapper { - position: relative; - display: flex; - align-items: center; -} - -.password-input-wrapper input { - flex: 1; - padding-right: 45px; -} - -.password-toggle { - position: absolute; - right: 8px; - background: none; - border: none; - cursor: pointer; - font-size: 1.2rem; - padding: 8px; - color: var(--text-color); - opacity: 0.6; - transition: opacity 0.2s; -} - -.password-toggle:hover { - opacity: 1; -} - -.label-row { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 5px; -} - -.label-row label { - margin-bottom: 0; -} - -.hint-toggle { - background: none; - border: 1px solid var(--border-color); - border-radius: 50%; - width: 18px; - height: 18px; - font-size: 0.7rem; - line-height: 1; - color: var(--text-secondary, #888); - cursor: pointer; - padding: 0; - display: inline-flex; - align-items: center; - justify-content: center; - opacity: 0.6; - transition: opacity 0.2s; - flex-shrink: 0; -} - -.hint-toggle:hover { - opacity: 1; -} - -.hint-toggle.active { - opacity: 1; - color: var(--primary-color, #4CAF50); - border-color: var(--primary-color, #4CAF50); -} - -.input-hint { - display: block; - margin: 0 0 6px 0; - color: #666; - font-size: 0.85rem; -} - -.fps-hint { - display: block; - margin-top: 4px; - font-size: 0.82rem; - color: var(--info-color, #2196F3); -} - -.slider-row { - display: flex; - align-items: center; - gap: 12px; -} - -.slider-row input[type="range"] { - flex: 1; -} - -.slider-value { - min-width: 28px; - text-align: center; - font-weight: 600; - font-size: 0.95rem; - color: var(--text-primary); -} - -.error-message { - background: rgba(244, 67, 54, 0.1); - border: 1px solid var(--danger-color); - color: var(--danger-color); - padding: 12px; - border-radius: 4px; - margin-top: 15px; - font-size: 0.9rem; -} - -.modal-footer { - padding: 16px 24px 24px 24px; - display: flex; - justify-content: flex-end; - gap: 8px; -} - -.modal-footer .btn-icon { - min-width: 60px; - padding: 10px 20px; - font-size: 1.4rem; -} - -/* Theme Toggle */ -.theme-toggle { - background: var(--card-bg); - border: 1px solid var(--border-color); - padding: 6px 12px; - border-radius: 4px; - cursor: pointer; - font-size: 1.2rem; - transition: transform 0.2s; - margin-left: 10px; -} - -.theme-toggle:hover { - transform: scale(1.1); -} - -/* Footer */ -.app-footer { - margin-top: 20px; - padding: 15px 0; - text-align: center; -} - -.footer-content { - color: var(--text-secondary); - font-size: 0.9rem; -} - -.footer-content p { - margin: 0; -} - - -.footer-content strong { - color: var(--text-color); -} - -.footer-content a { - color: var(--primary-color); - text-decoration: none; - transition: opacity 0.2s; -} - -.footer-content a:hover { - opacity: 0.8; - text-decoration: underline; -} - -/* Interactive Calibration Preview Edges */ -.calibration-preview { - position: relative; - width: 100%; - aspect-ratio: 16 / 9; - margin: 40px auto 40px; - background: var(--card-bg); - border: 2px solid var(--border-color); - border-radius: 8px; - overflow: visible; -} - -#calibration-preview-canvas { - position: absolute; - top: -40px; - left: -40px; - width: calc(100% + 80px); - height: calc(100% + 80px); - pointer-events: none; - z-index: 3; -} - -.preview-screen { - position: absolute; - top: 37px; - left: 57px; - right: 57px; - bottom: 37px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 4px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 6px; - color: white; - font-size: 14px; -} - -.preview-screen-border-width { - display: flex; - align-items: center; - gap: 6px; - font-size: 13px; - font-weight: 600; -} - -.preview-screen-border-width label { - white-space: nowrap; -} - -.preview-screen-border-width input { - width: 52px; - padding: 2px 4px; - font-size: 12px; - border: 1px solid rgba(255, 255, 255, 0.4); - border-radius: 3px; - background: rgba(255, 255, 255, 0.15); - color: white; - text-align: center; -} - -.preview-screen-border-width input:focus { - outline: none; - border-color: rgba(255, 255, 255, 0.7); - background: rgba(255, 255, 255, 0.25); -} - -.preview-screen-total { - font-size: 16px; - font-weight: 600; - opacity: 0.9; - transition: color 0.2s; - cursor: pointer; - user-select: none; -} - -.preview-screen-total:hover { - opacity: 1; -} - -.preview-screen-total.mismatch { - color: #FFC107; -} - -.inputs-dimmed .edge-led-input { - opacity: 0.2; - pointer-events: none; -} - -.preview-screen-controls { - display: flex; - align-items: center; - gap: 6px; -} - -.preview-edge { - position: absolute; - display: flex; - align-items: center; - justify-content: center; - font-size: 11px; - color: var(--text-secondary); - background: rgba(128, 128, 128, 0.15); - transition: background 0.2s; - z-index: 2; - user-select: none; -} - -/* Edge test toggle zones — positioned outside the container border */ -.edge-toggle { - position: absolute; - cursor: pointer; - z-index: 1; - background: rgba(128, 128, 128, 0.1); - border: 1px solid rgba(128, 128, 128, 0.35); - border-radius: 3px; - transition: background 0.2s, box-shadow 0.2s; -} - -.edge-toggle:hover { - background: rgba(128, 128, 128, 0.25); -} - -.preview-edge.edge-disabled { - opacity: 0.25; - pointer-events: none; -} - -.preview-edge.edge-disabled .edge-led-input { - pointer-events: auto; - opacity: 1; -} - -.edge-toggle.edge-disabled { - opacity: 0.15; - pointer-events: none; - cursor: default; -} - -.toggle-top { - top: -16px; - left: 56px; - right: 56px; - height: 16px; -} - -.toggle-bottom { - bottom: -16px; - left: 56px; - right: 56px; - height: 16px; -} - -.toggle-left { - left: -16px; - top: 36px; - bottom: 36px; - width: 16px; -} - -.toggle-right { - right: -16px; - top: 36px; - bottom: 36px; - width: 16px; -} - -.edge-top { - top: 0; - left: 56px; - right: 56px; - height: 36px; - border-radius: 6px 6px 0 0; - flex-direction: row; - gap: 8px; -} - -.edge-bottom { - bottom: 0; - left: 56px; - right: 56px; - height: 36px; - border-radius: 0 0 6px 6px; - flex-direction: row; - gap: 8px; -} - -.edge-left { - left: 0; - top: 36px; - bottom: 36px; - width: 56px; - flex-direction: column; - border-radius: 6px 0 0 6px; - gap: 4px; -} - -.edge-right { - right: 0; - top: 36px; - bottom: 36px; - width: 56px; - flex-direction: column; - border-radius: 0 6px 6px 0; - gap: 4px; -} - -.edge-led-input { - width: 46px; - padding: 2px 2px; - font-size: 12px; - box-sizing: border-box; - max-height: 100%; - text-align: center; - background: rgba(0, 0, 0, 0.3); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 3px; - color: inherit; -} - -.edge-led-input:focus { - border-color: var(--primary-color); - outline: none; -} - -.edge-top .edge-led-input, -.edge-bottom .edge-led-input { - width: 56px; -} - -/* Hide spinner arrows on edge inputs to save space */ -.edge-led-input::-webkit-outer-spin-button, -.edge-led-input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} -.edge-led-input[type=number] { - -moz-appearance: textfield; -} - -/* Edge span bars */ -.edge-span-bar { - position: absolute; - background: rgba(76, 175, 80, 0.3); - border: 1px solid rgba(76, 175, 80, 0.5); - border-radius: 2px; - cursor: grab; - transition: background 0.15s; -} - -.edge-span-bar:hover { - background: rgba(76, 175, 80, 0.45); -} - -.edge-span-bar:active { - cursor: grabbing; -} - -/* Horizontal edges: bar spans left-right */ -.edge-top .edge-span-bar, -.edge-bottom .edge-span-bar { - top: 0; - bottom: 0; -} - -/* Vertical edges: bar spans top-bottom */ -.edge-left .edge-span-bar, -.edge-right .edge-span-bar { - left: 0; - right: 0; -} - -/* Resize handles — large transparent hit area with narrow visible strip */ -.edge-span-handle { - position: absolute; - background: transparent; - z-index: 3; - opacity: 0; - transition: opacity 0.15s; -} - -.edge-span-handle::after { - content: ''; - position: absolute; - background: rgba(255, 255, 255, 0.75); - border: 1px solid rgba(76, 175, 80, 0.7); - border-radius: 2px; -} - -.edge-span-bar:hover .edge-span-handle { - opacity: 1; -} - -/* Horizontal handles */ -.edge-top .edge-span-handle, -.edge-bottom .edge-span-handle { - top: 0; - bottom: 0; - width: 16px; - cursor: ew-resize; -} - -.edge-top .edge-span-handle::after, -.edge-bottom .edge-span-handle::after { - top: 3px; - bottom: 3px; - left: 6px; - width: 4px; -} - -.edge-top .edge-span-handle-start, -.edge-bottom .edge-span-handle-start { - left: -8px; -} - -.edge-top .edge-span-handle-end, -.edge-bottom .edge-span-handle-end { - right: -8px; -} - -/* Vertical handles */ -.edge-left .edge-span-handle, -.edge-right .edge-span-handle { - left: 0; - right: 0; - height: 16px; - cursor: ns-resize; -} - -.edge-left .edge-span-handle::after, -.edge-right .edge-span-handle::after { - left: 3px; - right: 3px; - top: 6px; - height: 4px; -} - -.edge-left .edge-span-handle-start, -.edge-right .edge-span-handle-start { - top: -8px; -} - -.edge-left .edge-span-handle-end, -.edge-right .edge-span-handle-end { - bottom: -8px; -} - -/* Ensure LED input is above span bar */ -.edge-led-input { - position: relative; - z-index: 2; -} - -/* Corner start-position buttons */ -.preview-corner { - position: absolute; - width: 56px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - font-size: 18px; - line-height: 1; - color: rgba(128, 128, 128, 0.4); - cursor: pointer; - z-index: 5; - transition: color 0.2s, transform 0.2s, text-shadow 0.2s; - user-select: none; -} - -.preview-corner:hover { - color: rgba(76, 175, 80, 0.6); -} - -.preview-corner.active:hover { - transform: none; -} - -.preview-corner.active { - color: #4CAF50; - text-shadow: 0 0 8px rgba(76, 175, 80, 0.6); - font-size: 22px; -} - -.corner-top-left { top: 0; left: 0; } -.corner-top-right { top: 0; right: 0; } -.corner-bottom-left { bottom: 0; left: 0; } -.corner-bottom-right { bottom: 0; right: 0; } - -/* Direction toggle inside screen */ -.direction-toggle { - display: flex; - align-items: center; - gap: 4px; - height: 26px; - padding: 0 10px; - background: rgba(255, 255, 255, 0.15); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 12px; - color: white; - font-family: inherit; - font-size: 12px; - box-sizing: border-box; - cursor: pointer; - transition: background 0.2s; - user-select: none; -} - -.direction-toggle:hover { - background: rgba(255, 255, 255, 0.25); -} - -.direction-toggle #direction-icon { - font-size: 14px; -} - -.preview-hint { - text-align: center; - font-size: 0.8rem; - color: var(--text-secondary); - margin-top: 8px; -} - -@media (max-width: 768px) { - .displays-grid, - .devices-grid { - grid-template-columns: 1fr; - } - - header { - flex-direction: column; - gap: 15px; - text-align: center; - } - - .modal-content { - width: 95%; - margin: 20px; - } - - .modal-footer .btn { - min-width: 80px; - } -} - -/* Tutorial System */ -.tutorial-trigger-btn { - width: 28px; - height: 28px; - border-radius: 50%; - border: 2px solid var(--primary-color); - background: transparent; - color: var(--primary-color); - font-size: 1rem; - font-weight: bold; - cursor: pointer; - transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; - margin-left: auto; - margin-right: 8px; - flex-shrink: 0; -} - -.tutorial-trigger-btn:hover { - background: var(--primary-color); - color: white; -} - -#calibration-modal .modal-body { - position: relative; -} - -.tutorial-overlay { - display: none; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 100; - pointer-events: none; -} - -.tutorial-overlay.active { - display: block; - pointer-events: auto; -} - -.tutorial-backdrop { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.7); - transition: clip-path 0.3s ease; -} - -.tutorial-ring { - position: absolute; - border: 2px solid var(--primary-color); - border-radius: 6px; - pointer-events: none; - transition: all 0.3s ease; - animation: tutorial-pulse 2s infinite; -} - -@keyframes tutorial-pulse { - 0%, 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.6); } - 50% { box-shadow: 0 0 0 6px rgba(76, 175, 80, 0); } -} - -.tutorial-tooltip { - position: absolute; - width: 260px; - background: var(--card-bg); - border: 2px solid var(--primary-color); - border-radius: 8px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); - z-index: 102; - pointer-events: auto; - animation: tutorial-tooltip-in 0.25s ease-out; -} - -@keyframes tutorial-tooltip-in { - from { opacity: 0; transform: translateY(-8px); } - to { opacity: 1; transform: translateY(0); } -} - -.tutorial-tooltip-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 12px; - border-bottom: 1px solid var(--border-color); -} - -.tutorial-step-counter { - font-size: 0.8rem; - font-weight: 600; - color: var(--primary-color); -} - -.tutorial-close-btn { - background: none; - border: none; - color: #777; - font-size: 1.3rem; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - border-radius: 4px; - transition: color 0.2s, background 0.2s; - padding: 0; - line-height: 1; -} - -.tutorial-close-btn:hover { - color: var(--text-color); - background: rgba(128, 128, 128, 0.15); -} - -.tutorial-tooltip-text { - margin: 0; - padding: 12px; - line-height: 1.5; - color: var(--text-color); - font-size: 0.9rem; -} - -.tutorial-tooltip-nav { - display: flex; - gap: 6px; - padding: 8px 12px; - border-top: 1px solid var(--border-color); -} - -.tutorial-prev-btn, -.tutorial-next-btn { - flex: 1; - padding: 6px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; - font-weight: 600; - transition: opacity 0.2s; -} - -.tutorial-prev-btn { - background: var(--border-color); - color: var(--text-color); -} - -.tutorial-next-btn { - background: var(--primary-color); - color: white; -} - -.tutorial-prev-btn:hover:not(:disabled), -.tutorial-next-btn:hover:not(:disabled) { - opacity: 0.85; -} - -.tutorial-prev-btn:disabled, -.tutorial-next-btn:disabled { - opacity: 0.3; - cursor: not-allowed; -} - -.tutorial-target { - position: relative; - z-index: 101 !important; -} - -/* Fixed (viewport-level) tutorial overlay for device cards */ -.tutorial-overlay-fixed { - position: fixed; - z-index: 10000; -} - -.tutorial-overlay-fixed .tutorial-backdrop { - position: fixed; -} - -.tutorial-overlay-fixed .tutorial-ring { - position: fixed; -} - -.tutorial-overlay-fixed .tutorial-tooltip { - position: absolute; - z-index: 10002; - animation: none; - opacity: 1; -} - -/* target z-index for fixed overlay is set inline via JS (target is outside overlay DOM) */ - - -/* =========================== - Capture Templates Styles - =========================== */ - -.templates-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); - gap: 20px; -} - -.template-card { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 16px; - transition: box-shadow 0.2s; - display: flex; - flex-direction: column; - position: relative; -} - -.template-card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -.add-template-card { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 180px; - cursor: pointer; - border: 2px dashed var(--border-color); - background: transparent; - transition: border-color 0.2s, background 0.2s; -} - -.add-template-card:hover { - border-color: var(--primary-color); - background: rgba(33, 150, 243, 0.05); - box-shadow: none; -} - -.add-template-icon { - font-size: 2.5rem; - font-weight: 300; - color: var(--text-secondary); - line-height: 1; - transition: color 0.2s; -} - -.add-template-card:hover .add-template-icon { - color: var(--primary-color); -} - -.add-template-label { - font-size: 0.85rem; - color: var(--text-secondary); - margin-top: 8px; - font-weight: 500; -} - -.template-card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; -} - -.template-card .template-card-header { - padding-right: 24px; -} - -.template-name { - font-size: 18px; - font-weight: bold; - color: var(--text-color); -} - -.badge { - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; - font-weight: bold; - text-transform: uppercase; -} - -.template-description { - color: var(--text-secondary); - font-size: 14px; - margin-bottom: 12px; - line-height: 1.4; -} - -.template-config { - font-size: 14px; - color: var(--text-secondary); - margin-bottom: 8px; -} - -.filter-chain { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 4px; - margin-bottom: 8px; -} - -.filter-chain-item { - font-size: 0.7rem; - background: var(--border-color); - color: var(--text-secondary); - padding: 2px 8px; - border-radius: 10px; -} - -.filter-chain-arrow { - font-size: 0.7rem; - color: var(--text-muted); -} - -.template-config-details { - margin: 12px 0; - font-size: 13px; -} - -.template-config-details summary { - cursor: pointer; - color: var(--primary-color); - font-weight: 500; - padding: 4px 0; -} - -.template-config-details summary:hover { - text-decoration: underline; -} - -.template-no-config { - margin: 12px 0; - font-size: 13px; - color: var(--primary-color); - font-weight: 500; - padding: 4px 0; -} - -.config-table { - width: 100%; - margin-top: 8px; - border-collapse: collapse; - font-size: 13px; -} - -.config-table td { - padding: 4px 8px; - border-bottom: 1px solid var(--border-color); -} - -.config-table tr:last-child td { - border-bottom: none; -} - -.config-key { - color: var(--text-secondary); - white-space: nowrap; - width: 1%; -} - -.config-value { - color: var(--text-primary); - font-family: monospace; -} - -/* Engine config grid (property name left, input right) */ -.config-grid { - display: grid; - grid-template-columns: auto 1fr; - gap: 8px 12px; - align-items: center; - margin-top: 12px; -} - -.config-grid-label { - font-size: 0.85rem; - color: var(--text-secondary); - white-space: nowrap; - text-align: right; -} - -.config-grid-value input, -.config-grid-value select { - width: 100%; - padding: 6px 10px; - border: 1px solid var(--border-color); - border-radius: 4px; - background: var(--bg-color); - color: var(--text-color); - font-size: 0.85rem; -} - -.template-card-actions { - display: flex; - gap: 8px; - margin-top: auto; - padding-top: 12px; - border-top: 1px solid var(--border-color); - align-items: center; -} - -.template-card-actions .btn:not(.btn-icon) { - flex: 1; -} - -.template-card-actions .btn-icon { - flex-shrink: 0; -} - -.text-muted { - color: var(--text-secondary); - font-style: italic; - font-size: 13px; -} - -/* PP Filter List in Template Modal */ -.pp-filter-list { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 12px; -} - -.pp-filter-empty { - color: var(--text-secondary); - font-size: 13px; - text-align: center; - padding: 16px; - border: 1px dashed var(--border-color); - border-radius: 8px; -} - -.pp-filter-card { - border: 1px solid var(--border-color); - border-radius: 8px; - background: var(--bg-secondary); - padding: 10px 12px; -} - -.pp-filter-card-header { - display: flex; - align-items: center; - gap: 6px; - cursor: pointer; - user-select: none; -} - -.pp-filter-card.expanded .pp-filter-card-header { - margin-bottom: 8px; -} - -.pp-filter-card-chevron { - font-size: 10px; - color: var(--text-secondary); - flex-shrink: 0; - width: 12px; -} - -.pp-filter-card-name { - font-weight: 600; - font-size: 14px; - color: var(--text-primary); -} - -.pp-filter-card-summary { - color: var(--text-secondary); - font-size: 12px; - margin-right: 8px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.pp-filter-card-actions { - display: flex; - gap: 4px; - flex-shrink: 0; - margin-left: auto; -} - -.btn-filter-action { - background: none; - border: 1px solid var(--border-color); - border-radius: 4px; - color: var(--text-secondary); - cursor: pointer; - width: 26px; - height: 26px; - display: flex; - align-items: center; - justify-content: center; - font-size: 11px; - padding: 0; -} - -.btn-filter-action:hover:not(:disabled) { - background: var(--border-color); - color: var(--text-primary); -} - -.btn-filter-action:disabled { - opacity: 0.3; - cursor: not-allowed; -} - -.btn-filter-remove:hover:not(:disabled) { - background: rgba(239, 68, 68, 0.15); - border-color: rgba(239, 68, 68, 0.4); - color: #ef4444; -} - -.pp-filter-card-options { - display: flex; - flex-direction: column; - gap: 4px; -} - -.pp-filter-option { - display: flex; - flex-direction: column; - gap: 2px; -} - -.pp-filter-option label { - display: flex; - justify-content: space-between; - font-size: 12px; - color: var(--text-secondary); -} - -.pp-filter-option input[type="range"] { - width: 100%; -} - -.pp-filter-option-bool label { - justify-content: space-between; - gap: 8px; - align-items: center; - cursor: pointer; - padding: 4px 0; -} - -.pp-filter-option-bool input[type="checkbox"] { - appearance: none; - -webkit-appearance: none; - width: 34px; - min-width: 34px; - height: 18px; - background: var(--border-color); - border-radius: 9px; - position: relative; - cursor: pointer; - transition: background 0.2s; - order: 1; - margin: 0; -} - -.pp-filter-option-bool input[type="checkbox"]::after { - content: ''; - position: absolute; - top: 2px; - left: 2px; - width: 14px; - height: 14px; - background: white; - border-radius: 50%; - transition: transform 0.2s; -} - -.pp-filter-option-bool input[type="checkbox"]:checked { - background: var(--primary-color); -} - -.pp-filter-option-bool input[type="checkbox"]:checked::after { - transform: translateX(16px); -} - -.pp-filter-option-bool span { - order: 0; -} - -.pp-add-filter-row { - display: flex; - gap: 8px; - align-items: center; - margin-bottom: 4px; -} - -.pp-add-filter-select { - flex: 1; - padding: 6px 10px; - border: 1px solid var(--border-color); - border-radius: 6px; - background: var(--card-bg); - color: var(--text-primary); - font-size: 13px; -} - -.pp-add-filter-btn { - width: 34px; - height: 34px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid var(--border-color); - border-radius: 6px; - background: var(--card-bg); - color: var(--text-primary); - font-size: 20px; - cursor: pointer; - padding: 0; - line-height: 1; -} - -.pp-add-filter-btn:hover { - background: var(--border-color); -} - -/* Template Test Section */ -.template-test-section { - background: var(--bg-secondary); - border-radius: 8px; - padding: 16px; -} - -.template-test-section h3 { - margin-top: 0; - margin-bottom: 12px; - font-size: 16px; -} - -.test-results-container { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 16px; -} - -.test-preview-section, -.test-performance-section { - margin-bottom: 20px; -} - -.test-preview-section:last-child, -.test-performance-section:last-child { - margin-bottom: 0; -} - -.test-preview-section h4, -.test-performance-section h4 { - margin-top: 0; - margin-bottom: 12px; - font-size: 14px; - font-weight: 600; - color: var(--text-secondary); -} - -.test-preview-image { - border-radius: 4px; - overflow: hidden; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -.test-preview-image img { - display: block; - width: 100%; - height: auto; -} - -.test-performance-stats { - display: flex; - flex-direction: column; - gap: 8px; -} - -.stat-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 0; - border-bottom: 1px solid var(--border-color); - font-size: 14px; -} - -.stat-item:last-child { - border-bottom: none; -} - -.stat-item span { - color: var(--text-secondary); - font-weight: 500; -} - -.stat-item strong { - color: var(--text-color); - font-weight: 600; - font-family: monospace; -} - -/* Empty state */ -.empty-state { - text-align: center; - padding: 40px 20px; - color: var(--text-secondary); - font-size: 16px; -} - -/* Stream type badges */ -.badge-raw { - background: #1976d2; - color: white; -} - -.badge-processed { - background: #7b1fa2; - color: white; -} - -/* Stream info panel in stream selector modal */ -.stream-info-panel { - padding: 4px 0 0 0; - font-size: 14px; - line-height: 1.6; -} - -/* Stream sub-tabs */ -.stream-tab-bar { - display: flex; - align-items: center; - gap: 4px; - border-bottom: 2px solid var(--border-color); - margin-bottom: 16px; -} - -.stream-tab-btn { - background: none; - border: none; - padding: 8px 14px; - font-size: 0.9rem; - font-weight: 500; - color: var(--text-secondary); - cursor: pointer; - border-bottom: 2px solid transparent; - margin-bottom: -2px; - transition: color 0.2s, border-color 0.2s; -} - -.stream-tab-btn:hover { - color: var(--text-color); -} - -.stream-tab-btn.active { - color: var(--primary-color); - border-bottom-color: var(--primary-color); -} - -.stream-tab-count { - background: var(--border-color); - color: var(--text-secondary); - font-size: 0.7rem; - font-weight: 600; - padding: 1px 6px; - border-radius: 8px; - margin-left: 4px; -} - -.stream-tab-btn.active .stream-tab-count { - background: var(--primary-color); - color: #fff; -} - -.stream-tab-panel { - display: none; -} - -.stream-tab-panel.active { - display: block; -} - -/* Sub-tab content sections */ -.subtab-section { - margin-bottom: 24px; -} - -.subtab-section:last-child { - margin-bottom: 0; -} - -.subtab-section-header { - font-size: 1rem; - font-weight: 600; - color: var(--text-secondary); - margin: 0 0 12px 0; - padding-bottom: 8px; - border-bottom: 1px solid var(--border-color); -} - -/* Image Lightbox */ -.lightbox { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.92); - z-index: 10000; - justify-content: center; - align-items: center; - cursor: zoom-out; -} - -.lightbox.active { - display: flex; -} - -.lightbox-content { - position: relative; - max-width: 95%; - max-height: 95%; - cursor: default; -} - -.lightbox-content img { - max-width: 100%; - max-height: 90vh; - object-fit: contain; - border-radius: 4px; - display: block; -} - -.lightbox-close { - position: absolute; - top: 16px; - right: 16px; - background: rgba(255, 255, 255, 0.15); - border: none; - color: white; - font-size: 1.5rem; - width: 40px; - height: 40px; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.2s; - z-index: 1; -} - -.lightbox-close:hover { - background: rgba(255, 255, 255, 0.3); -} - -.lightbox-refresh-btn { - position: absolute; - top: 16px; - right: 64px; - background: rgba(255, 255, 255, 0.15); - border: none; - color: white; - font-size: 1.2rem; - width: 40px; - height: 40px; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.2s; - z-index: 1; -} - -.lightbox-refresh-btn:hover { - background: rgba(255, 255, 255, 0.3); -} - -.lightbox-refresh-btn.active { - background: var(--primary-color); -} - -.lightbox-stats { - position: absolute; - bottom: 8px; - left: 8px; - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(8px); - color: white; - padding: 8px 14px; - border-radius: 6px; - font-size: 0.8rem; - display: flex; - gap: 16px; - flex-wrap: wrap; -} - -.lightbox-stats .stat-item { - display: flex; - gap: 4px; - align-items: center; -} - -.lightbox-stats .stat-item span { - opacity: 0.7; -} - -.lightbox-stats .stat-item strong { - font-weight: 600; -} - -/* Display Picker Lightbox */ -.display-picker-content { - max-width: 900px; - width: 90%; - text-align: center; -} - -.display-picker-title { - color: white; - font-size: 1.3rem; - margin-bottom: 20px; - font-weight: 500; -} - -.display-picker-canvas { - background: rgba(255, 255, 255, 0.05); - border: 2px dashed rgba(255, 255, 255, 0.15); - border-radius: 8px; - padding: 24px; - width: 100%; - box-sizing: border-box; -} - -.layout-display-pickable { - cursor: pointer !important; - border: 2px solid var(--border-color) !important; - background: linear-gradient(135deg, rgba(128, 128, 128, 0.08), rgba(128, 128, 128, 0.03)) !important; -} - -.layout-display-pickable:hover { - box-shadow: 0 0 20px rgba(76, 175, 80, 0.4); - border-color: var(--primary-color) !important; -} - -.layout-display-pickable.selected { - border-color: var(--primary-color) !important; - box-shadow: 0 0 16px rgba(76, 175, 80, 0.5); - background: rgba(76, 175, 80, 0.12) !important; -} - -/* Display picker button in forms */ -.btn-display-picker { - width: 100%; - padding: 10px; - border: 1px solid var(--border-color); - border-radius: 4px; - background: var(--bg-color); - color: var(--text-color); - font-size: 1rem; - cursor: pointer; - text-align: left; - transition: border-color 0.2s, box-shadow 0.2s; - font-weight: 400; -} - -.btn-display-picker:hover { - border-color: var(--primary-color); - box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.15); -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .templates-grid { - grid-template-columns: 1fr; - } -} - -/* Static image stream styles */ -.image-preview-container { - text-align: center; - margin: 12px 0; - padding: 12px; - background: var(--bg-secondary); - border-radius: 8px; - border: 1px solid var(--border-color); -} -.stream-image-preview { - max-width: 100%; - max-height: 200px; - border-radius: 4px; - border: 1px solid var(--border-color); -} -.stream-image-info { - font-size: 0.75rem; - color: var(--text-muted); - margin-top: 6px; -} -.validation-status { - font-size: 0.8rem; - margin-top: 4px; - padding: 4px 8px; - border-radius: 4px; -} -.validation-status.success { - color: #4caf50; -} -.validation-status.error { - color: #f44336; -} -.validation-status.loading { - color: var(--text-muted); -} -.stream-card-props { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 8px; -} - -.stream-card-prop { - display: inline-block; - font-size: 0.75rem; - color: var(--text-secondary); - background: var(--border-color); - padding: 2px 8px; - border-radius: 10px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 180px; - vertical-align: middle; -} - -.stream-card-prop-full { - max-width: 100%; - word-break: break-all; - white-space: normal; - font-size: 0.7rem; -} - -/* Key Colors target styles */ -.kc-rect-list { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 8px; -} - -.kc-rect-row { - display: flex; - align-items: center; - gap: 6px; - padding: 8px; - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: 6px; -} - -.kc-rect-row input[type="text"] { - flex: 2; - min-width: 0; -} - -.kc-rect-row input[type="number"] { - flex: 1; - min-width: 0; - width: 60px; -} - -.kc-rect-row .kc-rect-remove-btn { - background: none; - border: none; - color: #777; - font-size: 1rem; - cursor: pointer; - padding: 4px; - border-radius: 4px; - flex-shrink: 0; - transition: color 0.2s, background 0.2s; -} - -.kc-rect-row .kc-rect-remove-btn:hover { - color: var(--danger-color); - background: rgba(244, 67, 54, 0.1); -} - -.kc-rect-labels { - display: flex; - gap: 6px; - padding: 0 8px; - margin-bottom: 4px; - font-size: 0.7rem; - color: var(--text-secondary); - font-weight: 600; -} - -.kc-rect-labels span:first-child { - flex: 2; -} - -.kc-rect-labels span { - flex: 1; - text-align: center; -} - -.kc-rect-labels span:last-child { - width: 28px; - flex: 0 0 28px; -} - -.kc-add-rect-btn { - width: 100%; - font-size: 0.85rem; - padding: 6px 12px; -} - -.kc-rect-empty { - text-align: center; - color: var(--text-secondary); - font-size: 0.85rem; - padding: 12px; - font-style: italic; -} - -.kc-color-swatches { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 8px; -} - -.kc-swatch { - display: flex; - flex-direction: column; - align-items: center; - gap: 3px; -} - -.kc-swatch-color { - width: 32px; - height: 32px; - border-radius: 6px; - border: 2px solid var(--border-color); - transition: background-color 0.3s; -} - -.kc-swatch-label { - font-size: 0.6rem; - color: var(--text-secondary); - max-width: 40px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - text-align: center; -} - -.kc-no-colors { - color: var(--text-secondary); - font-size: 0.8rem; - font-style: italic; - padding: 4px 0; -} - -/* Pattern Template Visual Editor */ -.modal-content-wide { - width: fit-content; - min-width: 500px; - max-width: calc(100vw - 40px); - max-height: calc(100vh - 40px); - display: flex; - flex-direction: column; -} - -.modal-content-wide .modal-body { - overflow-y: auto; - scrollbar-gutter: stable; - flex: 1 1 auto; - min-height: 0; -} - -.pattern-canvas-container { - position: relative; - border-radius: 8px; - overflow: hidden; - background: var(--bg-color); - border: 1px solid var(--border-color); - margin-bottom: 12px; - resize: both; - width: 820px; - min-width: 400px; - max-width: 100%; - min-height: 200px; - height: 450px; - max-height: calc(100vh - 400px); -} - -#pattern-canvas { - width: 100%; - height: 100%; - display: block; - cursor: default; -} - -.pattern-canvas-toolbar { - display: flex; - gap: 2px; - padding: 4px; - align-items: center; - background: var(--bg-secondary); - border-radius: 6px; - margin-top: 4px; -} - -.pattern-canvas-toolbar .btn { - flex: 0 0 auto; - min-width: 32px; - width: 32px; - height: 32px; - padding: 0; - font-size: 1rem; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; -} - -.pattern-bg-row { - display: flex; - gap: 0.5rem; - align-items: center; - margin-bottom: 8px; -} - -.pattern-bg-row select { - flex: 1; -} - -.pattern-capture-btn { - flex: 0 0 auto; - min-width: 36px !important; - width: 36px; - height: 36px; - padding: 0 !important; - font-size: 1.1rem; - line-height: 36px; - text-align: center; -} - -.pattern-rect-list { - display: flex; - flex-direction: column; - gap: 6px; - margin-top: 8px; - max-height: 200px; - overflow-y: auto; -} - -.pattern-rect-row { - display: flex; - align-items: center; - gap: 6px; - padding: 6px 8px; - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: 6px; - font-size: 0.85rem; - cursor: pointer; - transition: border-color 0.15s; -} - -.pattern-rect-row.selected { - border-color: var(--primary-color); - background: rgba(76, 175, 80, 0.08); -} - -.pattern-rect-row input[type="text"] { - flex: 2; - min-width: 0; - padding: 4px 6px; - font-size: 0.8rem; -} - -.pattern-rect-row input[type="number"] { - flex: 1; - min-width: 0; - width: 55px; - padding: 4px 6px; - font-size: 0.8rem; -} - -.pattern-rect-row .pattern-rect-remove-btn { - background: none; - border: none; - color: #777; - font-size: 0.9rem; - cursor: pointer; - padding: 2px 4px; - border-radius: 4px; - flex-shrink: 0; - transition: color 0.2s, background 0.2s; -} - -.pattern-rect-row .pattern-rect-remove-btn:hover { - color: var(--danger-color); - background: rgba(244, 67, 54, 0.1); -} - -.pattern-rect-labels { - display: flex; - gap: 6px; - padding: 0 8px; - margin-bottom: 2px; - font-size: 0.7rem; - color: var(--text-secondary); - font-weight: 600; -} - -.pattern-rect-labels span:first-child { - flex: 2; -} - -.pattern-rect-labels span { - flex: 1; - text-align: center; -} - -.pattern-rect-labels span:last-child { - width: 24px; - flex: 0 0 24px; -} - -/* ── Dashboard ── */ - -.dashboard-section { - margin-bottom: 16px; -} - -.dashboard-section-header { - font-size: 0.8rem; - font-weight: 600; - margin-bottom: 6px; - color: var(--text-secondary); - display: flex; - align-items: center; - gap: 6px; - text-transform: uppercase; - letter-spacing: 0.5px; - cursor: pointer; - user-select: none; -} - -.dashboard-section-chevron { - font-size: 0.6rem; - color: var(--text-secondary); - width: 10px; - display: inline-block; -} - -.dashboard-section-count { - background: var(--border-color); - color: var(--text-secondary); - border-radius: 10px; - padding: 0 6px; - font-size: 0.75rem; - font-weight: 600; -} - -.dashboard-subsection { - margin-bottom: 10px; - padding-left: 16px; -} -.dashboard-subsection .dashboard-section-header { - font-size: 0.72rem; -} - -.dashboard-stop-all { - margin-left: auto; - padding: 2px 8px; - font-size: 0.7rem; - white-space: nowrap; - flex: 0 0 auto; -} - -.dashboard-poll-wrap { - margin-left: auto; - display: flex; - align-items: center; - gap: 3px; -} - -.dashboard-poll-slider { - width: 48px; - height: 12px; - accent-color: var(--primary-color); - cursor: pointer; -} - -.dashboard-poll-value { - font-size: 0.6rem; - color: var(--text-secondary); - min-width: 18px; -} - -.dashboard-target { - display: grid; - grid-template-columns: 1fr auto auto; - align-items: center; - gap: 12px; - padding: 6px 12px; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 6px; - margin-bottom: 4px; -} - -.dashboard-target-info { - display: flex; - align-items: center; - gap: 6px; - min-width: 0; - overflow: hidden; -} - -.dashboard-target-icon { - font-size: 1rem; - flex-shrink: 0; -} - -.dashboard-target-name { - font-size: 0.85rem; - font-weight: 600; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: flex; - align-items: center; - gap: 4px; -} - -.dashboard-target-name .health-dot { - margin-right: 0; - flex-shrink: 0; -} - -.dashboard-target-subtitle { - font-size: 0.7rem; - color: var(--text-secondary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.dashboard-target-metrics { - display: flex; - gap: 12px; - align-items: center; -} - -.dashboard-metric { - text-align: center; - min-width: 48px; -} - -.dashboard-metric-value { - font-size: 0.85rem; - font-weight: 700; - color: var(--primary-color); - line-height: 1.2; -} - -.dashboard-metric-label { - font-size: 0.6rem; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.3px; -} - -.dashboard-fps-metric { - display: flex; - align-items: center; - gap: 6px; - min-width: auto; -} - -.dashboard-fps-sparkline { - position: relative; - width: 100px; - height: 36px; -} - -.dashboard-fps-label { - display: flex; - flex-direction: column; - align-items: center; - min-width: 36px; - line-height: 1.1; -} - -.dashboard-fps-target { - font-weight: 400; - opacity: 0.5; - font-size: 0.75rem; -} - -.dashboard-target-actions { - display: flex; - align-items: center; - gap: 4px; -} - -.dashboard-status-dot { - font-size: 1rem; - line-height: 1; -} - -.dashboard-status-dot.active { - color: #4CAF50; - animation: pulse 2s infinite; -} - -.dashboard-no-targets { - text-align: center; - padding: 32px 16px; - color: var(--text-secondary); - font-size: 0.9rem; -} - -.dashboard-badge-stopped { - padding: 2px 8px; - border-radius: 10px; - font-size: 0.7rem; - font-weight: 600; - background: var(--border-color); - color: var(--text-secondary); - flex-shrink: 0; -} - -.dashboard-badge-active { - padding: 2px 8px; - border-radius: 10px; - font-size: 0.7rem; - font-weight: 600; - background: var(--success-color, #28a745); - color: #fff; - flex-shrink: 0; -} - -.dashboard-profile .dashboard-target-metrics { - min-width: 48px; -} - -@media (max-width: 768px) { - .dashboard-target { - grid-template-columns: 1fr auto; - gap: 6px; - } - - .dashboard-target-metrics { - display: none; - } -} - -/* ===== PERFORMANCE CHARTS ===== */ - -.perf-charts-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 12px; -} - -.perf-chart-card { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 6px; - padding: 10px 12px; -} - -.perf-chart-wrap { - position: relative; - height: 60px; -} - -.perf-chart-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 4px; -} - -.perf-chart-label { - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.3px; - color: var(--text-secondary); -} - -.perf-chart-value { - font-size: 0.85rem; - font-weight: 700; -} - -.perf-chart-value.cpu { color: #2196F3; } -.perf-chart-value.ram { color: #4CAF50; } -.perf-chart-value.gpu { color: #FF9800; } - -.perf-chart-unavailable { - text-align: center; - padding: 20px 0; - color: var(--text-secondary); - font-size: 0.8rem; -} - -/* ===== PROFILES ===== */ - -.badge-profile-active { - background: var(--success-color, #28a745); - color: #fff; -} - -.badge-profile-inactive { - background: var(--border-color); - color: var(--text-color); -} - -.badge-profile-disabled { - background: var(--border-color); - color: var(--text-muted); - opacity: 0.7; -} - -.profile-status-disabled { - opacity: 0.6; -} - -.profile-logic-label { - font-size: 0.7rem; - font-weight: 600; - color: var(--text-muted); - padding: 0 4px; -} - -/* Profile condition editor rows */ -.profile-condition-row { - border: 1px solid var(--border-color); - border-radius: 6px; - padding: 10px; - margin-bottom: 8px; - background: var(--bg-secondary, var(--bg-color)); -} - -.condition-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.condition-type-label { - font-weight: 600; - font-size: 0.9rem; -} - -.btn-remove-condition { - background: none; - border: none; - color: var(--danger-color, #dc3545); - cursor: pointer; - font-size: 1rem; - padding: 2px 6px; -} - -.condition-fields { - display: flex; - flex-direction: column; - gap: 8px; -} - -.condition-field label { - display: block; - font-size: 0.85rem; - margin-bottom: 3px; - color: var(--text-muted); -} - -.condition-field select, -.condition-field textarea { - width: 100%; - padding: 6px 8px; - border: 1px solid var(--border-color); - border-radius: 4px; - background: var(--bg-color); - color: var(--text-color); - font-size: 0.9rem; - font-family: inherit; -} - -.condition-apps { - resize: vertical; - min-height: 60px; -} - -.condition-apps-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.btn-browse-apps { - background: none; - border: 1px solid var(--border-color); - color: var(--text-color); - font-size: 0.75rem; - padding: 2px 8px; - border-radius: 4px; - cursor: pointer; - transition: border-color 0.2s, background 0.2s; -} - -.btn-browse-apps:hover { - border-color: var(--primary-color); - background: rgba(33, 150, 243, 0.1); -} - -.process-picker { - margin-top: 6px; - border: 1px solid var(--border-color); - border-radius: 4px; - overflow: hidden; -} - -.process-picker-search { - width: 100%; - padding: 6px 8px; - border: none; - border-bottom: 1px solid var(--border-color); - background: var(--bg-color); - color: var(--text-color); - font-size: 0.85rem; - font-family: inherit; - outline: none; - box-sizing: border-box; -} - -.process-picker-list { - max-height: 160px; - overflow-y: auto; -} - -.process-picker-item { - padding: 4px 8px; - font-size: 0.8rem; - cursor: pointer; - transition: background 0.1s; -} - -.process-picker-item:hover { - background: rgba(33, 150, 243, 0.15); -} - -.process-picker-item.added { - color: var(--text-muted); - cursor: default; - opacity: 0.6; -} - -.process-picker-loading { - padding: 8px; - font-size: 0.8rem; - color: var(--text-muted); - text-align: center; -} - -/* Profile target checklist */ -.profile-targets-checklist { - max-height: 200px; - overflow-y: auto; - border: 1px solid var(--border-color); - border-radius: 4px; - padding: 6px; -} - -.profile-target-item { - display: flex; - align-items: center; - gap: 8px; - padding: 4px 6px; - cursor: pointer; - border-radius: 3px; -} - -.profile-target-item:hover { - background: var(--bg-secondary, var(--bg-color)); -} - -.profile-target-item input[type="checkbox"] { - margin: 0; -} - diff --git a/server/src/wled_controller/templates/index.html b/server/src/wled_controller/templates/index.html new file mode 100644 index 0000000..abf95bd --- /dev/null +++ b/server/src/wled_controller/templates/index.html @@ -0,0 +1,276 @@ + + + + + + LED Grab + + + + + + + + + + + + + + + +
+
+
+ +

LED Grab

+ +
+
+ API + + + + +
+
+ +
+
+ + + + +
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ + +
+ + +
+ +
+ + {% include 'modals/calibration.html' %} + {% include 'modals/device-settings.html' %} + {% include 'modals/target-editor.html' %} + {% include 'modals/kc-editor.html' %} + {% include 'modals/pattern-template.html' %} + {% include 'modals/api-key.html' %} + {% include 'modals/confirm.html' %} + {% include 'modals/add-device.html' %} + {% include 'modals/capture-template.html' %} + {% include 'modals/test-template.html' %} + {% include 'modals/test-stream.html' %} + {% include 'modals/test-pp-template.html' %} + {% include 'modals/stream.html' %} + {% include 'modals/pp-template.html' %} + {% include 'modals/profile-editor.html' %} + + {% include 'partials/tutorial-overlay.html' %} + {% include 'partials/image-lightbox.html' %} + {% include 'partials/display-picker.html' %} + + + + + diff --git a/server/src/wled_controller/templates/modals/add-device.html b/server/src/wled_controller/templates/modals/add-device.html new file mode 100644 index 0000000..3a27959 --- /dev/null +++ b/server/src/wled_controller/templates/modals/add-device.html @@ -0,0 +1,89 @@ + + diff --git a/server/src/wled_controller/templates/modals/api-key.html b/server/src/wled_controller/templates/modals/api-key.html new file mode 100644 index 0000000..b09f164 --- /dev/null +++ b/server/src/wled_controller/templates/modals/api-key.html @@ -0,0 +1,40 @@ + + diff --git a/server/src/wled_controller/templates/modals/calibration.html b/server/src/wled_controller/templates/modals/calibration.html new file mode 100644 index 0000000..ded8a84 --- /dev/null +++ b/server/src/wled_controller/templates/modals/calibration.html @@ -0,0 +1,144 @@ + + diff --git a/server/src/wled_controller/templates/modals/capture-template.html b/server/src/wled_controller/templates/modals/capture-template.html new file mode 100644 index 0000000..5e86374 --- /dev/null +++ b/server/src/wled_controller/templates/modals/capture-template.html @@ -0,0 +1,45 @@ + + diff --git a/server/src/wled_controller/templates/modals/confirm.html b/server/src/wled_controller/templates/modals/confirm.html new file mode 100644 index 0000000..ee1e5a0 --- /dev/null +++ b/server/src/wled_controller/templates/modals/confirm.html @@ -0,0 +1,16 @@ + + diff --git a/server/src/wled_controller/templates/modals/device-settings.html b/server/src/wled_controller/templates/modals/device-settings.html new file mode 100644 index 0000000..43531bc --- /dev/null +++ b/server/src/wled_controller/templates/modals/device-settings.html @@ -0,0 +1,90 @@ + + diff --git a/server/src/wled_controller/templates/modals/kc-editor.html b/server/src/wled_controller/templates/modals/kc-editor.html new file mode 100644 index 0000000..4b1564c --- /dev/null +++ b/server/src/wled_controller/templates/modals/kc-editor.html @@ -0,0 +1,80 @@ + + diff --git a/server/src/wled_controller/templates/modals/pattern-template.html b/server/src/wled_controller/templates/modals/pattern-template.html new file mode 100644 index 0000000..a5bbe48 --- /dev/null +++ b/server/src/wled_controller/templates/modals/pattern-template.html @@ -0,0 +1,67 @@ + + diff --git a/server/src/wled_controller/templates/modals/pp-template.html b/server/src/wled_controller/templates/modals/pp-template.html new file mode 100644 index 0000000..9d08124 --- /dev/null +++ b/server/src/wled_controller/templates/modals/pp-template.html @@ -0,0 +1,40 @@ + + diff --git a/server/src/wled_controller/templates/modals/profile-editor.html b/server/src/wled_controller/templates/modals/profile-editor.html new file mode 100644 index 0000000..2d14754 --- /dev/null +++ b/server/src/wled_controller/templates/modals/profile-editor.html @@ -0,0 +1,74 @@ + + diff --git a/server/src/wled_controller/templates/modals/stream.html b/server/src/wled_controller/templates/modals/stream.html new file mode 100644 index 0000000..f5d61ac --- /dev/null +++ b/server/src/wled_controller/templates/modals/stream.html @@ -0,0 +1,102 @@ + + diff --git a/server/src/wled_controller/templates/modals/target-editor.html b/server/src/wled_controller/templates/modals/target-editor.html new file mode 100644 index 0000000..b7b5131 --- /dev/null +++ b/server/src/wled_controller/templates/modals/target-editor.html @@ -0,0 +1,92 @@ + + diff --git a/server/src/wled_controller/templates/modals/test-pp-template.html b/server/src/wled_controller/templates/modals/test-pp-template.html new file mode 100644 index 0000000..a2a6a5d --- /dev/null +++ b/server/src/wled_controller/templates/modals/test-pp-template.html @@ -0,0 +1,27 @@ + + diff --git a/server/src/wled_controller/templates/modals/test-stream.html b/server/src/wled_controller/templates/modals/test-stream.html new file mode 100644 index 0000000..ae89c97 --- /dev/null +++ b/server/src/wled_controller/templates/modals/test-stream.html @@ -0,0 +1,23 @@ + + diff --git a/server/src/wled_controller/templates/modals/test-template.html b/server/src/wled_controller/templates/modals/test-template.html new file mode 100644 index 0000000..f3a008b --- /dev/null +++ b/server/src/wled_controller/templates/modals/test-template.html @@ -0,0 +1,31 @@ + + diff --git a/server/src/wled_controller/templates/partials/display-picker.html b/server/src/wled_controller/templates/partials/display-picker.html new file mode 100644 index 0000000..b9fb911 --- /dev/null +++ b/server/src/wled_controller/templates/partials/display-picker.html @@ -0,0 +1,10 @@ + + diff --git a/server/src/wled_controller/templates/partials/image-lightbox.html b/server/src/wled_controller/templates/partials/image-lightbox.html new file mode 100644 index 0000000..3c89adc --- /dev/null +++ b/server/src/wled_controller/templates/partials/image-lightbox.html @@ -0,0 +1,9 @@ + + diff --git a/server/src/wled_controller/templates/partials/tutorial-overlay.html b/server/src/wled_controller/templates/partials/tutorial-overlay.html new file mode 100644 index 0000000..b5b8047 --- /dev/null +++ b/server/src/wled_controller/templates/partials/tutorial-overlay.html @@ -0,0 +1,16 @@ + +
+
+
+
+
+ + +
+

+
+ + +
+
+
diff --git a/server/tests/test_api.py b/server/tests/test_api.py index 2501039..1cfbdda 100644 --- a/server/tests/test_api.py +++ b/server/tests/test_api.py @@ -10,13 +10,11 @@ client = TestClient(app) def test_root_endpoint(): - """Test root endpoint.""" + """Test root endpoint returns the HTML dashboard.""" response = client.get("/") assert response.status_code == 200 - data = response.json() - assert data["name"] == "WLED Screen Controller" - assert data["version"] == __version__ - assert "/docs" in data["docs"] + assert "text/html" in response.headers["content-type"] + assert "LED Grab" in response.text def test_health_check():