diff --git a/static/css/layout.css b/static/css/layout.css index 39944e1..bc8da5c 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -119,6 +119,10 @@ transition: transform 170ms ease; } +.preview-card .card-center-icon { + font-size: 1.18rem; +} + .preview-card.dragging { opacity: 0.65; } @@ -286,10 +290,18 @@ width: 4.3rem; height: 6rem; border-radius: 0.4rem; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; cursor: pointer; transition: transform 120ms ease; } +.vote-card .card-center-icon { + font-size: 1.65rem; +} + .vote-card:hover { transform: translateY(-0.2rem); } diff --git a/static/css/main.css b/static/css/main.css index 35783e0..4363f8b 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -165,6 +165,34 @@ input[type="number"]::-webkit-inner-spin-button { color: var(--card-text); } +.card-corner { + position: absolute; + z-index: 1; + font-weight: 700; + font-size: 0.82rem; + line-height: 1; + pointer-events: none; + user-select: none; +} + +.card-corner.top-left { + top: 0.34rem; + left: 0.34rem; +} + +.card-corner.bottom-right { + right: 0.34rem; + bottom: 0.34rem; + transform: rotate(180deg); +} + +.card-center-icon { + z-index: 1; + line-height: 1; + pointer-events: none; + user-select: none; +} + .vote-card.is-selected { outline: var(--selected-outline); outline-offset: -0.35rem; diff --git a/static/js/config.js b/static/js/config.js index 883ce5b..d160215 100644 --- a/static/js/config.js +++ b/static/js/config.js @@ -13,6 +13,8 @@ const SPECIAL_CARD_ORDER = { '☕': 3, }; +const CARD_ICONS = ['★', '◆', '✦', '☀', '☘', '⚙', '♣', '♠', '♥', '♦', '✚', '⚡', '☾', '✿']; + const roomConfigForm = document.getElementById('room-config-form'); const statusLine = document.getElementById('config-status'); const scaleSelect = document.getElementById('estimation-scale'); @@ -82,6 +84,36 @@ function createCard(value) { return { id: String(nextCardID++), value: value.toString() }; } +function iconForCard(value) { + if (value === '?') return '❓'; + if (value === '☕') return '☕'; + if (value === '∞') return '∞'; + + let hash = 0; + for (let i = 0; i < value.length; i += 1) { + hash = (hash * 31 + value.charCodeAt(i)) >>> 0; + } + return CARD_ICONS[hash % CARD_ICONS.length]; +} + +function appendCardFace(el, value) { + const topLeft = document.createElement('span'); + topLeft.className = 'card-corner top-left'; + topLeft.textContent = value; + + const center = document.createElement('span'); + center.className = 'card-center-icon'; + center.textContent = iconForCard(value); + + const bottomRight = document.createElement('span'); + bottomRight.className = 'card-corner bottom-right'; + bottomRight.textContent = value; + + el.appendChild(topLeft); + el.appendChild(center); + el.appendChild(bottomRight); +} + function getCardsForScale(scale) { return (SCALE_PRESETS[scale] || SCALE_PRESETS.fibonacci).map(createCard); } @@ -157,8 +189,8 @@ function buildCardElement(card) { const cardEl = document.createElement('div'); cardEl.className = 'preview-card'; cardEl.dataset.cardId = card.id; - cardEl.textContent = card.value; cardEl.draggable = true; + appendCardFace(cardEl, card.value); const removeBtn = document.createElement('button'); removeBtn.type = 'button'; diff --git a/static/js/room.js b/static/js/room.js index 348f552..5c58419 100644 --- a/static/js/room.js +++ b/static/js/room.js @@ -1,4 +1,5 @@ const USERNAME_KEY = 'scrumPoker.username'; +const CARD_ICONS = ['★', '◆', '✦', '☀', '☘', '⚙', '♣', '♠', '♥', '♦', '✚', '⚡', '☾', '✿']; const roomID = document.body.dataset.roomId; const params = new URLSearchParams(window.location.search); @@ -137,6 +138,36 @@ function parseNumericVote(value) { return Number(value); } +function iconForCard(value) { + if (value === '?') return '❓'; + if (value === '☕') return '☕'; + if (value === '∞') return '∞'; + + let hash = 0; + for (let i = 0; i < value.length; i += 1) { + hash = (hash * 31 + value.charCodeAt(i)) >>> 0; + } + return CARD_ICONS[hash % CARD_ICONS.length]; +} + +function appendCardFace(el, value) { + const topLeft = document.createElement('span'); + topLeft.className = 'card-corner top-left'; + topLeft.textContent = value; + + const center = document.createElement('span'); + center.className = 'card-center-icon'; + center.textContent = iconForCard(value); + + const bottomRight = document.createElement('span'); + bottomRight.className = 'card-corner bottom-right'; + bottomRight.textContent = value; + + el.appendChild(topLeft); + el.appendChild(center); + el.appendChild(bottomRight); +} + function calculateSummary(state) { const rows = new Map(); const numericVotes = []; @@ -221,7 +252,8 @@ function renderCards(cards, participants, isRevealed) { const card = document.createElement('button'); card.type = 'button'; card.className = 'vote-card'; - card.textContent = value; + card.setAttribute('aria-label', `Vote ${value}`); + appendCardFace(card, value); if (selfVote === value && !isRevealed) { card.classList.add('is-selected');