From 637b5f01670bddc4c666b0fbfc1e52a9968ea1b9 Mon Sep 17 00:00:00 2001 From: Daniel Legt Date: Thu, 5 Mar 2026 22:30:37 +0200 Subject: [PATCH] Update --- README.md | 13 +- src/templates/index.html | 73 +++- src/templates/room.html | 29 +- static/css/layout.css | 432 ++++++++++++++++++++ static/css/main.css | 181 +++++++++ static/css/styles.css | 721 --------------------------------- static/css/themes/modern.css | 79 ++++ static/css/themes/no-theme.css | 66 +++ static/css/themes/win98.css | 88 ++++ static/js/config.js | 41 +- static/js/room.js | 14 - static/js/ui-controls.js | 75 ++++ 12 files changed, 1022 insertions(+), 790 deletions(-) create mode 100644 static/css/layout.css create mode 100644 static/css/main.css delete mode 100644 static/css/styles.css create mode 100644 static/css/themes/modern.css create mode 100644 static/css/themes/no-theme.css create mode 100644 static/css/themes/win98.css create mode 100644 static/js/ui-controls.js diff --git a/README.md b/README.md index b560c77..38cfbe3 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,11 @@ Enterprise-style Scrum Poker application using Go, Gin, and SSE for real-time ro - Real-time updates via Server-Sent Events (SSE) - Strict state sanitization: unrevealed votes from other users are never broadcast - Backend authorization for admin-only actions (`reveal`, `reset`) -- Win98-themed frontend with: +- Themeable frontend with: + - CSS architecture split into `main.css`, `layout.css`, and `/themes/*` + - Theme options: `win98` (default), `modern`, `No Theme` + - Light/Dark mode independent from active theme + - Desktop taskbar controls + mobile top controls for theme/mode switching - Config page and card deck preview - Drag-and-drop card ordering - Card add/remove with animation completion handling @@ -28,8 +32,11 @@ Enterprise-style Scrum Poker application using Go, Gin, and SSE for real-time ro - `src/middleware/`: static cache headers middleware - `src/models/`: template page data models - `src/templates/`: HTML templates (`index.html`, `room.html`) -- `static/css/`: styles -- `static/js/`: frontend logic (`config.js`, `room.js`) +- `static/css/main.css`: shared component primitives +- `static/css/layout.css`: grids/flex/positioning/responsive layout +- `static/css/themes/`: drop-in theme files +- `static/js/ui-controls.js`: global theme + mode engine +- `static/js/`: page logic (`config.js`, `room.js`) ## Environment Variables diff --git a/src/templates/index.html b/src/templates/index.html index 4850b35..7be694d 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -7,11 +7,22 @@ - + + + + + -
- +
+
+ + +
@@ -128,25 +139,11 @@
- +
- -
@@ -160,8 +157,44 @@ + + + + + diff --git a/src/templates/room.html b/src/templates/room.html index e7ec007..377a863 100644 --- a/src/templates/room.html +++ b/src/templates/room.html @@ -7,11 +7,22 @@ - + + + + + -
- +
+
+ + +
@@ -134,6 +145,18 @@
+
+
+ + +
+
+ + diff --git a/static/css/layout.css b/static/css/layout.css new file mode 100644 index 0000000..228e374 --- /dev/null +++ b/static/css/layout.css @@ -0,0 +1,432 @@ +:root { + --ui-scale: 1.06; + --base-font-size: clamp(16px, 0.35vw + 0.9rem, 20px); +} + +#desktop { + flex: 1; + width: 100%; + padding: 4.3rem 1rem 1rem; + display: flex; + align-items: center; + justify-content: center; +} + +.mobile-control-strip { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 50; + padding: 0.45rem 0.55rem; +} + +.ui-controls { + display: flex; + align-items: center; + gap: 0.4rem; +} + +.theme-picker { + min-width: 9rem; +} + +.desktop-taskbar { + display: none; +} + +.config-window { + width: min(78rem, 100%); +} + +.intro-copy { + margin-bottom: 0.7rem; +} + +.room-form { + display: flex; + flex-direction: column; + gap: 0.65rem; +} + +.config-layout { + display: grid; + gap: 0.75rem; + grid-template-columns: minmax(0, 1fr) 24rem; +} + +.config-panel { + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.field-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.6rem; +} + +.field-group { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.number-input-wrap { + display: flex; + align-items: center; + gap: 0.35rem; +} + +.number-with-unit .input-unit { + min-width: 2rem; + text-align: right; +} + +.preview-content { + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.preview-meta { + display: flex; + justify-content: space-between; +} + +.preview-board { + min-height: 13rem; + padding: 0.6rem; +} + +.preview-cards { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.preview-card { + width: 3.15rem; + height: 4.45rem; + border-radius: 0.32rem; + display: inline-flex; + align-items: center; + justify-content: center; + position: relative; + user-select: none; + cursor: grab; + transition: transform 170ms ease; +} + +.preview-card.dragging { + opacity: 0.65; +} + +.preview-card.wiggle { + animation: wiggle 250ms linear infinite; +} + +@keyframes wiggle { + 0% { transform: rotate(-2deg); } + 50% { transform: rotate(2deg); } + 100% { transform: rotate(-2deg); } +} + +.preview-card-remove { + position: absolute; + top: -0.35rem; + right: -0.35rem; + width: 1rem; + height: 1rem; + opacity: 0; + transition: opacity 120ms ease, transform 120ms ease; + transform: scale(0.86); + pointer-events: none; +} + +.preview-card:hover .preview-card-remove, +.preview-card-remove:focus { + opacity: 1; + transform: scale(1); + pointer-events: auto; +} + +.preview-card.is-removing { + animation: card-pop-out 190ms ease forwards; +} + +@keyframes card-pop-out { + from { opacity: 1; transform: scale(1); } + to { opacity: 0; transform: scale(0.58) rotate(-10deg); } +} + +.card-editor { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.card-editor-row { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 0.35rem; +} + +.deck-tools-row { + display: flex; + justify-content: flex-end; + flex-wrap: wrap; + gap: 0.35rem; +} + +.icon-btn { + width: 2.45rem; + min-width: 2.45rem; + height: 2.15rem; + padding: 0.1rem; + display: inline-flex; + justify-content: center; + align-items: center; +} + +.icon-btn img { + width: 1.05rem; + height: 1.05rem; + image-rendering: pixelated; +} + +.preset-modal-overlay { + position: fixed; + inset: 0; + z-index: 70; + display: flex; + align-items: center; + justify-content: center; +} + +.preset-modal-window { + width: min(40rem, 92vw); +} + +.preset-list { + display: flex; + flex-direction: column; + gap: 0.35rem; + max-height: 11rem; + overflow-y: auto; + margin-bottom: 0.5rem; +} + +.preset-item { + display: flex; + justify-content: space-between; + gap: 0.5rem; + padding: 0.35rem; +} + +.preset-actions { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.preset-modal-actions { + display: flex; + justify-content: flex-end; + gap: 0.35rem; +} + +.import-pane { + margin-top: 0.5rem; +} + +.import-pane textarea { + resize: vertical; +} + +.actions-row { + display: flex; + justify-content: flex-end; + gap: 0.4rem; +} + +.room-desktop { + align-items: stretch; +} + +.room-grid { + width: min(78rem, 100%); + display: grid; + gap: 0.75rem; + grid-template-columns: 2fr 1fr; +} + +.room-main-window, +.side-panel-window { + min-height: 40rem; +} + +.room-meta { + display: flex; + justify-content: space-between; + margin-bottom: 0.55rem; +} + +.voting-board { + min-height: 14rem; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0.6rem; + align-content: flex-start; +} + +.vote-card { + width: 4.3rem; + height: 6rem; + border-radius: 0.4rem; + cursor: pointer; + transition: transform 120ms ease; +} + +.vote-card:hover { + transform: translateY(-0.2rem); +} + +.vote-summary-window { + margin-top: 0.7rem; +} + +.vote-summary-content { + display: flex; + flex-direction: column; + gap: 0.45rem; +} + +.summary-metrics { + display: flex; + justify-content: space-between; +} + +.side-panel-content { + display: flex; + flex-direction: column; + gap: 0.55rem; + height: 100%; +} + +.participants-scroll { + flex: 1; + min-height: 13rem; + overflow-y: auto; +} + +.participant-item { + display: flex; + justify-content: space-between; + padding: 0.35rem; +} + +.side-controls { + margin-top: auto; + display: flex; + flex-direction: column; + gap: 0.45rem; +} + +.links-block { + display: grid; + gap: 0.2rem; +} + +.admin-controls { + display: flex; + justify-content: flex-end; +} + +.join-window { + position: fixed; + z-index: 60; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: min(27rem, 92vw); +} + +.skeleton-line, +.skeleton-board, +.skeleton-table, +.skeleton-list, +.skeleton-controls { + height: 1.2rem; +} + +.skeleton-board { + height: 15rem; +} + +.skeleton-table { + height: 8rem; +} + +.skeleton-list { + height: 20rem; + margin-bottom: 0.45rem; +} + +.skeleton-controls { + height: 9rem; +} + +@media (min-width: 900px) { + .mobile-control-strip { + display: none; + } + + .desktop-taskbar { + display: flex; + align-items: center; + justify-content: flex-start; + min-height: 2.5rem; + padding: 0.3rem 0.55rem; + } + + #desktop { + padding: 1rem 1rem 3.4rem; + } +} + +@media (max-width: 960px) { + .config-layout, + .room-grid { + grid-template-columns: 1fr; + } + + .room-main-window, + .side-panel-window { + min-height: unset; + } +} + +@media (max-width: 720px) { + #desktop { + align-items: flex-start; + padding-top: 3.65rem; + } + + .field-row { + grid-template-columns: 1fr; + } + + .summary-metrics { + flex-direction: column; + gap: 0.25rem; + } +} + +@media (min-width: 2560px), (min-resolution: 2dppx) { + :root { + --ui-scale: 1.24; + } +} diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..35783e0 --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,181 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: calc(var(--base-font-size) * var(--ui-scale)); +} + +body { + min-height: 100vh; + display: flex; + flex-direction: column; + font-family: var(--font-main); + color: var(--text-primary); +} + +.mobile-control-strip, +.taskbar { + background: var(--surface-window); + border-top: var(--window-border-width) solid var(--border-outer); + box-shadow: var(--window-shadow); +} + +.taskbar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 45; +} + +input, +select, +textarea, +button { + font: inherit; +} + +.hidden { + display: none !important; +} + +.window { + background: var(--surface-window); + border: var(--window-border-width) solid var(--border-outer); + box-shadow: var(--window-shadow); +} + +.title-bar { + background: var(--title-bg); + color: var(--title-text); + padding: 0.25rem 0.45rem; + font-size: 1rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.title-bar-controls { + display: inline-flex; + gap: 0.12rem; +} + +.title-bar-controls button { + width: 1rem; + height: 1rem; + font-size: 0.72rem; + line-height: 0.6rem; + border: var(--control-border-width) solid var(--border-outer); + background: var(--surface-control); + color: var(--text-primary); +} + +.window-content { + padding: 0.75rem; +} + +.btn { + border: var(--control-border-width) solid var(--border-outer); + background: var(--surface-control); + color: var(--text-primary); + box-shadow: var(--button-shadow); + padding: 0.3rem 0.65rem; + cursor: pointer; +} + +.btn:active { + box-shadow: var(--button-shadow-active); +} + +.btn-primary { + font-weight: 700; +} + +input[type="text"], +input[type="number"], +input[type="password"], +select, +textarea { + background: var(--surface-input); + color: var(--text-primary); + border: var(--input-border-width) solid var(--border-input); + padding: 0.35rem 0.45rem; + width: 100%; +} + +input:focus, +select:focus, +textarea:focus { + outline: none; + box-shadow: var(--focus-ring); +} + +input[type="number"] { + appearance: textfield; + -moz-appearance: textfield; +} + +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.status-line { + background: var(--surface-status); + border: var(--input-border-width) solid var(--border-input); + padding: 0.32rem 0.5rem; + min-height: 1.8rem; +} + +.participant-list { + list-style: none; + background: var(--surface-input); + border: var(--input-border-width) solid var(--border-input); +} + +.participant-item { + border-bottom: 1px dashed var(--border-muted); +} + +.participant-item:last-child { + border-bottom: none; +} + +.summary-table { + width: 100%; + border-collapse: collapse; + background: var(--surface-input); +} + +.summary-table th, +.summary-table td { + border: 1px solid var(--border-muted); + padding: 0.35rem 0.45rem; + text-align: left; +} + +.vote-card, +.preview-card { + border: var(--card-border-width) solid var(--card-border); + background: var(--card-bg); + color: var(--card-text); +} + +.vote-card.is-selected { + outline: var(--selected-outline); + outline-offset: -0.35rem; +} + +.vote-card.impact { + animation: vote-impact 170ms ease; +} + +@keyframes vote-impact { + 0% { transform: scale(1); } + 40% { transform: scale(0.91); } + 100% { transform: scale(1); } +} diff --git a/static/css/styles.css b/static/css/styles.css deleted file mode 100644 index 201d571..0000000 --- a/static/css/styles.css +++ /dev/null @@ -1,721 +0,0 @@ -:root { - --desktop-bg: #008080; - --window-bg: #c0c0c0; - --window-text: #000000; - --border-light: #ffffff; - --border-dark: #000000; - --border-mid-light: #dfdfdf; - --border-mid-dark: #808080; - --title-bg: #000080; - --title-text: #ffffff; - --input-bg: #ffffff; - --status-bg: #b3b3b3; - --board-bg: #0f6d3d; - --card-bg: #ffffff; - --card-text: #000000; -} - -[data-theme="dark"] { - --desktop-bg: #0a0a0a; - --window-bg: #2b2b2b; - --window-text: #e0e0e0; - --border-light: #555555; - --border-dark: #000000; - --border-mid-light: #3a3a3a; - --border-mid-dark: #1a1a1a; - --title-bg: #000000; - --title-text: #00ff00; - --input-bg: #111111; - --status-bg: #1b1b1b; - --board-bg: #0b2f16; - --card-bg: #171717; - --card-text: #00ff66; -} - -* { - box-sizing: border-box; - margin: 0; - padding: 0; - font-family: 'VT323', monospace; -} - -body { - background-color: var(--desktop-bg); - color: var(--window-text); - min-height: 100vh; - display: flex; - flex-direction: column; - background-image: radial-gradient(circle, rgba(0, 0, 0, 0.12) 1px, transparent 1px); - background-size: 4px 4px; -} - -#desktop { - flex: 1; - width: 100%; - display: flex; - align-items: center; - justify-content: center; - padding: 28px 16px 40px; -} - -.top-bar { - position: fixed; - top: 12px; - right: 12px; - z-index: 10; -} - -.window { - background-color: var(--window-bg); - border: 2px solid; - border-color: var(--border-light) var(--border-dark) var(--border-dark) var(--border-light); - padding: 2px; - box-shadow: inset 1px 1px var(--border-mid-light), inset -1px -1px var(--border-mid-dark); -} - -.title-bar { - background-color: var(--title-bg); - color: var(--title-text); - padding: 2px 4px; - font-size: 1.25rem; - display: flex; - justify-content: space-between; - align-items: center; - letter-spacing: 0.8px; -} - -.title-bar-controls { - display: inline-flex; - gap: 2px; -} - -.title-bar-controls button { - background: var(--window-bg); - border: 1px solid; - border-color: var(--border-light) var(--border-dark) var(--border-dark) var(--border-light); - width: 16px; - height: 16px; - font-size: 0.8rem; - line-height: 10px; - text-align: center; - color: var(--window-text); - font-weight: bold; - pointer-events: none; -} - -.window-content { - padding: 12px; -} - -.room-form { - display: flex; - flex-direction: column; - gap: 10px; -} - -.field-group { - display: flex; - flex-direction: column; -} - -.field-group label, -legend { - font-size: 1.2rem; - margin-bottom: 4px; -} - -input[type="text"], -input[type="number"], -input[type="password"], -select { - background: var(--input-bg); - color: var(--window-text); - border: 2px solid; - border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark); - padding: 5px 6px; - font-size: 1.2rem; - width: 100%; - outline: none; -} - -input:focus, -select:focus { - box-shadow: inset 0 0 0 1px var(--title-bg); -} - -input[type="number"] { - appearance: textfield; - -moz-appearance: textfield; -} - -input[type="number"]::-webkit-outer-spin-button, -input[type="number"]::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -.number-input-wrap { - display: flex; - align-items: center; - background: var(--input-bg); - border: 2px solid; - border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark); - padding: 0 6px; - min-height: 38px; -} - -.number-input-wrap input[type="number"] { - border: 0; - box-shadow: none; - background: transparent; - padding: 5px 0; -} - -.number-with-unit { - gap: 8px; -} - -.input-unit { - font-size: 1rem; - opacity: 0.8; - min-width: 26px; - text-align: right; -} - -.btn { - background: var(--window-bg); - color: var(--window-text); - border: 2px solid; - border-color: var(--border-light) var(--border-dark) var(--border-dark) var(--border-light); - box-shadow: inset 1px 1px var(--border-mid-light), inset -1px -1px var(--border-mid-dark); - padding: 4px 12px; - font-size: 1.2rem; - cursor: pointer; - margin-left: 6px; -} - -.btn:active { - border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark); - box-shadow: inset 1px 1px var(--border-mid-dark), inset -1px -1px var(--border-mid-light); - padding: 5px 11px 3px 13px; -} - -.btn-primary { - font-weight: bold; -} - -.actions-row { - text-align: right; - margin-top: 4px; -} - -.status-line { - background: var(--status-bg); - border: 2px solid; - border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark); - padding: 5px 8px; - font-size: 1.1rem; - min-height: 30px; -} - -.hidden { - display: none !important; -} - -/* Config page */ -.config-window { - width: 100%; - max-width: 980px; -} - -.intro-copy { - font-size: 1.3rem; - margin-bottom: 12px; -} - -.config-layout { - display: grid; - grid-template-columns: minmax(0, 1fr) 340px; - gap: 12px; - align-items: start; -} - -.config-panel { - display: flex; - flex-direction: column; - gap: 10px; -} - -.field-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; -} - -.options-box { - padding: 8px; -} - -.options-box legend { - padding: 0 4px; -} - -.option-item { - display: flex; - align-items: center; - gap: 8px; - font-size: 1.2rem; - margin: 6px 0; -} - -.option-item input[type="checkbox"] { - width: 16px; - height: 16px; - accent-color: #000080; -} - -[data-theme="dark"] .option-item input[type="checkbox"] { - accent-color: #00aa00; -} - -.preview-window { - width: 100%; -} - -.preview-content { - display: flex; - flex-direction: column; - gap: 10px; -} - -.preview-meta { - display: flex; - justify-content: space-between; - font-size: 1.1rem; -} - -.preview-board { - background: var(--board-bg); - border: 2px solid; - border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark); - border-radius: 2px; - min-height: 190px; - padding: 10px; -} - -.preview-cards { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-content: flex-start; -} - -.preview-card { - position: relative; - width: 50px; - height: 72px; - background: var(--card-bg); - border: 2px solid #000; - border-radius: 5px; - color: var(--card-text); - display: inline-flex; - align-items: center; - justify-content: center; - font-size: 1.5rem; - font-weight: bold; - box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.5); - user-select: none; - transition: transform 180ms ease; - cursor: grab; -} - -.preview-card.dragging { - opacity: 0.6; -} - -.preview-card.wiggle { - animation: wiggle 250ms linear infinite; -} - -@keyframes wiggle { - 0% { transform: rotate(-2deg); } - 50% { transform: rotate(2deg); } - 100% { transform: rotate(-2deg); } -} - -.preview-card-remove { - position: absolute; - top: -6px; - right: -6px; - width: 18px; - height: 18px; - border: 1px solid; - border-color: var(--border-light) var(--border-dark) var(--border-dark) var(--border-light); - background: #a40000; - color: #fff; - font-size: 0.8rem; - line-height: 14px; - opacity: 0; - transform: scale(0.85); - pointer-events: none; - transition: opacity 130ms ease, transform 130ms ease; - cursor: pointer; -} - -.preview-card:hover .preview-card-remove, -.preview-card-remove:focus { - opacity: 1; - transform: scale(1); - pointer-events: auto; -} - -.preview-card.is-removing { - animation: card-pop-out 190ms ease forwards; -} - -@keyframes card-pop-out { - from { - opacity: 1; - transform: scale(1) rotate(0deg); - } - to { - opacity: 0; - transform: scale(0.58) rotate(-10deg); - } -} - -.hint-text { - font-size: 1rem; -} - -.card-editor { - display: flex; - flex-direction: column; - gap: 4px; -} - -.card-editor label { - font-size: 1.1rem; -} - -.card-editor-row { - display: grid; - grid-template-columns: 1fr auto auto; - gap: 6px; -} - -.deck-tools-row { - display: flex; - gap: 6px; -} - -.icon-btn { - min-width: 36px; - padding-left: 8px; - padding-right: 8px; -} - -.preset-picker { - margin-top: 4px; -} - -.preset-list { - display: flex; - flex-direction: column; - gap: 6px; - max-height: 150px; - overflow-y: auto; - margin-bottom: 8px; -} - -.preset-item { - display: flex; - justify-content: space-between; - gap: 8px; - padding: 6px; - border: 1px dashed var(--border-mid-dark); -} - -.preset-meta { - font-size: 1rem; - opacity: 0.8; -} - -.preset-actions { - display: flex; - align-items: center; - gap: 4px; -} - -.preset-actions .btn { - font-size: 1rem; - padding: 2px 8px; - margin-left: 0; -} - -.import-btn { - margin-left: 0; -} - -.import-pane { - margin-top: 8px; -} - -.import-pane textarea { - width: 100%; - resize: vertical; - background: var(--input-bg); - color: var(--window-text); - border: 2px solid; - border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark); - padding: 6px; - font-size: 1rem; -} - -/* Room page */ -.room-desktop { - align-items: stretch; - justify-content: center; - padding-top: 60px; -} - -.room-grid { - width: min(1180px, 100%); - display: grid; - gap: 12px; - grid-template-columns: 2fr 1fr; - align-items: stretch; -} - -.room-main-window, -.side-panel-window { - min-height: 640px; -} - -.room-meta { - display: flex; - justify-content: space-between; - margin-bottom: 10px; - font-size: 1.15rem; -} - -.voting-board { - background: var(--board-bg); - border: 2px solid; - border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark); - min-height: 230px; - padding: 12px; - display: flex; - flex-wrap: wrap; - gap: 10px; - align-content: flex-start; -} - -.vote-card { - width: 72px; - height: 100px; - border-radius: 6px; - border: 2px solid #000; - background: var(--card-bg); - color: var(--card-text); - font-size: 2rem; - box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.45); - cursor: pointer; - transition: transform 120ms ease; -} - -.vote-card:hover { - transform: translateY(-3px); -} - -.vote-card.is-selected { - outline: 2px dotted currentColor; - outline-offset: -6px; -} - -.vote-card.impact { - animation: vote-impact 170ms ease; -} - -@keyframes vote-impact { - 0% { transform: scale(1); } - 40% { transform: scale(0.91); } - 100% { transform: scale(1); } -} - -.vote-summary-window { - margin-top: 12px; -} - -.vote-summary-content { - display: flex; - flex-direction: column; - gap: 8px; -} - -.summary-table { - width: 100%; - border-collapse: collapse; - background: var(--input-bg); -} - -.summary-table th, -.summary-table td { - border: 1px solid var(--border-mid-dark); - padding: 6px 8px; - text-align: left; -} - -.summary-metrics { - display: flex; - justify-content: space-between; - font-size: 1.1rem; -} - -.side-panel-content { - display: flex; - flex-direction: column; - gap: 10px; - height: calc(100% - 4px); -} - -.participants-scroll { - flex: 1; - min-height: 250px; - overflow-y: auto; -} - -.participant-list { - list-style: none; - background: var(--input-bg); - border: 2px solid; - border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark); - padding: 5px; -} - -.participant-item { - display: flex; - justify-content: space-between; - padding: 5px; - border-bottom: 1px dashed var(--border-mid-dark); - font-size: 1.15rem; -} - -.participant-item:last-child { - border-bottom: 0; -} - -.side-controls { - margin-top: auto; - display: flex; - flex-direction: column; - gap: 8px; -} - -.links-block { - display: grid; - gap: 4px; -} - -.links-block input { - font-size: 1rem; -} - -.admin-controls { - display: flex; - justify-content: flex-end; -} - -.join-window { - position: fixed; - z-index: 30; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: min(420px, 92vw); -} - -#join-error { - margin-top: 8px; -} - -.skeleton-line, -.skeleton-board, -.skeleton-table, -.skeleton-list, -.skeleton-controls { - background: linear-gradient(90deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.08)); - background-size: 220% 100%; - animation: skeleton-shimmer 1.2s ease infinite; - border: 1px solid var(--border-mid-dark); -} - -.skeleton-line { - height: 18px; - margin-bottom: 10px; -} - -.skeleton-line.short { - width: 40%; - margin-top: 12px; -} - -.skeleton-board { - height: 250px; -} - -.skeleton-table { - height: 140px; -} - -.skeleton-list { - height: 350px; - margin-bottom: 10px; -} - -.skeleton-controls { - height: 180px; -} - -@keyframes skeleton-shimmer { - from { background-position: 0 0; } - to { background-position: -200% 0; } -} - -@media (max-width: 960px) { - .config-layout, - .room-grid { - grid-template-columns: 1fr; - } - - .room-main-window, - .side-panel-window { - min-height: unset; - } -} - -@media (max-width: 720px) { - #desktop { - align-items: flex-start; - padding-top: 56px; - } - - .field-row { - grid-template-columns: 1fr; - } - - .actions-row { - display: flex; - justify-content: flex-end; - gap: 8px; - } - - .summary-metrics { - flex-direction: column; - gap: 4px; - } - - .btn { - margin-left: 0; - } -} diff --git a/static/css/themes/modern.css b/static/css/themes/modern.css new file mode 100644 index 0000000..bd36fc1 --- /dev/null +++ b/static/css/themes/modern.css @@ -0,0 +1,79 @@ +: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; + --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; + --card-border-width: 1px; + --selected-outline: 2px solid #175cd3; + --modal-overlay: rgba(15, 23, 42, 0.35); +} + +: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; +} + +:root[data-ui-theme="modern"] body { + background-color: var(--desktop-bg); + background-image: var(--desktop-pattern); + background-size: cover; +} + +:root[data-ui-theme="modern"] .window, +:root[data-ui-theme="modern"] .vote-card, +:root[data-ui-theme="modern"] .preview-card, +:root[data-ui-theme="modern"] .btn, +:root[data-ui-theme="modern"] input, +:root[data-ui-theme="modern"] select, +:root[data-ui-theme="modern"] textarea { + border-radius: 0.45rem; +} + +: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); +} + +: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%); +} + +:root[data-ui-theme="modern"] .preset-modal-overlay { + background: var(--modal-overlay); +} diff --git a/static/css/themes/no-theme.css b/static/css/themes/no-theme.css new file mode 100644 index 0000000..3f615c1 --- /dev/null +++ b/static/css/themes/no-theme.css @@ -0,0 +1,66 @@ +:root[data-ui-theme="none"] { + --font-main: ui-monospace, monospace; + --desktop-bg: #ffffff; + --desktop-pattern: none; + --surface-window: transparent; + --surface-control: #f5f5f5; + --surface-input: #ffffff; + --surface-status: #ffffff; + --text-primary: #000000; + --title-bg: transparent; + --title-text: #000000; + --border-outer: #bdbdbd; + --border-input: #bdbdbd; + --border-muted: #d3d3d3; + --window-border-width: 1px; + --control-border-width: 1px; + --input-border-width: 1px; + --window-shadow: none; + --button-shadow: none; + --button-shadow-active: none; + --focus-ring: 0 0 0 1px #777777; + --card-bg: #ffffff; + --card-text: #000000; + --card-border: #bdbdbd; + --card-border-width: 1px; + --selected-outline: 1px dashed #000000; + --modal-overlay: rgba(0, 0, 0, 0.2); +} + +:root[data-ui-theme="none"][data-theme="dark"] { + --desktop-bg: #111111; + --surface-window: transparent; + --surface-control: #232323; + --surface-input: #161616; + --surface-status: #161616; + --text-primary: #e8e8e8; + --title-bg: transparent; + --title-text: #e8e8e8; + --border-outer: #4a4a4a; + --border-input: #4a4a4a; + --border-muted: #3a3a3a; + --focus-ring: 0 0 0 1px #9a9a9a; + --card-bg: #161616; + --card-text: #e8e8e8; + --card-border: #4a4a4a; +} + +:root[data-ui-theme="none"] body { + background-color: var(--desktop-bg); + background-image: none; +} + +:root[data-ui-theme="none"] .title-bar-controls, +:root[data-ui-theme="none"] .icon-btn img { + display: none; +} + +:root[data-ui-theme="none"] .window, +:root[data-ui-theme="none"] .preview-board, +:root[data-ui-theme="none"] .voting-board { + border-style: dashed; +} + +:root[data-ui-theme="none"] .preset-modal-overlay { + background: var(--modal-overlay); +} diff --git a/static/css/themes/win98.css b/static/css/themes/win98.css new file mode 100644 index 0000000..0790b88 --- /dev/null +++ b/static/css/themes/win98.css @@ -0,0 +1,88 @@ +:root:not([data-ui-theme]), +:root[data-ui-theme="win98"] { + --font-main: 'VT323', monospace; + --desktop-bg: #008080; + --desktop-pattern: radial-gradient(circle, rgba(0, 0, 0, 0.12) 1px, transparent 1px); + --surface-window: #c0c0c0; + --surface-control: #c0c0c0; + --surface-input: #ffffff; + --surface-status: #b3b3b3; + --text-primary: #000000; + --title-bg: #000080; + --title-text: #ffffff; + --border-outer: #000000; + --border-input: #000000; + --border-muted: #808080; + --window-border-width: 2px; + --control-border-width: 1px; + --input-border-width: 2px; + --window-shadow: inset 1px 1px #dfdfdf, inset -1px -1px #808080; + --button-shadow: inset 1px 1px #dfdfdf, inset -1px -1px #808080; + --button-shadow-active: inset 1px 1px #808080, inset -1px -1px #dfdfdf; + --focus-ring: inset 0 0 0 1px #000080; + --card-bg: #ffffff; + --card-text: #000000; + --card-border: #000000; + --card-border-width: 2px; + --selected-outline: 2px dotted currentColor; + --modal-overlay: rgba(0, 0, 0, 0.45); +} + +:root[data-ui-theme="win98"][data-theme="dark"] { + --desktop-bg: #0a0a0a; + --desktop-pattern: radial-gradient(circle, rgba(0, 255, 80, 0.08) 1px, transparent 1px); + --surface-window: #2b2b2b; + --surface-control: #2b2b2b; + --surface-input: #111111; + --surface-status: #1b1b1b; + --text-primary: #e0e0e0; + --title-bg: #000000; + --title-text: #00ff66; + --border-outer: #000000; + --border-input: #555555; + --border-muted: #3b3b3b; + --window-shadow: inset 1px 1px #3a3a3a, inset -1px -1px #1a1a1a; + --button-shadow: inset 1px 1px #3a3a3a, inset -1px -1px #1a1a1a; + --button-shadow-active: inset 1px 1px #1a1a1a, inset -1px -1px #3a3a3a; + --focus-ring: inset 0 0 0 1px #00ff66; + --card-bg: #171717; + --card-text: #00ff66; + --card-border: #555555; +} + +body { + background-color: var(--desktop-bg); + background-image: var(--desktop-pattern); + background-size: 4px 4px; +} + +.preview-board, +.voting-board { + background: #0f6d3d; + border: 2px solid #000; +} + +:root[data-ui-theme="win98"][data-theme="dark"] .preview-board, +:root[data-ui-theme="win98"][data-theme="dark"] .voting-board { + background: #0a2c14; +} + +.preset-modal-overlay { + background: var(--modal-overlay); +} + +.skeleton-line, +.skeleton-board, +.skeleton-table, +.skeleton-list, +.skeleton-controls { + background: linear-gradient(90deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.08)); + background-size: 220% 100%; + border: 1px solid var(--border-muted); + animation: skeleton-shimmer 1.2s ease infinite; +} + +@keyframes skeleton-shimmer { + from { background-position: 0 0; } + to { background-position: -200% 0; } +} diff --git a/static/js/config.js b/static/js/config.js index f8d91b9..883ce5b 100644 --- a/static/js/config.js +++ b/static/js/config.js @@ -13,7 +13,6 @@ const SPECIAL_CARD_ORDER = { '☕': 3, }; -const themeToggleBtn = document.getElementById('theme-toggle'); const roomConfigForm = document.getElementById('room-config-form'); const statusLine = document.getElementById('config-status'); const scaleSelect = document.getElementById('estimation-scale'); @@ -29,14 +28,14 @@ const usernameInput = document.getElementById('username'); const savePresetButton = document.getElementById('save-preset'); const pickerToggleButton = document.getElementById('preset-picker-toggle'); const shareDeckButton = document.getElementById('share-deck'); -const presetPicker = document.getElementById('preset-picker'); +const presetModalOverlay = document.getElementById('preset-modal-overlay'); +const presetModalCloseButton = document.getElementById('preset-modal-close'); +const presetModalDoneButton = document.getElementById('preset-modal-done'); const presetList = document.getElementById('preset-list'); const importToggleButton = document.getElementById('import-toggle'); const importPane = document.getElementById('import-pane'); const importInput = document.getElementById('import-b64'); const importApplyButton = document.getElementById('import-apply'); - -let isDarkMode = false; let nextCardID = 1; let currentCards = []; let draggingCardID = ''; @@ -48,17 +47,6 @@ if (savedUsername && !usernameInput.value) { usernameInput.value = savedUsername; } -themeToggleBtn.addEventListener('click', () => { - isDarkMode = !isDarkMode; - if (isDarkMode) { - document.documentElement.setAttribute('data-theme', 'dark'); - themeToggleBtn.textContent = 'Light Mode'; - return; - } - - document.documentElement.removeAttribute('data-theme'); - themeToggleBtn.textContent = 'Dark Mode'; -}); function parseNumericCard(value) { if (!/^-?\d+(\.\d+)?$/.test(value)) { @@ -341,13 +329,13 @@ function renderPresetList() { } function showPresetPicker() { - presetPicker.classList.remove('hidden'); + presetModalOverlay.classList.remove('hidden'); pickerToggleButton.setAttribute('aria-expanded', 'true'); renderPresetList(); } function hidePresetPicker() { - presetPicker.classList.add('hidden'); + presetModalOverlay.classList.add('hidden'); pickerToggleButton.setAttribute('aria-expanded', 'false'); } @@ -414,7 +402,7 @@ savePresetButton.addEventListener('click', () => { }); pickerToggleButton.addEventListener('click', () => { - if (presetPicker.classList.contains('hidden')) { + if (presetModalOverlay.classList.contains('hidden')) { showPresetPicker(); return; } @@ -464,20 +452,15 @@ importApplyButton.addEventListener('click', () => { } }); -document.addEventListener('click', (event) => { - const target = event.target; - if (!(target instanceof Element)) { - return; +presetModalOverlay.addEventListener('click', (event) => { + if (event.target === presetModalOverlay) { + hidePresetPicker(); } - if (presetPicker.classList.contains('hidden')) { - return; - } - if (presetPicker.contains(target) || pickerToggleButton.contains(target)) { - return; - } - hidePresetPicker(); }); +presetModalCloseButton.addEventListener('click', hidePresetPicker); +presetModalDoneButton.addEventListener('click', hidePresetPicker); + customCardInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); diff --git a/static/js/room.js b/static/js/room.js index 657b05e..9af8f1b 100644 --- a/static/js/room.js +++ b/static/js/room.js @@ -3,7 +3,6 @@ const USERNAME_KEY = 'scrumPoker.username'; const roomID = document.body.dataset.roomId; const params = new URLSearchParams(window.location.search); -const themeToggleBtn = document.getElementById('theme-toggle'); const roomSkeleton = document.getElementById('room-skeleton'); const roomGrid = document.getElementById('room-grid'); const roomTitle = document.getElementById('room-title'); @@ -28,8 +27,6 @@ const joinRoleInput = document.getElementById('join-role'); const joinPasswordInput = document.getElementById('join-password'); const joinAdminTokenInput = document.getElementById('join-admin-token'); const joinError = document.getElementById('join-error'); - -let isDarkMode = false; let participantID = params.get('participantId') || ''; let adminToken = params.get('adminToken') || ''; let eventSource = null; @@ -38,17 +35,6 @@ const savedUsername = localStorage.getItem(USERNAME_KEY) || ''; joinUsernameInput.value = savedUsername; joinAdminTokenInput.value = adminToken; -themeToggleBtn.addEventListener('click', () => { - isDarkMode = !isDarkMode; - if (isDarkMode) { - document.documentElement.setAttribute('data-theme', 'dark'); - themeToggleBtn.textContent = 'Light Mode'; - return; - } - - document.documentElement.removeAttribute('data-theme'); - themeToggleBtn.textContent = 'Dark Mode'; -}); function setJoinError(message) { if (!message) { diff --git a/static/js/ui-controls.js b/static/js/ui-controls.js new file mode 100644 index 0000000..73761aa --- /dev/null +++ b/static/js/ui-controls.js @@ -0,0 +1,75 @@ +(() => { + const THEME_KEY = 'scrumPoker.ui.theme'; + const MODE_KEY = 'scrumPoker.ui.mode'; + const DEFAULT_THEME = 'win98'; + + function applyTheme(theme) { + const normalized = theme || DEFAULT_THEME; + document.documentElement.setAttribute('data-ui-theme', normalized); + } + + function applyMode(mode) { + if (mode === 'dark') { + document.documentElement.setAttribute('data-theme', 'dark'); + return; + } + document.documentElement.removeAttribute('data-theme'); + } + + function getCurrentMode() { + return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'; + } + + function syncControls() { + const theme = document.documentElement.getAttribute('data-ui-theme') || DEFAULT_THEME; + const mode = getCurrentMode(); + + document.querySelectorAll('[data-role="theme-picker"]').forEach((el) => { + el.value = theme; + }); + + document.querySelectorAll('[data-role="mode-toggle"]').forEach((el) => { + el.textContent = mode === 'dark' ? 'Light Mode' : 'Dark Mode'; + }); + } + + function initUIControls() { + if (window.__uiControlsInitialized) { + syncControls(); + return; + } + window.__uiControlsInitialized = true; + + const savedTheme = localStorage.getItem(THEME_KEY) || DEFAULT_THEME; + const savedMode = localStorage.getItem(MODE_KEY) || 'light'; + applyTheme(savedTheme); + applyMode(savedMode); + syncControls(); + + document.querySelectorAll('[data-role="theme-picker"]').forEach((picker) => { + picker.addEventListener('change', (event) => { + const value = event.target.value || DEFAULT_THEME; + localStorage.setItem(THEME_KEY, value); + applyTheme(value); + syncControls(); + }); + }); + + document.querySelectorAll('[data-role="mode-toggle"]').forEach((button) => { + button.addEventListener('click', () => { + const next = getCurrentMode() === 'dark' ? 'light' : 'dark'; + localStorage.setItem(MODE_KEY, next); + applyMode(next); + syncControls(); + }); + }); + } + + window.initUIControls = initUIControls; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initUIControls, { once: true }); + } else { + initUIControls(); + } +})();