diff --git a/frontend/src/components/RankBonusModal.svelte b/frontend/src/components/RankBonusModal.svelte new file mode 100644 index 0000000..33a6ad2 --- /dev/null +++ b/frontend/src/components/RankBonusModal.svelte @@ -0,0 +1,104 @@ + + +{#if me.needs_rank_3_bonus} + +{/if} + + diff --git a/frontend/src/components/scene/ObstacleItem.svelte b/frontend/src/components/scene/ObstacleItem.svelte new file mode 100644 index 0000000..fae2514 --- /dev/null +++ b/frontend/src/components/scene/ObstacleItem.svelte @@ -0,0 +1,116 @@ + + +
{ if (!is_completed) e.preventDefault(); }} + on:dragenter={(e) => { if (!is_completed) e.currentTarget.classList.add("drag-over"); }} + on:dragleave={(e) => { if (!is_completed) e.currentTarget.classList.remove("drag-over"); }} + on:drop={(e) => { + if (is_completed) return; + e.preventDefault(); + e.currentTarget.classList.remove("drag-over"); + const cardCode = e.dataTransfer.getData('text/plain'); + if (cardCode) { + if (isJoker(cardCode)) playJoker(cardCode); + else playCard(cardCode); + } + }}> + {#if error} +
{error}
+ {/if} +
+ + Original card +
+
+

{obs.title} {#if in_challenge && !embedded}⚔️ In Challenge{/if}

+ + {is_red ? 'Red' : 'Black'} obstacle · match to draw + +

{obs.description}

+
+ + +
+ Current Difficulty + + {#if is_face_active} + {challengedRank !== null ? `${challengedRank} (Rat's Rank)` : "Challenged Rat's Rank"} + {:else} + {obs.current_value} + {/if} + +
+ + +
+ Successes + {success_count} / {state.players.length} +
+ + +
+
Card History (Column)
+
+ + {#each column_cards as pc} + + {/each} +
+
+ + {#if is_completed && state.player.role === 'deep'} +
+ +
+ {/if} +
diff --git a/frontend/src/lib/tooltip.js b/frontend/src/lib/tooltip.js new file mode 100644 index 0000000..9d09846 --- /dev/null +++ b/frontend/src/lib/tooltip.js @@ -0,0 +1,84 @@ +// A styled hover/focus tooltip that replaces the native `title` attribute. +// +// use:tooltip={"plain text"} -> rendered as text +// use:tooltip={{ html: "rich" }} -> rendered as HTML (trusted, static only) +// +// Only ever pass trusted, static HTML (e.g. built from the rulebook tables in +// lib/cards.js); never interpolate raw player input into the `html` form. +// +// The floating element is appended to and positioned with fixed +// coordinates so it escapes any overflow:hidden / transformed ancestors +// (cards, modals, scrolling columns). + +function contentEmpty(content) { + if (!content) return true; + if (typeof content === 'object') return !content.html; + return String(content).trim() === ''; +} + +export function tooltip(node, content) { + let current = content; + let tip = null; + + function show() { + if (tip || contentEmpty(current)) return; + tip = document.createElement('div'); + tip.className = 'tooltip-pop'; + tip.setAttribute('role', 'tooltip'); + if (typeof current === 'object' && current.html != null) { + tip.innerHTML = current.html; + } else { + tip.textContent = String(current); + } + document.body.appendChild(tip); + position(); + requestAnimationFrame(() => tip && tip.classList.add('visible')); + } + + function position() { + if (!tip) return; + const r = node.getBoundingClientRect(); + const t = tip.getBoundingClientRect(); + const margin = 8; + // Prefer above the target; flip below when there isn't room. + let top = r.top - t.height - margin; + if (top < margin) top = r.bottom + margin; + if (top + t.height > window.innerHeight - margin) { + top = Math.max(margin, window.innerHeight - t.height - margin); + } + let left = r.left + r.width / 2 - t.width / 2; + left = Math.max(margin, Math.min(left, window.innerWidth - t.width - margin)); + tip.style.top = `${top}px`; + tip.style.left = `${left}px`; + } + + function hide() { + if (tip) { + tip.remove(); + tip = null; + } + } + + node.addEventListener('mouseenter', show); + node.addEventListener('mouseleave', hide); + node.addEventListener('focus', show); + node.addEventListener('blur', hide); + + return { + update(newContent) { + current = newContent; + if (tip) { + // Refresh the content of an already-visible tip in place. + hide(); + show(); + } + }, + destroy() { + hide(); + node.removeEventListener('mouseenter', show); + node.removeEventListener('mouseleave', hide); + node.removeEventListener('focus', show); + node.removeEventListener('blur', hide); + }, + }; +}