From f72875bef17d57cac70acea8b967a86ec6bbb0a6 Mon Sep 17 00:00:00 2001 From: Daniel Legt Date: Fri, 6 Mar 2026 10:57:57 +0200 Subject: [PATCH] Theme --- src/templates/index.html | 1 - static/css/layout.css | 1 + static/css/main.css | 42 ++++++- static/css/themes/modern.css | 233 ++++++++++++++++++++++++++++------- static/js/config.js | 2 +- static/js/room.js | 59 ++++++++- 6 files changed, 288 insertions(+), 50 deletions(-) diff --git a/src/templates/index.html b/src/templates/index.html index c3bf564..162d0d3 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -86,7 +86,6 @@
- sec
diff --git a/static/css/layout.css b/static/css/layout.css index 2cc85d8..5d09b60 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -287,6 +287,7 @@ height: 6rem; border-radius: 0.4rem; cursor: pointer; + overflow: visible; transition: transform 120ms ease; } diff --git a/static/css/main.css b/static/css/main.css index 6016edc..19fdab3 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -145,6 +145,12 @@ input[type="number"]::-webkit-inner-spin-button { border-bottom: none; } +.participant-name.is-admin { + color: #2f58ff; + text-shadow: 0 0 0.2rem rgba(47, 88, 255, 0.32); + font-weight: 700; +} + .summary-table { width: 100%; border-collapse: collapse; @@ -164,13 +170,41 @@ input[type="number"]::-webkit-inner-spin-button { } .vote-card.impact { - animation: vote-impact 170ms ease; + animation: vote-impact 320ms cubic-bezier(0.2, 0.85, 0.2, 1); } @keyframes vote-impact { - 0% { transform: scale(1); } - 40% { transform: scale(0.91); } - 100% { transform: scale(1); } + 0% { transform: translateX(0) scale(1) rotate(0deg); } + 15% { transform: translateX(-0.18rem) scale(0.9) rotate(-2.2deg); } + 30% { transform: translateX(0.14rem) scale(1.06) rotate(1.7deg); } + 45% { transform: translateX(-0.1rem) scale(0.98) rotate(-1.4deg); } + 60% { transform: translateX(0.08rem) scale(1.04) rotate(1deg); } + 100% { transform: translateX(0) scale(1) rotate(0deg); } +} + +.vote-particle { + position: absolute; + left: 50%; + top: 50%; + width: 0.32rem; + height: 0.32rem; + border-radius: 999px; + pointer-events: none; + background: hsl(var(--hue, 120) 95% 66%); + box-shadow: 0 0 0.35rem hsl(var(--hue, 120) 95% 66%); + transform: translate(-50%, -50%) scale(1); + animation: vote-particle-burst 420ms ease-out forwards; +} + +@keyframes vote-particle-burst { + 0% { + opacity: 0.95; + transform: translate(-50%, -50%) scale(1); + } + 100% { + opacity: 0; + transform: translate(calc(-50% + var(--tx, 0px)), calc(-50% + var(--ty, 0px))) scale(0.2); + } } .terminal-window .title-bar { diff --git a/static/css/themes/modern.css b/static/css/themes/modern.css index bd36fc1..6ae3f2d 100644 --- a/static/css/themes/modern.css +++ b/static/css/themes/modern.css @@ -1,56 +1,64 @@ :root[data-ui-theme="modern"] { - --font-main: 'Segoe UI', 'Inter', Arial, sans-serif; - --desktop-bg: #e8edf4; - --desktop-pattern: linear-gradient(135deg, #eef3f8 0%, #dde7f2 100%); - --surface-window: #ffffff; - --surface-control: #f1f4f8; - --surface-input: #ffffff; - --surface-status: #f8fafc; - --text-primary: #101828; - --title-bg: #175cd3; - --title-text: #ffffff; - --border-outer: #c4ced9; - --border-input: #b8c3d2; - --border-muted: #d5dde8; + --font-main: "Segoe UI Variable Text", "Segoe UI", "Inter", "SF Pro Text", "Helvetica Neue", Arial, sans-serif; + --desktop-bg: #282c34; + --desktop-pattern: + radial-gradient(circle at 14% 18%, rgba(97, 175, 239, 0.18) 0, rgba(97, 175, 239, 0) 46%), + radial-gradient(circle at 88% 10%, rgba(198, 120, 221, 0.18) 0, rgba(198, 120, 221, 0) 40%), + radial-gradient(circle at 54% 100%, rgba(86, 182, 194, 0.13) 0, rgba(86, 182, 194, 0) 52%), + linear-gradient(165deg, #2c313c 0%, #232731 100%); + --surface-window: rgba(40, 44, 52, 0.92); + --surface-control: #3a404b; + --surface-input: #21252b; + --surface-status: #2f343f; + --text-primary: #abb2bf; + --title-bg: linear-gradient(90deg, #353b46 0%, #2c313c 100%); + --title-text: #e6edf7; + --border-outer: #4b5263; + --border-input: #5c6370; + --border-muted: #3a404b; --window-border-width: 1px; --control-border-width: 1px; --input-border-width: 1px; - --window-shadow: 0 12px 30px rgba(16, 24, 40, 0.14); - --button-shadow: none; - --button-shadow-active: none; - --focus-ring: 0 0 0 2px rgba(23, 92, 211, 0.22); - --card-bg: #ffffff; - --card-text: #101828; - --card-border: #b8c3d2; + --window-shadow: 0 18px 45px rgba(14, 16, 20, 0.42); + --button-shadow: 0 4px 16px rgba(0, 0, 0, 0.22); + --button-shadow-active: inset 0 1px 0 rgba(0, 0, 0, 0.25); + --focus-ring: 0 0 0 2px rgba(97, 175, 239, 0.35); + --card-bg: #21252b; + --card-text: #e6edf7; + --card-border: #5c6370; --card-border-width: 1px; - --selected-outline: 2px solid #175cd3; - --modal-overlay: rgba(15, 23, 42, 0.35); + --selected-outline: 2px solid #61afef; + --modal-overlay: rgba(10, 12, 16, 0.62); } :root[data-ui-theme="modern"][data-theme="dark"] { - --desktop-bg: #0b1220; - --desktop-pattern: linear-gradient(140deg, #111b2c 0%, #09101d 100%); - --surface-window: #111827; - --surface-control: #1f2937; - --surface-input: #0f172a; - --surface-status: #172033; - --text-primary: #e5edf7; - --title-bg: #1d4ed8; - --title-text: #ffffff; - --border-outer: #334155; - --border-input: #3f5169; - --border-muted: #334155; - --window-shadow: 0 14px 34px rgba(0, 0, 0, 0.45); - --focus-ring: 0 0 0 2px rgba(96, 165, 250, 0.34); - --card-bg: #1e293b; - --card-text: #e5edf7; - --card-border: #334155; + --desktop-bg: #21252b; + --desktop-pattern: + radial-gradient(circle at 12% 16%, rgba(97, 175, 239, 0.16) 0, rgba(97, 175, 239, 0) 46%), + radial-gradient(circle at 86% 8%, rgba(198, 120, 221, 0.16) 0, rgba(198, 120, 221, 0) 42%), + linear-gradient(165deg, #252932 0%, #1d2027 100%); + --surface-window: rgba(33, 37, 43, 0.95); + --surface-control: #343a45; + --surface-input: #1d2127; + --surface-status: #2a2f38; + --text-primary: #b9c2d0; + --title-bg: linear-gradient(90deg, #2f343f 0%, #272c35 100%); + --title-text: #eef3fb; + --border-outer: #5c6370; + --border-input: #6c7484; + --border-muted: #404652; + --window-shadow: 0 20px 52px rgba(8, 10, 13, 0.58); + --focus-ring: 0 0 0 2px rgba(97, 175, 239, 0.42); + --card-bg: #1d2127; + --card-text: #eef3fb; + --card-border: #6c7484; } :root[data-ui-theme="modern"] body { background-color: var(--desktop-bg); background-image: var(--desktop-pattern); background-size: cover; + background-attachment: fixed; } :root[data-ui-theme="modern"] .window, @@ -60,20 +68,161 @@ :root[data-ui-theme="modern"] input, :root[data-ui-theme="modern"] select, :root[data-ui-theme="modern"] textarea { + border-radius: 0.62rem; +} + +:root[data-ui-theme="modern"] .window { + backdrop-filter: blur(5px); +} + +:root[data-ui-theme="modern"] .mobile-control-strip, +:root[data-ui-theme="modern"] .taskbar { + background: rgba(40, 44, 52, 0.82); + border-top-color: var(--border-input); + backdrop-filter: blur(8px); +} + +:root[data-ui-theme="modern"] .title-bar { + font-size: 0.96rem; + letter-spacing: 0.01em; + border-bottom: 1px solid rgba(97, 175, 239, 0.3); +} + +:root[data-ui-theme="modern"] .title-bar-controls button { border-radius: 0.45rem; + border-color: var(--border-input); + background: #323843; +} + +:root[data-ui-theme="modern"] .title-bar-controls button:hover, +:root[data-ui-theme="modern"] .btn:hover { + background: #434a57; + border-color: #7d8699; + color: #e6edf7; +} + +:root[data-ui-theme="modern"] .btn { + transition: background 120ms ease, border-color 120ms ease, transform 120ms ease; +} + +:root[data-ui-theme="modern"] .btn:active { + transform: translateY(1px); +} + +:root[data-ui-theme="modern"] .btn-primary { + background: #61afef; + border-color: #61afef; + color: #10131a; + font-weight: 700; +} + +:root[data-ui-theme="modern"] .btn-primary:hover { + background: #79bcf3; + border-color: #79bcf3; + color: #0d1117; +} + +:root[data-ui-theme="modern"] input::placeholder, +:root[data-ui-theme="modern"] textarea::placeholder { + color: #7d8699; +} + +:root[data-ui-theme="modern"] .status-line, +:root[data-ui-theme="modern"] .summary-table, +:root[data-ui-theme="modern"] .participant-list { + border-radius: 0.5rem; +} + +:root[data-ui-theme="modern"] .summary-table th { + color: #61afef; + background: #272c35; +} + +:root[data-ui-theme="modern"] .participant-item:hover { + background: rgba(97, 175, 239, 0.08); +} + +:root[data-ui-theme="modern"] .participant-name.is-admin { + color: #d19a66; + text-shadow: 0 0 0.25rem rgba(209, 154, 102, 0.28); } :root[data-ui-theme="modern"] .preview-board, :root[data-ui-theme="modern"] .voting-board { - background: linear-gradient(180deg, #0f6f54 0%, #0a5c45 100%); - border: 1px solid var(--border-input); + background: + linear-gradient(145deg, rgba(97, 175, 239, 0.07) 0%, rgba(33, 37, 43, 0) 34%), + linear-gradient(0deg, rgba(92, 99, 112, 0.08), rgba(92, 99, 112, 0.08)), + #20242b; + border: 1px solid #5c6370; + box-shadow: inset 0 0 0 1px rgba(97, 175, 239, 0.15), inset 0 -20px 40px rgba(0, 0, 0, 0.2); } :root[data-ui-theme="modern"][data-theme="dark"] .preview-board, :root[data-ui-theme="modern"][data-theme="dark"] .voting-board { - background: linear-gradient(180deg, #0b3e33 0%, #082d25 100%); + background: + linear-gradient(145deg, rgba(97, 175, 239, 0.07) 0%, rgba(33, 37, 43, 0) 35%), + linear-gradient(0deg, rgba(108, 116, 132, 0.08), rgba(108, 116, 132, 0.08)), + #1b1f25; + border-color: #6c7484; +} + +:root[data-ui-theme="modern"] .vote-card, +:root[data-ui-theme="modern"] .preview-card { + box-shadow: 0 5px 16px rgba(0, 0, 0, 0.28); +} + +:root[data-ui-theme="modern"] .vote-card:hover { + border-color: #61afef; +} + +:root[data-ui-theme="modern"] .vote-card.is-selected { + box-shadow: 0 0 0 1px #61afef, 0 10px 22px rgba(97, 175, 239, 0.22); +} + +:root[data-ui-theme="modern"] .hint-text { + color: #7d8699; +} + +:root[data-ui-theme="modern"] .icon-btn img { + image-rendering: auto; + filter: saturate(0.9) brightness(0.9); } :root[data-ui-theme="modern"] .preset-modal-overlay { background: var(--modal-overlay); } + +:root[data-ui-theme="modern"] .skeleton-line, +:root[data-ui-theme="modern"] .skeleton-board, +:root[data-ui-theme="modern"] .skeleton-table, +:root[data-ui-theme="modern"] .skeleton-list, +:root[data-ui-theme="modern"] .skeleton-controls { + background: linear-gradient(90deg, rgba(92, 99, 112, 0.22), rgba(122, 132, 148, 0.32), rgba(92, 99, 112, 0.22)); + background-size: 220% 100%; + border: 1px solid #4b5263; + border-radius: 0.4rem; + animation: modern-skeleton-shimmer 1.2s ease infinite; +} + +@keyframes modern-skeleton-shimmer { + from { background-position: 0 0; } + to { background-position: -200% 0; } +} + +:root[data-ui-theme="modern"] .terminal-window .title-bar { + background: #21252b; + color: #98c379; +} + +:root[data-ui-theme="modern"] .terminal-window .title-bar-controls button { + background: #2f343f; + color: #98c379; + border-color: #5c6370; +} + +:root[data-ui-theme="modern"] .terminal-log-output { + background: #1b1f25; + color: #abb2bf; + border: 1px solid #4b5263; + font-family: "JetBrains Mono", "Cascadia Code", "SFMono-Regular", Consolas, "Liberation Mono", monospace; +} diff --git a/static/js/config.js b/static/js/config.js index 8c3e712..e3ee38d 100644 --- a/static/js/config.js +++ b/static/js/config.js @@ -522,7 +522,7 @@ roomConfigForm.addEventListener('submit', async (event) => { return; } - const target = `/room/${encodeURIComponent(data.roomId)}?participantId=${encodeURIComponent(data.creatorParticipantId)}&adminToken=${encodeURIComponent(data.adminToken)}`; + const target = `/room/${encodeURIComponent(data.roomId)}?participantId=${encodeURIComponent(data.creatorParticipantId)}&adminToken=${encodeURIComponent(data.adminToken)}&username=${encodeURIComponent(payload.creatorUsername)}`; window.location.assign(target); } catch (_err) { statusLine.textContent = 'Network error while creating room.'; diff --git a/static/js/room.js b/static/js/room.js index 7cf7189..d8fdbda 100644 --- a/static/js/room.js +++ b/static/js/room.js @@ -33,12 +33,13 @@ const joinAdminTokenInput = document.getElementById('join-admin-token'); const joinError = document.getElementById('join-error'); let participantID = params.get('participantId') || ''; let adminToken = params.get('adminToken') || ''; +const prefillUsername = params.get('username') || ''; let eventSource = null; let latestLinks = { participantLink: '', adminLink: '' }; let latestAdminLogs = []; const savedUsername = localStorage.getItem(USERNAME_KEY) || ''; -joinUsernameInput.value = savedUsername; +joinUsernameInput.value = prefillUsername || savedUsername; joinAdminTokenInput.value = adminToken; if (!window.CardUI || typeof window.CardUI.appendFace !== 'function') { @@ -59,6 +60,8 @@ function setJoinError(message) { function updateURL() { const next = new URL(window.location.href); + next.searchParams.delete('username'); + if (participantID) { next.searchParams.set('participantId', participantID); } else { @@ -103,6 +106,7 @@ async function joinRoom({ username, role, password, participantIdOverride }) { localStorage.setItem(USERNAME_KEY, data.username); updateURL(); setJoinError(''); + return data; } function renderParticipants(participants, isRevealed) { @@ -114,12 +118,14 @@ function renderParticipants(participants, isRevealed) { item.className = 'participant-item'; const name = document.createElement('span'); + name.className = 'participant-name'; let label = participant.username; if (participant.id === participantID) { label += ' (You)'; } if (participant.isAdmin) { label += ' [Admin]'; + name.classList.add('is-admin'); } name.textContent = label; @@ -247,6 +253,7 @@ function renderCards(cards, participants, isRevealed) { card.classList.remove('impact'); void card.offsetWidth; card.classList.add('impact'); + spawnVoteParticles(card); castVote(value); }); @@ -254,6 +261,26 @@ function renderCards(cards, participants, isRevealed) { }); } +function spawnVoteParticles(card) { + const particleCount = 12; + for (let i = 0; i < particleCount; i += 1) { + const particle = document.createElement('span'); + particle.className = 'vote-particle'; + + const angle = (Math.PI * 2 * i) / particleCount + (Math.random() * 0.4 - 0.2); + const distance = 18 + Math.random() * 26; + const dx = Math.cos(angle) * distance; + const dy = Math.sin(angle) * distance; + + particle.style.setProperty('--tx', `${dx.toFixed(2)}px`); + particle.style.setProperty('--ty', `${dy.toFixed(2)}px`); + particle.style.setProperty('--hue', `${Math.floor(Math.random() * 80) + 90}`); + + particle.addEventListener('animationend', () => particle.remove(), { once: true }); + card.appendChild(particle); + } +} + function formatLogTime(raw) { const parsed = new Date(raw); if (Number.isNaN(parsed.getTime())) { @@ -417,12 +444,17 @@ joinForm.addEventListener('submit', async (event) => { adminToken = joinAdminTokenInput.value.trim(); try { - await joinRoom({ + const result = await joinRoom({ username, role: joinRoleInput.value, password: joinPasswordInput.value, participantIdOverride: participantID, }); + if (result.isAdmin) { + const adminRoomURL = `/room/${encodeURIComponent(roomID)}?participantId=${encodeURIComponent(participantID)}&adminToken=${encodeURIComponent(adminToken)}`; + window.location.assign(adminRoomURL); + return; + } connectSSE(); } catch (err) { if (participantID) { @@ -433,6 +465,27 @@ joinForm.addEventListener('submit', async (event) => { } }); +async function tryAutoJoinExistingParticipant() { + if (!participantID) { + return; + } + + const username = joinUsernameInput.value.trim() || prefillUsername || 'host'; + + try { + await joinRoom({ + username, + role: 'participant', + password: joinPasswordInput.value, + participantIdOverride: participantID, + }); + connectSSE(); + } catch (_err) { + participantID = ''; + updateURL(); + } +} + window.addEventListener('pagehide', () => { if (!participantID) { return; @@ -441,3 +494,5 @@ window.addEventListener('pagehide', () => { const payload = JSON.stringify({ participantId: participantID }); navigator.sendBeacon(`/api/rooms/${encodeURIComponent(roomID)}/leave`, new Blob([payload], { type: 'application/json' })); }); + +void tryAutoJoinExistingParticipant();