-
-
—
-
Rádio Indoor
-
-
Parado
+
diff --git a/frontend/src/app.css b/frontend/src/app.css
index 2b95e76..a1b3e07 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -1,407 +1,239 @@
-/* Indoor Radio — theme layer over the Industry tokens. Vanilla CSS, no build step. */
-
-:root, [data-theme="light"] {
- --color-bg: #f2f2f3;
- --color-surface: #e9e9ea;
- --color-text: #1d1f20;
- --color-accent: #5980a6;
- --color-divider: color-mix(in srgb, #1d1f20 16%, transparent);
- --muted: color-mix(in srgb, #1d1f20 52%, transparent);
- --tint: color-mix(in srgb, #1d1f20 5%, transparent);
- --tint-strong: color-mix(in srgb, #1d1f20 10%, transparent);
- --accent-tint: color-mix(in srgb, #5980a6 12%, transparent);
- --onbrand: #f2f2f3;
- --field: #1d2d3d;
-}
-
-[data-theme="dark"] {
- --color-bg: #17191a;
- --color-surface: #202325;
- --color-text: #eff0f2;
- --color-accent: #749dc4;
- --color-divider: color-mix(in srgb, #eff0f2 18%, transparent);
- --muted: color-mix(in srgb, #eff0f2 52%, transparent);
- --tint: color-mix(in srgb, #eff0f2 7%, transparent);
- --tint-strong: color-mix(in srgb, #eff0f2 13%, transparent);
- --accent-tint: color-mix(in srgb, #749dc4 16%, transparent);
- --onbrand: #14171a;
- --field: #0f1418;
+/* Spotify-style dark theme. Tokens from changes-request/004-layout-change/DESIGN.md. */
+@font-face {
+ font-family: "Figtree";
+ font-style: normal;
+ font-weight: 300 900;
+ font-display: swap;
+ src: url("./assets/fonts/figtree-latin-variable.woff2") format("woff2");
+}
+
+:root {
+ --bg: #121212;
+ --surface: #181818;
+ --mid: #1f1f1f;
+ --card-hover: #2a2a2a;
+ --text: #ffffff;
+ --muted: #b3b3b3;
+ --border: #4d4d4d;
+ --border-light: #7c7c7c;
+ --green: #1ed760;
+ --green-border: #1db954;
+ --red: #f3727f;
+ --orange: #ffa42b;
+ --blue: #539df5;
+ --shadow-heavy: rgba(0, 0, 0, .5) 0 8px 24px;
+ --shadow-mid: rgba(0, 0, 0, .3) 0 8px 8px;
+ --inset: rgb(18, 18, 18) 0 1px 0, rgb(124, 124, 124) 0 0 0 1px inset;
+ --font: "Figtree", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ --side-w: 300px;
+ --bar-h: 96px;
}
-html, body { height: 100%; }
+* { box-sizing: border-box; }
+html, body { height: 100%; margin: 0; }
body {
- margin: 0;
- background: var(--color-bg);
- color: var(--color-text);
- font-family: var(--font-body);
+ background: #000;
+ color: var(--text);
+ font-family: var(--font);
+ font-size: 14px;
+ -webkit-font-smoothing: antialiased;
overflow: hidden;
- -webkit-user-select: none;
user-select: none;
}
-* { box-sizing: border-box; }
-a { color: var(--color-accent); text-decoration: none; }
-a:hover { color: var(--color-accent-700); }
-::-webkit-scrollbar { width: 10px; }
-::-webkit-scrollbar-thumb { background: var(--tint-strong); }
-::-webkit-scrollbar-track { background: transparent; }
-
-.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; }
-.kicker {
- font-family: var(--font-heading);
- font-size: 11px;
- letter-spacing: 0.14em;
- text-transform: uppercase;
- color: var(--muted);
-}
-
-.icon-btn {
- display: inline-flex; align-items: center; justify-content: center;
- width: 34px; height: 34px;
- border: 1px solid var(--color-divider);
- background: transparent; color: var(--color-text);
- cursor: pointer;
-}
-.icon-btn:hover { background: var(--tint); }
-.icon-btn:active { background: var(--tint-strong); }
-.icon-btn[aria-pressed="true"] { background: var(--tint-strong); }
-
-/* spot buttons — blueprint cells, the one grid that repeats */
-.spot {
- position: relative;
- display: flex; flex-direction: column; gap: 10px;
- min-height: 118px; padding: 13px;
- border: 1px solid var(--color-divider);
- background: transparent; color: var(--color-text);
- font-family: var(--font-body); text-align: left; cursor: pointer;
-}
-.spot:hover { background: var(--tint); }
-.spot:active { background: var(--tint-strong); }
-.spot[aria-current="true"] { border-color: var(--color-accent); background: var(--accent-tint); }
-.spot[disabled] { opacity: 0.45; cursor: not-allowed; }
-.spot.is-pending, .spot.is-failed { opacity: 1; cursor: not-allowed; }
-.spot-veil {
- position: absolute; inset: 0; z-index: 2;
- display: flex; align-items: center; justify-content: center;
- padding: 12px; text-align: center;
- background: color-mix(in srgb, var(--color-canvas, #111) 78%, transparent);
- font-family: var(--font-body); font-size: 13px; line-height: 1.3;
- color: var(--color-text);
-}
-.spot-title { font-family: var(--font-heading); font-size: 18px; line-height: 1.14; }
-.spot-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
-.spot-foot { margin-top: auto; }
-.spot.has-art {
- background-size: cover;
- background-position: center;
- background-repeat: no-repeat;
- min-height: 140px;
- justify-content: flex-end;
-}
-.spot.has-art.has-label::before {
- content: "";
- position: absolute;
- inset: 0;
- z-index: 0;
- pointer-events: none;
- background: linear-gradient(to top, color-mix(in srgb, var(--color-bg) 72%, transparent), transparent 58%);
-}
-.spot.has-art .spot-title,
-.spot.has-art .kicker,
-.spot.has-art .spot-foot {
- position: relative;
- z-index: 1;
- color: var(--color-text);
-}
-
-.filter {
- font-family: var(--font-heading); font-size: 13px; letter-spacing: 0.06em;
- text-transform: uppercase; padding: 6px 12px;
- border: 1px solid var(--color-divider); background: transparent;
- color: var(--muted); cursor: pointer;
-}
-.filter:hover { background: var(--tint); color: var(--color-text); }
-.filter[aria-pressed="true"] { background: var(--color-accent); border-color: var(--color-accent); color: var(--onbrand); }
-
-.slider { width: 104px; height: 24px; display: flex; align-items: center; cursor: pointer; }
-.slider-track { position: relative; width: 100%; height: 4px; background: var(--tint-strong); }
-.slider-fill { display: block; height: 100%; background: var(--color-text); }
-.slider-knob { position: absolute; top: -4px; width: 2px; height: 12px; background: var(--color-text); }
-
-:focus { outline: none; }
-:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
-
-@keyframes bars { 0%, 100% { transform: scaleY(0.3); } 50% { transform: scaleY(1); } }
-@keyframes chime { 0% { opacity: 0.25; } 50% { opacity: 1; } 100% { opacity: 0.25; } }
-.eq {
- display: flex; align-items: flex-end; gap: 2px;
- height: 14px; flex: none;
-}
-.eq[hidden] { display: none; }
-.eq i {
- display: block; width: 2px; height: 14px;
- background: var(--color-accent); transform-origin: bottom;
- animation: bars 900ms ease-in-out infinite;
-}
-.eq i:nth-child(2) { animation-delay: 140ms; }
-.eq i:nth-child(3) { animation-delay: 280ms; }
-.eq i:nth-child(4) { animation-delay: 420ms; }
-.chiming { animation: chime 700ms ease-in-out infinite; }
-
-/* hover states declarados aqui (o export estático não usa atributos style-hover) */
-#chimeToggle:hover { background: var(--tint); }
-#play:hover { filter: brightness(1.08); }
-
-.shell {
- position: relative;
- width: 100%; height: 100vh; min-width: 980px; min-height: 640px;
- display: grid;
- grid-template-rows: 66px auto 54px minmax(0, 1fr) 112px;
- grid-template-areas:
- "top"
- "update"
- "toolbar"
- "main"
- "bar";
- background: var(--color-bg); color: var(--color-text); overflow: hidden;
-}
-.top { grid-area: top; }
-.update-bar { grid-area: update; }
-.toolbar { grid-area: toolbar; }
-#main { grid-area: main; }
-.bar { grid-area: bar; }
-.top {
- display: grid; grid-template-columns: auto 1fr auto; align-items: center;
- gap: 20px; padding: 0 20px; border-bottom: 1px solid var(--color-divider);
-}
-.brand-block { display: flex; align-items: center; gap: 16px; }
-.logo-slot {
- width: 168px; height: 42px; display: flex; align-items: center; justify-content: center;
- overflow: hidden;
-}
-.logo-slot img { max-height: 40px; max-width: 160px; object-fit: contain; }
-.logo-ph { font-size: 10px; letter-spacing: 0.16em; color: var(--muted); }
-.store-name { font-family: var(--font-heading); font-size: 17px; line-height: 1.1; }
-.store-unit-row { display: flex; align-items: center; gap: 6px; margin-top: 2px; }
-.store-unit { font-size: 11px; letter-spacing: 0.08em; color: var(--muted); }
-.unit-copy { width: 22px; height: 22px; flex: 0 0 22px; }
-.unit-copy svg { display: block; }
-.top-right { display: flex; align-items: center; gap: 12px; }
-.conn-pill {
- display: flex; align-items: center; gap: 7px; padding: 5px 11px;
- border: 1px solid var(--color-divider);
-}
-.conn-pill i { width: 7px; height: 7px; display: block; background: var(--muted); }
-.conn-pill.ok i { background: var(--color-accent); }
-.clock { font-size: 12px; color: var(--muted); }
-
-.update-bar {
- display: flex; align-items: center; gap: 14px; padding: 10px 20px;
- background: var(--field); color: #f2f2f3; font-size: 14px;
-}
-.update-bar[hidden] { display: none; }
-.update-bar.warn { background: #3a2a12; color: #f2c56b; }
-
-.toolbar {
- display: flex; align-items: center; gap: 16px; padding: 0 20px;
- border-bottom: 1px solid var(--color-divider);
-}
-.filters { display: flex; gap: 6px; }
-.grid-count { font-size: 11px; color: var(--muted); margin-left: auto; }
-.sync-mini {
- display: flex; flex-direction: column; gap: 3px;
- flex: 1; min-width: 220px; max-width: 460px;
-}
-.sync-line { display: flex; align-items: center; gap: 8px; }
-.sync-track { flex: 1; height: 4px; background: var(--tint-strong); }
-.sync-track i { display: block; height: 100%; width: 0; background: var(--color-accent); }
-.sync-detail { font-size: 11px; color: var(--muted); line-height: 1.3; }
-#chimeToggle {
- display: flex; align-items: center; gap: 9px; padding: 5px 11px;
- border: 1px solid var(--color-divider); background: transparent;
- color: var(--color-text); cursor: pointer;
-}
-#chimeState { font-family: var(--font-heading); font-size: 13px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-accent); }
-
-#main { overflow: auto; padding: 16px 20px 20px; }
-#main.grid-spots {
- display: grid; grid-template-columns: repeat(auto-fill, minmax(238px, 1fr));
- gap: 12px; align-content: start;
-}
-
-.hist-line {
- display: grid;
- grid-template-columns: 58px 88px minmax(0, 1fr);
- grid-template-areas: "time kind title";
- align-items: center; gap: 14px; padding: 8px 4px;
- border-top: 1px solid color-mix(in srgb, var(--color-text) 7%, transparent);
-}
-.hist-time { grid-area: time; }
-.hist-line .badge { grid-area: kind; }
-.hist-title { grid-area: title; }
-.hist-line .badge {
- font-family: var(--font-heading); font-size: 10px; letter-spacing: 0.12em;
- text-transform: uppercase; text-align: center; padding: 2px 0;
- border: 1px solid var(--color-divider); color: var(--muted);
-}
-.hist-line .badge:empty { border-color: transparent; }
-.hist-line .badge.is-spot { background: color-mix(in srgb, var(--color-text) 8%, transparent); }
-.hist-title {
- display: block; width: 100%; min-width: 0;
- margin: 0; padding: 0; border: 0; background: none;
- color: inherit; font: inherit; text-align: left; cursor: pointer;
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
-}
-.hist-title:hover { color: var(--color-accent); }
-
-.call-wrap { max-width: 520px; display: flex; flex-direction: column; gap: 14px; }
-.call-tabs { display: flex; gap: 6px; }
-.call-wrap label { font-family: var(--font-heading); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
-.call-wrap input:not([type="radio"]), .call-wrap select, .call-wrap textarea {
- width: 100%; padding: 8px 10px; border: 1px solid var(--color-divider);
- background: var(--color-surface); color: var(--color-text); font-family: var(--font-body); font-size: 15px;
-}
-.gender-pick { display: flex; gap: 8px; }
-.gender-pick label {
- flex: 1; display: flex; flex-direction: row; align-items: center; gap: 8px;
- margin: 0; padding: 8px 12px; border: 1px solid var(--color-divider);
- text-transform: none; letter-spacing: 0; font-size: 15px; font-family: var(--font-body);
- color: var(--color-text); cursor: pointer; background: var(--color-surface);
-}
-.gender-pick input[type="radio"] { width: auto; padding: 0; accent-color: var(--color-accent); }
-.gender-pick label:has(input:checked) { border-color: var(--color-accent); background: var(--accent-tint); }
-.call-wrap textarea { min-height: 90px; resize: vertical; }
-.call-actions { display: flex; gap: 10px; align-items: center; }
-
-.bar {
- display: grid; grid-template-columns: 88px 1fr 260px; align-items: center;
- gap: 22px; padding: 0 20px; border-top: 1px solid var(--color-divider);
- background: var(--color-surface);
- height: 112px; min-height: 112px; max-height: 112px;
-}
-.play-btn {
- width: 72px; height: 78px; display: flex; flex-direction: column;
- align-items: center; justify-content: center; gap: 4px;
- border: 1px solid var(--color-accent); background: var(--color-accent); color: var(--onbrand); cursor: pointer;
-}
-.play-label {
- font-family: var(--font-heading); font-size: 11px; letter-spacing: 0.08em;
- text-transform: uppercase; line-height: 1;
-}
-.now { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
-.now-row { display: flex; align-items: center; gap: 12px; min-width: 0; }
-.np-kind { font-size: 10px; padding: 3px 8px; border: 1px solid var(--color-divider); flex: none; }
-#npTitle { font-family: var(--font-heading); font-size: 21px; line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.np-state { font-size: 11px; color: var(--muted); margin-left: auto; flex: none; }
-.prog { flex: 1; height: 4px; background: var(--tint-strong); }
-.prog i { display: block; height: 100%; width: 0; background: var(--color-accent); }
-.vol-wrap { display: flex; align-items: center; justify-content: flex-end; gap: 12px; }
-.mute-btn {
- display: inline-flex; flex-direction: column; align-items: center;
- width: auto; min-width: 44px; height: auto; padding: 6px 8px;
- gap: 4px;
-}
-.mute-label {
- font-family: var(--font-heading); font-size: 10px; letter-spacing: 0.08em;
- text-transform: uppercase; line-height: 1;
-}
-#vol { width: 104px; accent-color: var(--color-text); }
-#volRead { font-size: 11px; color: var(--muted); width: 28px; text-align: right; }
-
-.state-banner {
- position: fixed;
- left: 50%; top: 42%;
- transform: translate(-50%, -50%);
- z-index: 40;
- pointer-events: none;
- padding: 14px 28px;
- background: color-mix(in srgb, var(--color-text) 88%, transparent);
- color: var(--color-bg);
- font-family: var(--font-heading);
- font-size: 18px;
- letter-spacing: 0.04em;
- text-align: center;
- max-width: 72%;
-}
-.state-banner-mute { top: 52%; }
-.state-banner[hidden] { display: none; }
-
-.debug-overlay {
- position: fixed; inset: 0; z-index: 80;
- background: color-mix(in srgb, #000 45%, transparent);
- display: flex; align-items: center; justify-content: center;
- padding: 24px;
-}
-.debug-overlay[hidden] { display: none; }
-.debug-panel {
- width: min(560px, 100%);
- max-height: min(80vh, 720px);
- overflow: auto;
- background: var(--color-surface);
- color: var(--color-text);
- border: 1px solid var(--color-divider);
- padding: 16px 18px 18px;
-}
-.debug-head {
- display: flex; align-items: center; justify-content: space-between;
- gap: 12px; margin-bottom: 12px;
-}
-.debug-head b { font-family: var(--font-heading); font-size: 16px; letter-spacing: 0.06em; text-transform: uppercase; }
-.debug-body { font-size: 13px; line-height: 1.45; }
-.debug-row {
- display: grid; grid-template-columns: 148px 1fr; gap: 8px;
- padding: 5px 0; border-top: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent);
-}
+button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
+button:disabled { cursor: default; }
+:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
+.mono { font-variant-numeric: tabular-nums; letter-spacing: .02em; }
+.kicker { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.4px; color: var(--muted); }
+.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
+[hidden] { display: none !important; }
+
+/* shell */
+.shell { height: 100vh; display: grid; grid-template-rows: 1fr var(--bar-h); }
+.body { display: grid; grid-template-columns: var(--side-w) 1fr; min-height: 0; padding: 8px 8px 0; gap: 8px; }
+@media (max-width: 1100px) { :root { --side-w: 240px; } }
+
+/* sidebar */
+.side { background: var(--bg); border-radius: 8px; display: flex; flex-direction: column; min-height: 0; }
+.brand { padding: 20px 20px 12px; display: flex; flex-direction: column; gap: 14px; }
+.logo-slot { height: 44px; display: flex; align-items: center; justify-content: center; }
+.logo-slot img { max-height: 44px; max-width: 220px; width: auto; }
+.logo-ph { font-size: 10px; letter-spacing: .16em; color: var(--muted); }
+.store-name { font-size: 16px; font-weight: 700; line-height: 1.2; }
+.store-unit-row { display: flex; align-items: center; gap: 8px; margin-top: 4px; }
+.store-unit { font-size: 12px; color: var(--muted); letter-spacing: .08em; }
+.nav { display: flex; flex-direction: column; gap: 4px; padding: 8px 12px; }
+.nav .filter { display: flex; align-items: center; gap: 14px; padding: 10px 12px; border-radius: 6px; font-size: 14px; font-weight: 400; color: var(--muted); text-align: left; }
+.nav .filter svg { width: 22px; height: 22px; fill: currentColor; flex: none; }
+.nav .filter:hover { color: var(--text); }
+.nav .filter[aria-pressed="true"] { color: var(--text); font-weight: 700; background: var(--mid); }
+.nav .count { margin-left: auto; font-size: 12px; color: var(--muted); font-weight: 400; }
+.side-foot { margin-top: auto; padding: 12px 20px 18px; display: flex; flex-direction: column; gap: 12px; border-top: 1px solid #222; }
+.sync-mini { display: flex; flex-direction: column; gap: 6px; }
+.cache-row { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); }
+.track { height: 4px; border-radius: 2px; background: var(--border); overflow: hidden; }
+.track i { display: block; height: 100%; width: 0; background: var(--green); transition: width .3s; }
+.sync-detail { font-size: 11px; color: var(--muted); }
+.toggle { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-radius: 9999px; background: var(--mid); font-size: 13px; font-weight: 700; }
+.toggle .sw { width: 36px; height: 20px; border-radius: 10px; background: var(--border); position: relative; transition: background .15s; }
+.toggle .sw::after { content: ""; position: absolute; top: 3px; left: 3px; width: 14px; height: 14px; border-radius: 50%; background: #fff; transition: left .15s; }
+.toggle[aria-pressed="true"] .sw { background: var(--green); }
+.toggle[aria-pressed="true"] .sw::after { left: 19px; }
+.conn { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
+.conn i { width: 8px; height: 8px; border-radius: 50%; background: var(--red); }
+.conn.ok i { background: var(--green); box-shadow: 0 0 8px var(--green); }
+.version { font-size: 11px; color: var(--muted); display: flex; justify-content: space-between; }
+
+/* main pane */
+.main { background: var(--bg); border-radius: 8px; min-height: 0; display: flex; flex-direction: column; overflow: hidden; background-image: linear-gradient(180deg, #2a2a2a 0, var(--bg) 260px); }
+.topbar { display: flex; align-items: center; gap: 12px; padding: 14px 24px 0; }
+.grid-count { font-size: 11px; color: var(--muted); letter-spacing: .08em; }
+.clock { margin-left: auto; font-size: 13px; color: var(--muted); }
+.icon-btn { width: 32px; height: 32px; border-radius: 50%; background: rgba(0, 0, 0, .6); display: inline-flex; align-items: center; justify-content: center; color: var(--muted); font-weight: 700; }
+.icon-btn:hover { color: var(--text); background: #000; }
+.icon-btn-sm { width: 24px; height: 24px; }
+.icon-btn-sm svg { width: 12px; height: 12px; }
+.update-bar { margin: 12px 24px 0; padding: 10px 16px; border-radius: 8px; background: var(--blue); color: #000; font-size: 13px; font-weight: 700; box-shadow: var(--shadow-mid); }
+.state-banner { margin: 12px 24px 0; padding: 12px 20px; border-radius: 8px; background: var(--orange); color: #000; font-weight: 700; box-shadow: var(--shadow-mid); }
+.state-banner-idle { background: var(--mid); color: var(--muted); font-weight: 400; box-shadow: none; }
+.state-banner-mute { background: var(--red); }
+.head { padding: 22px 24px 8px; display: flex; align-items: baseline; gap: 14px; }
+.head h1 { font-size: 24px; font-weight: 700; margin: 0; }
+.head .sub { font-size: 13px; color: var(--muted); }
+#main { padding: 8px 24px 24px; overflow: auto; min-height: 0; flex: 1; }
+.empty { color: var(--muted); font-size: 14px; }
+
+/* spots */
+.grid-spots { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 16px; align-content: start; }
+.spot { position: relative; padding: 14px; border-radius: 8px; background: var(--surface); text-align: left; display: flex; flex-direction: column; gap: 12px; transition: background .2s; }
+.spot:hover { background: var(--card-hover); }
+.spot .art { aspect-ratio: 1; border-radius: 6px; background: linear-gradient(135deg, #2b2b2b, #3d3d3d); box-shadow: var(--shadow-mid); display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; background-size: cover; background-position: center; }
+.spot .glyph { font-size: 38px; font-weight: 700; color: #6b6b6b; letter-spacing: -.04em; }
+.spot.has-art .glyph { display: none; }
+.spot .spot-title { font-size: 15px; font-weight: 700; line-height: 1.25; }
+.spot .kicker { display: block; margin-top: 2px; }
+.play-fab { position: absolute; right: 10px; bottom: 10px; width: 44px; height: 44px; border-radius: 50%; background: var(--green); display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow-mid); opacity: 0; transform: translateY(8px); transition: all .2s; }
+.play-fab svg { width: 20px; height: 20px; fill: #000; margin-left: 2px; }
+.spot:hover .play-fab { opacity: 1; transform: translateY(0); }
+.spot:disabled { cursor: default; }
+.spot:disabled:hover { background: var(--surface); }
+.spot:disabled .play-fab { display: none; }
+.spot.is-pending .art, .spot.is-failed .art { filter: grayscale(1) brightness(.6); }
+.spot .state { font-size: 12px; font-weight: 600; margin-top: 4px; }
+.spot .state.dl { color: var(--orange); }
+.spot .state.err { color: var(--red); }
+.badge { display: inline-block; padding: 2px 8px; border-radius: 2px; font-size: 10.5px; font-weight: 600; text-transform: capitalize; background: var(--mid); color: var(--muted); }
+.badge.is-spot { background: var(--green); color: #000; }
+.badge.is-live { background: var(--red); color: #000; }
+
+/* history */
+.hist-line { display: grid; grid-template-columns: 56px 80px 1fr 40px; align-items: center; gap: 14px; padding: 10px 12px; border-radius: 6px; font-size: 14px; }
+.hist-line:hover { background: rgba(255, 255, 255, .06); }
+.hist-time { color: var(--muted); font-size: 13px; }
+.hist-title { text-align: left; padding: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.hist-replay { width: 32px; height: 32px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: var(--muted); opacity: 0; }
+.hist-line:hover .hist-replay { opacity: 1; color: var(--text); }
+.hist-replay svg { width: 16px; height: 16px; fill: currentColor; }
+
+/* calls */
+.call-wrap { display: flex; flex-direction: column; gap: 20px; }
+.call-tabs { display: flex; gap: 8px; }
+.call-tabs .filter { padding: 8px 16px; border-radius: 9999px; background: var(--mid); color: var(--muted); font-size: 13px; font-weight: 700; }
+.call-tabs .filter[aria-pressed="true"] { background: var(--text); color: #000; }
+.call-form { display: grid; grid-template-columns: repeat(2, minmax(220px, 360px)); gap: 16px 20px; }
+.call-form label { display: flex; flex-direction: column; gap: 8px; font-size: 12px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: 1.4px; }
+.call-form input, .call-form textarea { font: inherit; font-size: 15px; color: var(--text); background: var(--mid); border: 0; border-radius: 500px; padding: 12px 20px; box-shadow: var(--inset); outline: 0; user-select: text; }
+.call-form textarea { border-radius: 16px; min-height: 96px; resize: vertical; }
+.call-form label.wide { grid-column: 1 / -1; }
+.call-form input:focus, .call-form textarea:focus { box-shadow: rgb(18, 18, 18) 0 1px 0, #fff 0 0 0 1px inset; }
+.call-actions { grid-column: 1 / -1; display: flex; align-items: center; gap: 14px; margin-top: 6px; }
+.gender-pick { display: flex; gap: 16px; font-size: 14px; color: var(--text); text-transform: none; letter-spacing: 0; font-weight: 400; }
+.cta { padding: 14px 32px; border-radius: 500px; background: var(--green); color: #000; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.4px; transition: transform .1s; }
+.cta:hover { transform: scale(1.04); background: #3be477; }
+.cta:disabled { background: var(--border); color: var(--muted); transform: none; }
+
+/* unregistered support */
+.support-main { display: block; }
+.support-card { max-width: 620px; margin: 24px auto; padding: 32px; border-radius: 8px; background: var(--surface); box-shadow: var(--shadow-mid); display: grid; grid-template-columns: 1fr 148px; gap: 24px; align-items: center; }
+.support-copy p { margin: 0 0 10px; }
+.support-lead { font-size: 18px; font-weight: 700; }
+.support-lines { color: var(--muted); }
+.support-qr { padding: 0; border-radius: 8px; overflow: hidden; background: #fff; }
+.support-qr img { display: block; }
+
+/* now playing bar */
+.now { background: #000; display: grid; grid-template-columns: 30% 1fr 30%; align-items: center; padding: 0 16px; gap: 16px; }
+.np-left { display: flex; align-items: center; gap: 14px; min-width: 0; }
+.cover { width: 56px; height: 56px; border-radius: 4px; background: linear-gradient(135deg, #3a3a3a, #202020); flex: none; display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow-mid); }
+.cover svg { width: 24px; height: 24px; fill: var(--muted); }
+.np-meta { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
+.np-title { font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.np-sub { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 8px; }
+.np-center { display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 0; }
+.play { width: 40px; height: 40px; border-radius: 50%; background: #fff; color: #000; display: flex; align-items: center; justify-content: center; transition: transform .1s; }
+.play:hover { transform: scale(1.06); }
+.play:disabled { background: var(--border); transform: none; }
+.play svg { width: 18px; height: 18px; fill: #000; }
+.play #playIcon { display: inline-flex; }
+.cut { width: 32px; height: 32px; border-radius: 50%; color: var(--muted); display: flex; align-items: center; justify-content: center; }
+.cut:hover, .cut.on { color: var(--text); }
+.cut svg { width: 18px; height: 18px; fill: currentColor; }
+.prog { width: 100%; max-width: 560px; display: flex; align-items: center; gap: 10px; font-size: 11px; color: var(--muted); }
+.bar { flex: 1; height: 4px; border-radius: 2px; background: var(--border); position: relative; overflow: hidden; }
+.bar i { display: block; height: 100%; width: 0; border-radius: 2px; background: #fff; }
+.prog:hover .bar i { background: var(--green); }
+.np-right { display: flex; align-items: center; justify-content: flex-end; gap: 10px; }
+.vol { width: 110px; -webkit-appearance: none; appearance: none; height: 4px; border-radius: 2px; background: linear-gradient(90deg, #fff 0 var(--vol-pct, 85%), var(--border) var(--vol-pct, 85%)); }
+.vol::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #fff; opacity: 0; }
+.vol:hover { background: linear-gradient(90deg, var(--green) 0 var(--vol-pct, 85%), var(--border) var(--vol-pct, 85%)); }
+.vol:hover::-webkit-slider-thumb { opacity: 1; }
+.vol-read { font-size: 12px; color: var(--muted); width: 24px; }
+.eq { display: inline-flex; align-items: flex-end; gap: 2px; height: 12px; }
+.eq i { width: 3px; background: var(--green); animation: eq 1s infinite ease-in-out; }
+.eq i:nth-child(1) { height: 60%; animation-delay: 0s; }
+.eq i:nth-child(2) { height: 100%; animation-delay: .2s; }
+.eq i:nth-child(3) { height: 40%; animation-delay: .4s; }
+.eq i:nth-child(4) { height: 80%; animation-delay: .1s; }
+@keyframes eq { 0%, 100% { transform: scaleY(.4); } 50% { transform: scaleY(1); } }
+
+/* on air */
+.onair { position: fixed; left: 50%; bottom: calc(var(--bar-h) + 16px); transform: translateX(-50%); display: flex; align-items: center; gap: 16px; padding: 12px 12px 12px 20px; border-radius: 9999px; background: var(--surface); box-shadow: var(--shadow-heavy); border: 1px solid var(--green-border); max-width: 70vw; z-index: 20; }
+.onair .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--red); box-shadow: 0 0 10px var(--red); animation: blink 1s infinite; flex: none; }
+@keyframes blink { 50% { opacity: .3; } }
+.onair .txt { display: flex; flex-direction: column; gap: 2px; font-size: 14px; font-weight: 700; min-width: 0; }
+.onair .txt #airText { font-weight: 400; color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.onair .air-time { color: var(--muted); font-size: 12px; flex: none; }
+.onair .stop { padding: 8px 18px; border-radius: 9999px; border: 1px solid var(--border-light); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.4px; flex: none; }
+.onair .stop:hover { border-color: #fff; }
+
+/* toasts */
+.toasts { position: fixed; right: 24px; bottom: calc(var(--bar-h) + 16px); display: flex; flex-direction: column; gap: 8px; z-index: 30; }
+.toast { padding: 12px 18px; border-radius: 8px; background: var(--surface); box-shadow: var(--shadow-heavy); border-left: 4px solid var(--green); font-size: 13px; min-width: 220px; }
+.toast.err { border-left-color: var(--red); }
+.toast b { display: block; }
+
+/* service desk dialog */
+.debug-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, .7); display: flex; align-items: center; justify-content: center; z-index: 40; }
+.debug-panel { width: min(720px, 92vw); max-height: 88vh; display: flex; flex-direction: column; background: var(--surface); border-radius: 8px; box-shadow: var(--shadow-heavy); }
+.debug-head { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; font-size: 18px; }
+.debug-body { padding: 0 20px 12px; overflow: auto; font-size: 13px; user-select: text; }
+.debug-contact { display: grid; grid-template-columns: 1fr 120px; gap: 16px; align-items: center; padding: 12px; border-radius: 8px; background: var(--mid); margin-bottom: 12px; }
+.debug-contact img { width: 120px; height: 120px; border-radius: 6px; cursor: pointer; }
+.debug-contact p { margin: 0 0 6px; }
+.debug-row { display: grid; grid-template-columns: 150px 1fr; gap: 12px; padding: 6px 0; border-bottom: 1px solid #222; }
.debug-row span:first-child { color: var(--muted); }
-.debug-contact { display: flex; align-items: center; gap: 16px; margin: 8px 0 14px; }
-.debug-contact img { width: 96px; height: 96px; background: #fff; padding: 6px; cursor: pointer; border: 1px solid var(--color-divider); }
-.debug-actions { margin-top: 14px; display: flex; justify-content: flex-end; }
-
-.onair {
- position: fixed; left: 20px; right: 20px; bottom: 124px;
- display: flex; align-items: center; gap: 12px; padding: 8px 14px;
- background: var(--color-text); color: var(--color-bg); z-index: 50;
-}
-.onair[hidden] { display: none; }
-.toasts { position: fixed; top: 16px; right: 16px; display: flex; flex-direction: column; gap: 8px; z-index: 60; }
-.toast { padding: 10px 14px; background: var(--color-surface); border: 1px solid var(--color-divider); max-width: 320px; }
-.toast.err { border-color: #c46a4a; }
-.toast b { display: block; font-family: var(--font-heading); }
-.empty { color: var(--muted); font-size: 14px; padding: 40px; text-align: center; }
-
-#main.support-main {
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 100%;
-}
-.support-card {
- display: flex;
- align-items: center;
- gap: 28px;
- max-width: 720px;
- padding: 8px 12px;
-}
-.support-copy { min-width: 0; }
-.support-lead {
- margin: 0 0 12px;
- font-family: var(--font-heading);
- font-size: 20px;
- line-height: 1.35;
-}
-.support-lines {
- margin: 0;
- color: var(--muted);
- font-size: 15px;
- line-height: 1.5;
-}
-.support-lines + .support-lines { margin-top: 4px; }
-.support-qr {
- flex: none;
- margin: 0;
- padding: 8px;
- border: 1px solid var(--color-divider);
- background: #fff;
- cursor: pointer;
-}
-.support-qr:hover { outline: 2px solid var(--color-accent); outline-offset: 2px; }
-.support-qr img { display: block; width: 148px; height: 148px; }
+.debug-actions { padding: 12px 20px 20px; display: flex; justify-content: flex-end; }
+
+/* combo suggestions (replaces the native datalist) */
+.combo { position: relative; display: flex; flex-direction: column; }
+.combo input { width: 100%; }
+.combo-menu { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 15; background: var(--surface); border-radius: 8px; box-shadow: var(--shadow-heavy); padding: 6px; max-height: 240px; overflow: auto; display: flex; flex-direction: column; gap: 2px; }
+.combo-item { text-align: left; padding: 10px 12px; border-radius: 6px; font-size: 14px; font-weight: 400; color: var(--text); text-transform: none; letter-spacing: 0; }
+.combo-item:hover, .combo-item.is-active { background: var(--card-hover); }
+
+/* frameless window: drag regions and controls */
+.topbar, .brand { --wails-draggable: drag; }
+.topbar button, .brand button, .win { --wails-draggable: no-drag; }
+.win { display: inline-flex; gap: 6px; margin-left: 8px; padding-left: 12px; border-left: 1px solid #333; }
+.win .icon-btn svg { width: 14px; height: 14px; fill: currentColor; }
+.win-close:hover { background: var(--red); color: #000; }
diff --git a/frontend/src/main.js b/frontend/src/main.js
index e4612d7..6b0bd89 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -1,6 +1,5 @@
-import './industry.css';
import './app.css';
-import {BrowserOpenURL, ClipboardSetText, EventsOn} from '../wailsjs/runtime/runtime';
+import {BrowserOpenURL, ClipboardSetText, EventsOn, Quit, WindowMinimise, WindowToggleMaximise} from '../wailsjs/runtime/runtime';
import qrWhatsapp from './assets/images/whatsapp-suporte.png';
import {
AnnounceCall,
@@ -23,21 +22,18 @@ const el = (tag, cls, text) => {
return n;
};
const ICON = {
- play: '
',
- pause: '
',
- sun: '
',
- moon: '
',
- vol: '
',
- muted: '
',
- copy: '
',
-};
-const marks = node => {
- ['tl', 'tr', 'bl', 'br'].forEach(p => {
- const i = document.createElement('i');
- i.className = 'corner ' + p;
- node.appendChild(i);
- });
+ play: '
',
+ pause: '
',
+ vol: '
',
+ muted: '
',
+ copy: '
',
+ note: '
',
+ spots: '
',
+ history: '
',
+ calls: '
',
};
+const HISTORY_LIMIT = 15;
+const VIEW_TITLE = {spots: 'Spots', history: 'Últimos tocados', calls: 'Chamadas'};
const state = {
view: 'spots',
@@ -104,12 +100,6 @@ function tickClock() {
$('clock').textContent = String(n.getHours()).padStart(2, '0') + ':' + String(n.getMinutes()).padStart(2, '0');
}
-function applyTheme(theme) {
- document.documentElement.setAttribute('data-theme', theme);
- $('theme').innerHTML = theme === 'dark' ? ICON.sun : ICON.moon;
- localStorage.setItem('theme', theme);
-}
-
function paintFilters() {
const host = $('filters');
host.replaceChildren();
@@ -122,12 +112,17 @@ function paintFilters() {
if (state.view === 'spots' && !(state.deskapp.buttons || []).length) state.view = tabs[0] ? tabs[0].id : 'history';
if (state.view === 'calls' && !(state.deskapp.calls || []).length) state.view = tabs[0] ? tabs[0].id : 'history';
for (const t of tabs) {
- const b = el('button', 'filter', t.name);
+ const b = el('button', 'filter');
b.type = 'button';
+ b.innerHTML = ICON[t.id] || '';
+ b.append(el('span', null, t.name));
+ if (t.id === 'spots') b.append(el('span', 'count', String((state.deskapp.buttons || []).length)));
b.setAttribute('aria-pressed', String(state.view === t.id));
b.addEventListener('click', () => { state.viewLocked = true; state.view = t.id; paint(); });
host.append(b);
}
+ $('viewTitle').textContent = state.unregistered && state.view === 'history' ? 'Player não cadastrado' : (VIEW_TITLE[state.view] || '');
+ $('viewSub').textContent = state.view === 'spots' ? 'Toque um botão para anunciar' : state.view === 'calls' && state.deskapp.voice && state.deskapp.voice.name ? 'Voz: ' + state.deskapp.voice.name : '';
}
function paintChime() {
@@ -151,32 +146,28 @@ function paintSpots() {
const imageUrl = String(b.imageUrl || b.imageURL || '').trim();
const hasArt = /^https:\/\//i.test(imageUrl);
const label = String(b.label || '').trim();
- const card = el('button', 'spot blueprint'
+ const card = el('button', 'spot'
+ (pending ? ' is-pending' : b.failed ? ' is-failed' : '')
- + (hasArt ? ' has-art' : '')
- + (hasArt && label ? ' has-label' : ''));
+ + (hasArt ? ' has-art' : ''));
card.type = 'button';
card.disabled = !b.ready || fireLocked();
card.setAttribute('aria-label', label || b.id);
+ card.title = b.failed ? 'Indisponível no momento: falha no download' : pending ? 'Indisponível no momento: arquivo baixando' : label;
+ const art = el('span', 'art');
if (hasArt) {
- card.style.backgroundImage = 'url("' + imageUrl.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '")';
- }
- if (label) {
- const row = el('span', 'spot-row');
- row.append(el('span', 'kicker', b.group || 'Spot'));
- card.append(row);
- card.append(el('span', 'spot-title', label));
- }
- const foot = el('span', 'spot-row spot-foot mono');
- foot.append(el('span', null, b.failed ? 'Falhou' : b.ready ? '' : 'Baixando'));
- card.append(foot);
- if (pending || b.failed) {
- const veil = el('span', 'spot-veil', b.failed
- ? 'Indisponível no momento — falha no download'
- : 'Indisponível no momento — arquivo baixando');
- card.append(veil);
+ art.style.backgroundImage = 'url("' + imageUrl.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '")';
}
- marks(card);
+ art.append(el('span', 'glyph', glyphFor(label || b.id)));
+ const fab = el('span', 'play-fab');
+ fab.innerHTML = ICON.play;
+ art.append(fab);
+ card.append(art);
+ const meta = el('span');
+ meta.append(el('span', 'spot-title', label || b.id));
+ meta.append(el('span', 'kicker', b.group || 'Spot'));
+ if (pending) meta.append(el('span', 'state dl', 'Baixando…'));
+ if (b.failed) meta.append(el('span', 'state err', 'Falha no download'));
+ card.append(meta);
card.addEventListener('click', () => {
if (!b.ready || fireLocked()) return;
withFire(() => PlayDeskApp(b.id));
@@ -185,6 +176,13 @@ function paintSpots() {
}
}
+function glyphFor(label) {
+ const words = String(label || '').trim().split(/\s+/).filter(Boolean);
+ if (!words.length) return '';
+ if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
+ return (words[0][0] + words[1][0]).toUpperCase();
+}
+
function paintUnregisteredSupport() {
const main = $('main');
main.className = 'support-main';
@@ -219,7 +217,7 @@ async function paintHistory() {
main.className = '';
main.replaceChildren();
let rows = [];
- try { rows = await Timeline() || []; } catch { rows = []; }
+ try { rows = (await Timeline() || []).slice(0, HISTORY_LIMIT); } catch { rows = []; }
$('gridCount').textContent = rows.length ? rows.length + ' ITENS' : '';
if (!rows.length) {
main.append(el('p', 'empty', ''));
@@ -229,16 +227,23 @@ async function paintHistory() {
const line = el('div', 'hist-line');
line.append(el('span', 'mono hist-time', row.time || ''));
const kind = row.kind || '';
- const badge = el('span', 'badge' + (kind === 'spot' ? ' is-spot' : ''), kind);
+ const badge = el('span', 'badge' + (kind === 'spot' ? ' is-spot' : kind === 'chamada' || kind === 'call' ? ' is-live' : ''), kind);
line.append(badge);
const title = el('button', 'hist-title', row.title || '');
title.type = 'button';
+ title.title = 'Copiar nome';
+ title.addEventListener('click', () => copyText(row.title || '', 'Nome'));
+ line.append(title);
if (row.replayId) {
- title.addEventListener('click', () => {
- withFire(() => PlayDeskApp(row.replayId));
- });
+ const replay = el('button', 'hist-replay');
+ replay.type = 'button';
+ replay.title = 'Tocar de novo';
+ replay.innerHTML = ICON.play;
+ replay.addEventListener('click', () => withFire(() => PlayDeskApp(row.replayId)));
+ line.append(replay);
+ } else {
+ line.append(el('span'));
}
- line.append(title);
main.append(line);
}
}
@@ -274,29 +279,55 @@ function paintCalls() {
tabs.append(b);
}
wrap.append(tabs);
+ const form = el('div', 'call-form');
+ wrap.append(form);
const vals = state.values[state.call] || {};
const setVal = (id, value) => {
state.values[state.call] = {...(state.values[state.call] || {}), [id]: value};
};
const addCombo = (id, label, list) => {
- wrap.append(el('label', null, label));
+ const lab = el('label', null, label);
+ form.append(lab);
+ const box = el('div', 'combo');
const i = el('input');
i.type = 'text';
- i.setAttribute('list', 'dl-' + list + '-' + id);
i.value = vals[id] || '';
i.autocomplete = 'off';
- const dl = el('datalist');
- dl.id = 'dl-' + list + '-' + id;
- for (const opt of listFor(list)) {
- const o = document.createElement('option');
- o.value = opt;
- dl.append(o);
- }
- i.addEventListener('input', () => setVal(id, i.value));
- wrap.append(i, dl);
+ const menu = el('div', 'combo-menu');
+ menu.hidden = true;
+ const options = listFor(list);
+ let active = -1;
+ const fold = v => String(v || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
+ const close = () => { menu.hidden = true; active = -1; };
+ const pick = value => { i.value = value; setVal(id, value); close(); };
+ const render = () => {
+ const q = fold(i.value.trim());
+ const hits = options.filter(o => !q || fold(o).includes(q)).slice(0, 8);
+ menu.replaceChildren();
+ hits.forEach((o, idx) => {
+ const item = el('button', 'combo-item' + (idx === active ? ' is-active' : ''), o);
+ item.type = 'button';
+ item.addEventListener('pointerdown', ev => { ev.preventDefault(); pick(o); });
+ menu.append(item);
+ });
+ menu.hidden = !hits.length;
+ };
+ i.addEventListener('input', () => { active = -1; setVal(id, i.value); render(); });
+ i.addEventListener('focus', render);
+ i.addEventListener('blur', () => setTimeout(close, 120));
+ i.addEventListener('keydown', ev => {
+ const items = menu.querySelectorAll('.combo-item');
+ if (ev.key === 'ArrowDown' && items.length) { ev.preventDefault(); active = (active + 1) % items.length; render(); }
+ else if (ev.key === 'ArrowUp' && items.length) { ev.preventDefault(); active = (active - 1 + items.length) % items.length; render(); }
+ else if (ev.key === 'Enter' && active >= 0 && items[active]) { ev.preventDefault(); pick(items[active].textContent); }
+ else if (ev.key === 'Escape') close();
+ });
+ box.append(i, menu);
+ lab.append(box);
};
const addGender = () => {
- wrap.append(el('label', null, 'Gênero'));
+ const glab = el('label', null, 'Gênero');
+ form.append(glab);
const row = el('div', 'gender-pick');
[['m', 'Masculino'], ['f', 'Feminino']].forEach(([id, name]) => {
const lab = document.createElement('label');
@@ -307,13 +338,13 @@ function paintCalls() {
i.checked = vals.genero === id;
i.addEventListener('change', () => {
setVal('genero', id);
- const btn = wrap.querySelector('.btn-primary');
+ const btn = wrap.querySelector('.cta');
if (btn) btn.disabled = false;
});
lab.append(i, document.createTextNode(' ' + name));
row.append(lab);
});
- wrap.append(row);
+ glab.append(row);
};
const addField = (id, label, type, list) => {
if (id === 'genero' || type === 'gender') {
@@ -323,24 +354,25 @@ function paintCalls() {
addCombo(id, label, list || id);
return;
}
- wrap.append(el('label', null, label));
+ const lab = el('label', type === 'area' ? 'wide' : null, label);
+ form.append(lab);
if (type === 'area') {
const t = el('textarea');
t.value = vals[id] || '';
t.addEventListener('input', () => setVal(id, t.value));
- wrap.append(t);
+ lab.append(t);
} else {
const i = el('input');
i.type = 'text';
i.value = vals[id] || '';
if (id === 'placa' || type === 'plate') i.maxLength = 7;
i.addEventListener('input', () => setVal(id, i.value));
- wrap.append(i);
+ lab.append(i);
}
};
- const form = (state.deskapp.callForms || {})[state.call];
- if (form && form.fields && form.fields.length) {
- const fields = form.fields.slice();
+ const serverForm = (state.deskapp.callForms || {})[state.call];
+ if (serverForm && serverForm.fields && serverForm.fields.length) {
+ const fields = serverForm.fields.slice();
// gender radios hidden for now — DeepSeek infers funcionário/funcionária from the name
for (const f of fields) {
addField(f.id, f.label || f.id, f.type || 'text', f.list);
@@ -356,7 +388,7 @@ function paintCalls() {
addField('text', 'Texto', 'area');
}
const actions = el('div', 'call-actions');
- const go = el('button', 'btn btn-primary', 'Anunciar');
+ const go = el('button', 'cta', 'Anunciar');
go.type = 'button';
go.disabled = fireLocked();
go.addEventListener('click', () => {
@@ -364,7 +396,7 @@ function paintCalls() {
withFire(() => AnnounceCall(state.call, state.values[state.call] || {}));
});
actions.append(go);
- wrap.append(actions);
+ form.append(actions);
main.append(wrap);
}
@@ -400,6 +432,8 @@ function paintTransport(snap) {
$('playIcon').innerHTML = snap.wanted ? ICON.pause : ICON.play;
$('playLabel').textContent = snap.wanted ? 'Pausar' : 'Tocar';
$('play').setAttribute('aria-label', snap.wanted ? 'Pausar' : 'Tocar');
+ $('play').title = snap.wanted ? 'Pausar' : 'Tocar';
+ $('npKind').className = 'badge' + (snap.nowKind === 'spot' ? ' is-spot' : snap.busy ? ' is-live' : '');
$('eq').hidden = !live;
$('elapsed').textContent = snap.elapsedLabel || '0:00';
$('npKind').textContent = snap.nowKind || '—';
@@ -429,13 +463,12 @@ function paintTransport(snap) {
}
$('muteBanner').hidden = !snap.muted || !!snap.unregistered;
if (!state.volDrag && snap.volume != null) {
- $('vol').value = String(snap.volume);
- $('volRead').textContent = String(snap.volume);
+ setVolumeUI(snap.volume);
}
const conn = $('conn');
const offline = snap.status === 'stopped' || snap.ws === 'down';
- conn.className = 'conn-pill' + (snap.ws === 'up' || snap.status === 'live' ? ' ok' : '');
+ conn.className = 'conn' + (snap.ws === 'up' || snap.status === 'live' ? ' ok' : '');
$('connText').textContent = offline && !snap.wanted ? 'Offline' : snap.ws === 'up' || snap.status === 'live' ? 'Conectado' : 'Conectando';
const sync = $('syncMini');
@@ -467,7 +500,7 @@ function paintTransport(snap) {
$('onair').hidden = false;
$('stage').textContent = snap.stage || '';
$('airText').textContent = snap.airText || '';
- $('airTime').textContent = (snap.airTime || 0).toFixed(1) + 's';
+ $('airTime').textContent = Math.round(snap.airTime || 0) + 's';
} else {
$('onair').hidden = true;
}
@@ -493,15 +526,21 @@ function paintTransport(snap) {
function applyDesk(ui) {
state.deskapp = ui || {buttons: [], calls: ['interno', 'estacionamento', 'livre'], lists: {}};
- if (state.deskapp.accent) document.documentElement.style.setProperty('--color-accent', state.deskapp.accent);
paint();
}
+function setVolumeUI(v) {
+ $('vol').value = String(v);
+ $('vol').style.setProperty('--vol-pct', v + '%');
+ $('volRead').textContent = String(v);
+}
+
async function boot() {
const data = await Bootstrap();
state.templates = data.templates;
state.chimeEnabled = data.chimeEnabled !== false;
- if (data.version) document.title = 'Rádio Indoor ' + data.version;
+ if (data.version) document.title = 'MegaPlay Radio ' + data.version;
+ $('appVersion').textContent = data.version || '';
state.unregistered = !!data.unregistered;
$('storeName').textContent = data.networkName || 'Player não encontrado';
setUnit(data.deviceUid);
@@ -520,18 +559,16 @@ async function boot() {
setInterval(() => { if (state.view === 'history') paintHistory(); }, 2000);
}
-applyTheme(localStorage.getItem('theme') || 'dark');
-$('theme').addEventListener('click', () => {
- applyTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
-});
$('play').addEventListener('click', () => TogglePlay());
+$('playIcon').innerHTML = ICON.play;
+$('cover').innerHTML = ICON.note;
$('muteIcon').innerHTML = ICON.vol;
$('mute').addEventListener('click', () => ToggleMute());
$('vol').addEventListener('pointerdown', () => { state.volDrag = true; });
$('vol').addEventListener('pointerup', () => { state.volDrag = false; });
$('vol').addEventListener('pointercancel', () => { state.volDrag = false; });
$('vol').addEventListener('input', ev => {
- $('volRead').textContent = ev.target.value;
+ setVolumeUI(ev.target.value);
SetVolume(Number(ev.target.value));
});
$('cancel').addEventListener('click', () => CancelAnnounce());
@@ -545,13 +582,15 @@ function setUnit(uid) {
$('copyUid').hidden = !state.deviceUid;
}
-$('copyUid').innerHTML = ICON.copy;
-$('copyUid').addEventListener('click', () => {
- if (!state.deviceUid) return;
- ClipboardSetText(state.deviceUid).then(ok => {
- toast(ok === false ? 'Falha' : 'UID', ok === false ? 'não foi possível copiar' : state.deviceUid + ' copiado', ok === false);
+function copyText(text, label) {
+ if (!text) return;
+ ClipboardSetText(text).then(ok => {
+ toast(ok === false ? 'Falha' : label, ok === false ? 'não foi possível copiar' : text + ' copiado', ok === false);
}).catch(err => toast('Falha', String(err), true));
-});
+}
+
+$('copyUid').innerHTML = ICON.copy;
+$('copyUid').addEventListener('click', () => copyText(state.deviceUid, 'UID'));
EventsOn('player', paintTransport);
EventsOn('deskapp', applyDesk);
@@ -658,6 +697,9 @@ function closeDebug() {
$('debugOverlay').hidden = true;
}
+$('winMin').addEventListener('click', () => WindowMinimise());
+$('winMax').addEventListener('click', () => WindowToggleMaximise());
+$('winClose').addEventListener('click', () => Quit());
$('debugBtn').addEventListener('click', openDebug);
$('debugClose').addEventListener('click', closeDebug);
$('debugOverlay').addEventListener('click', ev => {
diff --git a/main.go b/main.go
index 1f1a00e..286518b 100644
--- a/main.go
+++ b/main.go
@@ -49,6 +49,7 @@ func main() {
Height: 800,
MinWidth: 980,
MinHeight: 640,
+ Frameless: true,
AssetServer: &assetserver.Options{
Assets: assets,
},