mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-08-22 22:32:28 -04:00
958 lines
23 KiB
Svelte
958 lines
23 KiB
Svelte
<script lang="ts">
|
|
import { fade, fly } from "svelte/transition";
|
|
import { getBookById, toOrdinal } from "$lib/utils/game";
|
|
import {
|
|
getVerseSnippet,
|
|
shareResult,
|
|
copyToClipboard as clipboardCopy,
|
|
} from "$lib/utils/share";
|
|
import Container from "./Container.svelte";
|
|
import CountdownTimer from "./CountdownTimer.svelte";
|
|
import StreakCounter from "./StreakCounter.svelte";
|
|
import ChapterGuess from "./ChapterGuess.svelte";
|
|
|
|
interface StatsData {
|
|
solveRank: number;
|
|
guessRank: number;
|
|
totalSolves: number;
|
|
averageGuesses: number;
|
|
tiedCount: number;
|
|
percentile: number;
|
|
}
|
|
|
|
interface WeightedMessage {
|
|
text: string;
|
|
weight: number;
|
|
}
|
|
|
|
let {
|
|
statsData,
|
|
correctBookId,
|
|
handleShare,
|
|
copyToClipboard,
|
|
copied = $bindable(false),
|
|
statsSubmitted,
|
|
guessCount,
|
|
reference,
|
|
onChapterGuessCompleted,
|
|
shareText,
|
|
verseText,
|
|
streak = 0,
|
|
streakPercentile = null,
|
|
isLoggedIn = false,
|
|
anonymousId = "",
|
|
}: {
|
|
statsData: StatsData | null;
|
|
correctBookId: string;
|
|
handleShare: () => void;
|
|
copyToClipboard: () => void;
|
|
copied: boolean;
|
|
statsSubmitted: boolean;
|
|
guessCount: number;
|
|
reference: string;
|
|
onChapterGuessCompleted: () => void;
|
|
shareText: string;
|
|
verseText: string;
|
|
streak?: number;
|
|
streakPercentile?: number | null;
|
|
isLoggedIn?: boolean;
|
|
anonymousId?: string;
|
|
} = $props();
|
|
|
|
let bookName = $derived(getBookById(correctBookId)?.name ?? "");
|
|
let hasWebShare = $derived(
|
|
typeof navigator !== "undefined" && "share" in navigator,
|
|
);
|
|
let copySuccess = $state(false);
|
|
let bubbleCopied = $state(false);
|
|
let copyTracked = $state(false);
|
|
let showSnippetOption = $state(false);
|
|
let includeSnippet = $state(false);
|
|
let showProgressDropdown = $state(false);
|
|
|
|
let effectiveShareText = $derived(
|
|
includeSnippet
|
|
? (() => {
|
|
const snippet = getVerseSnippet(verseText);
|
|
const lines = shareText.split("\n");
|
|
return [
|
|
...lines.slice(0, -1),
|
|
snippet,
|
|
lines[lines.length - 1],
|
|
].join("\n");
|
|
})()
|
|
: shareText,
|
|
);
|
|
|
|
// List of congratulations messages with weights
|
|
const congratulationsMessages: WeightedMessage[] = [
|
|
{ text: "Congratulations!", weight: 10 },
|
|
{ text: "You got it!", weight: 1000 },
|
|
{ text: "Yup.", weight: 100 },
|
|
{ text: "Very nice!", weight: 1 },
|
|
];
|
|
|
|
// Function to select a random message based on weights
|
|
function getRandomCongratulationsMessage(): string {
|
|
// Special case for first try success
|
|
if (guessCount === 1) {
|
|
const n = Math.random();
|
|
if (n < 0.99) {
|
|
return "First try!";
|
|
} else {
|
|
return "Axios!";
|
|
}
|
|
}
|
|
|
|
const totalWeight = congratulationsMessages.reduce(
|
|
(sum, msg) => sum + msg.weight,
|
|
0,
|
|
);
|
|
let random = Math.random() * totalWeight;
|
|
|
|
for (const message of congratulationsMessages) {
|
|
random -= message.weight;
|
|
if (random <= 0) {
|
|
return message.text;
|
|
}
|
|
}
|
|
|
|
// Fallback to first message if something goes wrong
|
|
return congratulationsMessages[0].text;
|
|
}
|
|
|
|
// Generate the congratulations message
|
|
let congratulationsMessage = $derived(getRandomCongratulationsMessage());
|
|
</script>
|
|
|
|
<div class="flex flex-col gap-6">
|
|
<Container
|
|
class="w-full px-4 sm:px-6 py-6 sm:py-8 bg-linear-to-r from-green-400/10 to-green-600/30 text-gray-800 dark:text-gray-100 shadow-2xl text-center fade-in"
|
|
>
|
|
<div class="flex flex-col gap-3">
|
|
<p class="text-2xl sm:text-3xl md:text-4xl leading-tight">
|
|
{congratulationsMessage} The verse is from
|
|
<span class="font-black font-triodion text-3xl md:text-4xl"
|
|
>{bookName}</span
|
|
>.
|
|
</p>
|
|
<p class="text-lg sm:text-xl md:text-2xl">
|
|
You guessed correctly after {guessCount}
|
|
{guessCount === 1 ? "guess" : "guesses"}.
|
|
</p>
|
|
<!-- {#if streak >= 7}
|
|
<p
|
|
class="italic tracking-wider px-8 font-semibold text-gray-500"
|
|
>
|
|
Thank you for making BIBDLE part of your daily routine!
|
|
</p>
|
|
{/if} -->
|
|
</div>
|
|
</Container>
|
|
|
|
<!-- S++ Bonus Challenge for first try -->
|
|
{#if guessCount === 1}
|
|
<ChapterGuess
|
|
{reference}
|
|
bookId={correctBookId}
|
|
onCompleted={onChapterGuessCompleted}
|
|
/>
|
|
{/if}
|
|
|
|
<div class="flex flex-row gap-3 items-stretch w-full">
|
|
<div class="flex-2 min-w-0 flex flex-col">
|
|
<CountdownTimer />
|
|
</div>
|
|
{#if streak > 0}
|
|
<div class="flex-1 min-w-0 flex flex-col">
|
|
<StreakCounter {streak} {streakPercentile} />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Statistics Display -->
|
|
{#if statsData}
|
|
<Container
|
|
class="w-full p-4 bg-white/50 dark:bg-black/30 backdrop-blur-sm text-gray-800 dark:text-gray-100 shadow-lg text-center"
|
|
>
|
|
<div
|
|
class="grid grid-cols-3 gap-4 gap-x-8 text-center"
|
|
in:fade={{ delay: 800 }}
|
|
>
|
|
<!-- Solve Rank Column -->
|
|
<div class="flex flex-col">
|
|
<div
|
|
class="text-3xl sm:text-4xl font-black border-b border-gray-300 dark:border-gray-600 pb-2"
|
|
>
|
|
#{statsData.solveRank}
|
|
</div>
|
|
<div class="text-sm sm:text-sm opacity-90 mt-1">
|
|
You were the {toOrdinal(statsData.solveRank)} person to solve
|
|
today
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Guess Rank Column -->
|
|
<div class="flex flex-col">
|
|
<div
|
|
class="text-3xl sm:text-4xl font-black border-b border-gray-300 dark:border-gray-600 pb-2"
|
|
>
|
|
{toOrdinal(statsData.guessRank)}
|
|
</div>
|
|
<div class="text-sm sm:text-sm opacity-90 mt-1">
|
|
You ranked {toOrdinal(statsData.guessRank)} of {statsData.totalSolves}
|
|
{statsData.totalSolves === 1
|
|
? "solve"
|
|
: "solves"}{statsData.tiedCount > 0
|
|
? `, tied with ${statsData.tiedCount} ${statsData.tiedCount === 1 ? "other" : "others"}`
|
|
: ""}.<br />
|
|
{#if statsData.percentile <= 25}
|
|
<span class="font-bold">
|
|
(Top {statsData.percentile}%)
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Average Column -->
|
|
<div class="flex flex-col">
|
|
<div
|
|
class="text-3xl sm:text-4xl font-black border-b border-gray-300 dark:border-gray-600 pb-2"
|
|
>
|
|
{statsData.averageGuesses}
|
|
</div>
|
|
<div class="text-sm sm:text-sm opacity-90 mt-1">
|
|
People solved after {statsData.averageGuesses}
|
|
{statsData.averageGuesses === 1 ? "guess" : "guesses"} on
|
|
average
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Container>
|
|
{:else if !statsSubmitted}
|
|
<Container
|
|
class="w-full p-6 bg-white/50 dark:bg-black/30 backdrop-blur-sm text-gray-800 dark:text-gray-100 shadow-lg text-center"
|
|
>
|
|
<div class="text-sm opacity-80">Submitting stats...</div>
|
|
</Container>
|
|
{/if}
|
|
|
|
<div class="share-card" in:fly={{ y: 40, duration: 400, delay: 600 }}>
|
|
<div class="big-text font-black! text-center text-gray-300!">
|
|
Share your result
|
|
</div>
|
|
<div class="chat-window">
|
|
<!-- Received bubble: primary action (share / copy) -->
|
|
<div class="bubble-wrapper received-wrapper">
|
|
<button
|
|
class="bubble bubble-received"
|
|
class:success={copySuccess}
|
|
aria-label={hasWebShare ? "Share" : "Copy to clipboard"}
|
|
data-umami-event={hasWebShare
|
|
? "Share"
|
|
: "Copy to Clipboard"}
|
|
onclick={() => {
|
|
if (hasWebShare) {
|
|
shareResult(effectiveShareText);
|
|
} else {
|
|
if (!copyTracked) {
|
|
copyTracked = true;
|
|
}
|
|
clipboardCopy(effectiveShareText);
|
|
copySuccess = true;
|
|
setTimeout(() => {
|
|
copySuccess = false;
|
|
}, 3000);
|
|
}
|
|
}}
|
|
>
|
|
{#if hasWebShare}
|
|
📤 Tap here to share
|
|
{:else if copySuccess}
|
|
✅ Copied!
|
|
{:else}
|
|
📋 Copy to clipboard
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Sent bubble: share text preview -->
|
|
<div class="bubble-wrapper">
|
|
<button
|
|
class="bubble bubble-sent"
|
|
aria-label="Copy to clipboard"
|
|
data-umami-event="Copy to Clipboard"
|
|
onclick={() => {
|
|
if (!copyTracked) {
|
|
copyTracked = true;
|
|
}
|
|
clipboardCopy(effectiveShareText);
|
|
showSnippetOption = true;
|
|
bubbleCopied = true;
|
|
setTimeout(() => {
|
|
bubbleCopied = false;
|
|
}, 2000);
|
|
}}>{effectiveShareText}</button
|
|
>
|
|
{#if hasWebShare}
|
|
<span class="copy-hint"
|
|
>{bubbleCopied ? "copied!" : "(tap to copy)"}</span
|
|
>
|
|
{:else}
|
|
<span class="copy-hint"
|
|
>{bubbleCopied ? "copied!" : ""}</span
|
|
>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{#if streak >= 7}
|
|
<div class="big-text tracking-widest! font-black! text-center mt-4">
|
|
Thank you for making Bibdle part of your daily routine! —George
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if showSnippetOption}
|
|
<div class="snippet-toggle-row mr-4" in:fly={{ y: -8, duration: 220 }}>
|
|
<span class="snippet-label">Show verse snippet in share?</span>
|
|
<button
|
|
class="snippet-toggle"
|
|
class:on={includeSnippet}
|
|
onclick={() => (includeSnippet = !includeSnippet)}
|
|
aria-pressed={includeSnippet}
|
|
aria-label="Show snippet in share"
|
|
>
|
|
<span class="toggle-thumb"></span>
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if isLoggedIn}
|
|
<div class="signin-prompt">
|
|
<a
|
|
href="/progress"
|
|
class="progress-btn w-full"
|
|
data-umami-event="See your progress (logged in)"
|
|
>
|
|
<span>📈 See your progress</span>
|
|
<svg class="progress-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="m9 18 6-6-6-6" />
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
{:else}
|
|
<div class="signin-prompt">
|
|
<button
|
|
type="button"
|
|
class="progress-btn w-full"
|
|
aria-expanded={showProgressDropdown}
|
|
onclick={() => (showProgressDropdown = !showProgressDropdown)}
|
|
data-umami-event="See your progress (logged out)"
|
|
>
|
|
<span>📈 See your progress</span>
|
|
<svg class="progress-chev" class:open={showProgressDropdown} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="m6 9 6 6 6-6" />
|
|
</svg>
|
|
</button>
|
|
{#if showProgressDropdown}
|
|
<p class="signin-text text-gray-800 dark:text-gray-300">
|
|
Create an account (or sign in) to track your progress
|
|
</p>
|
|
<form method="POST" action="/auth/apple" class="w-full">
|
|
<input type="hidden" name="anonymousId" value={anonymousId} />
|
|
<button
|
|
type="submit"
|
|
class="apple-signin-btn"
|
|
data-umami-event="Sign in with Apple"
|
|
>
|
|
<svg
|
|
class="apple-icon"
|
|
viewBox="0 0 24 24"
|
|
fill="currentColor"
|
|
>
|
|
<path
|
|
d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"
|
|
/>
|
|
</svg>
|
|
Sign in with Apple
|
|
</button>
|
|
</form>
|
|
<form method="POST" action="/auth/google" class="w-full">
|
|
<input type="hidden" name="anonymousId" value={anonymousId} />
|
|
<button
|
|
type="submit"
|
|
class="google-signin-btn"
|
|
data-umami-event="Sign in with Google"
|
|
>
|
|
<svg
|
|
class="google-icon"
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
fill="#4285F4"
|
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
/>
|
|
<path
|
|
fill="#34A853"
|
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
/>
|
|
<path
|
|
fill="#FBBC05"
|
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
|
|
/>
|
|
<path
|
|
fill="#EA4335"
|
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
/>
|
|
</svg>
|
|
Sign in with Google
|
|
</button>
|
|
</form>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="signin-prompt">
|
|
<a
|
|
href="https://discord.gg/yWQXbGK8SD"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="discord-btn w-full"
|
|
data-umami-event="Join the BIBDLE Discord"
|
|
>
|
|
<svg
|
|
class="discord-icon"
|
|
viewBox="0 0 127.14 96.36"
|
|
fill="currentColor"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"
|
|
/>
|
|
</svg>
|
|
<span>Join the BIBDLE Discord!</span>
|
|
<svg class="progress-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="m9 18 6-6-6-6" />
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="signin-prompt">
|
|
<a
|
|
href="https://donate.stripe.com/9B6aEZ9WgeZF3qYfF987K00"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="progress-btn w-full"
|
|
data-umami-event="Support bibdle's development"
|
|
>
|
|
<span>❤️ Support bibdle's development</span>
|
|
<svg class="progress-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="m9 18 6-6-6-6" />
|
|
</svg>
|
|
</a>
|
|
<div class="big-text font-black! text-center text-gray-300! mt-2">
|
|
BIBDLE will be ad-free forever.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
:global(.fade-in) {
|
|
animation: fadeIn 0.5s ease-out;
|
|
}
|
|
|
|
/* ── Share card ── */
|
|
.share-card {
|
|
background: oklch(94% 0.028 298.626);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 1.25rem;
|
|
padding: 1.25rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.6rem;
|
|
width: 100%;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.share-card {
|
|
background: oklch(22% 0.025 298.626);
|
|
border-color: rgba(255, 255, 255, 0.1);
|
|
}
|
|
}
|
|
|
|
.share-card::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='1'/%3E%3C/svg%3E");
|
|
opacity: 0.04;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ── Chat window ── */
|
|
.chat-window {
|
|
--sent-color: #0b93f6;
|
|
--received-color: #3a3a3c;
|
|
--bg: oklch(94% 0.028 298.626);
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 0 0.5rem 0;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.chat-window {
|
|
--bg: oklch(22% 0.025 298.626);
|
|
}
|
|
}
|
|
|
|
/* ── Bubble wrappers ── */
|
|
.bubble-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
gap: 2px;
|
|
}
|
|
|
|
.received-wrapper {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
/* ── Shared bubble base ── */
|
|
.bubble {
|
|
position: relative;
|
|
max-width: 255px;
|
|
margin-bottom: 0;
|
|
padding: 10px 20px;
|
|
line-height: 1.3;
|
|
word-wrap: break-word;
|
|
border-radius: 25px;
|
|
text-align: left;
|
|
white-space: pre-wrap;
|
|
font-size: 1rem;
|
|
cursor: pointer;
|
|
transition:
|
|
filter 80ms ease,
|
|
transform 80ms ease;
|
|
user-select: none;
|
|
}
|
|
|
|
/* ── Sent bubble (share text preview) ── */
|
|
.bubble-sent {
|
|
color: white;
|
|
background: var(--sent-color);
|
|
transform: rotate(-2deg);
|
|
}
|
|
|
|
.bubble-sent:hover {
|
|
background-color: #2ea8ff;
|
|
transform: rotate(-2deg) translateY(-2px);
|
|
}
|
|
|
|
.bubble-sent:hover::before {
|
|
background-color: #2ea8ff;
|
|
}
|
|
|
|
.bubble-sent:active {
|
|
background-color: #0878d4;
|
|
transform: rotate(-2deg) scale(0.97);
|
|
}
|
|
|
|
.bubble-sent:active::before {
|
|
background-color: #0878d4;
|
|
}
|
|
|
|
/* Sent tail: bottom-right */
|
|
.bubble-sent::before,
|
|
.bubble-sent::after {
|
|
position: absolute;
|
|
bottom: 0;
|
|
height: 25px;
|
|
content: "";
|
|
}
|
|
|
|
.bubble-sent::before {
|
|
width: 20px;
|
|
right: -7px;
|
|
background-color: var(--sent-color);
|
|
border-bottom-left-radius: 16px 14px;
|
|
}
|
|
|
|
.bubble-sent::after {
|
|
width: 26px;
|
|
right: -26px;
|
|
border-bottom-left-radius: 10px;
|
|
background-color: var(--bg);
|
|
}
|
|
|
|
/* ── Received bubble (action button) ── */
|
|
.bubble-received {
|
|
color: #f5f5f7;
|
|
background: var(--received-color);
|
|
transform: rotate(2deg);
|
|
padding: 14px 24px;
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
min-width: 14rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.bubble-received:hover {
|
|
background-color: #4a4a4e;
|
|
transform: rotate(2deg) translateY(-2px);
|
|
}
|
|
|
|
.bubble-received:hover::before {
|
|
background-color: #4a4a4e;
|
|
}
|
|
|
|
.bubble-received:active {
|
|
background-color: #2a2a2c;
|
|
transform: rotate(2deg) scale(0.97);
|
|
}
|
|
|
|
.bubble-received:active::before {
|
|
background-color: #2a2a2c;
|
|
}
|
|
|
|
.bubble-received.success {
|
|
background: #c7f7d4;
|
|
color: #155724;
|
|
}
|
|
|
|
/* Received tail: bottom-left (mirror of sent) */
|
|
.bubble-received::before,
|
|
.bubble-received::after {
|
|
position: absolute;
|
|
bottom: 0;
|
|
height: 25px;
|
|
content: "";
|
|
}
|
|
|
|
.bubble-received::before {
|
|
width: 20px;
|
|
left: -7px;
|
|
background-color: var(--received-color);
|
|
border-bottom-right-radius: 16px 14px;
|
|
}
|
|
|
|
.bubble-received::after {
|
|
width: 26px;
|
|
left: -26px;
|
|
border-bottom-right-radius: 10px;
|
|
background-color: var(--bg);
|
|
}
|
|
|
|
.bubble-received.success::before {
|
|
background-color: #c7f7d4;
|
|
}
|
|
|
|
/* ── Copy hints ── */
|
|
.copy-hint {
|
|
font-size: 0.68rem;
|
|
color: #444;
|
|
font-weight: 400;
|
|
letter-spacing: 0.01em;
|
|
padding-right: 32px;
|
|
transform: rotate(-2deg);
|
|
transform-origin: right center;
|
|
margin-top: -6px;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.copy-hint {
|
|
color: #aaa;
|
|
}
|
|
}
|
|
|
|
/* ── Snippet toggle row ── */
|
|
.snippet-toggle-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
gap: 0.5rem;
|
|
padding: 0 0.25rem;
|
|
}
|
|
|
|
.snippet-label {
|
|
font-size: 0.72rem;
|
|
color: #666;
|
|
letter-spacing: 0.01em;
|
|
user-select: none;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.snippet-label {
|
|
color: #999;
|
|
}
|
|
}
|
|
|
|
.snippet-toggle {
|
|
position: relative;
|
|
width: 36px;
|
|
height: 20px;
|
|
border-radius: 10px;
|
|
background: #ccc;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: background 200ms ease;
|
|
flex-shrink: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.snippet-toggle.on {
|
|
background: #34c759;
|
|
}
|
|
|
|
.toggle-thumb {
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 50%;
|
|
background: white;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
transition: transform 200ms ease;
|
|
}
|
|
|
|
.snippet-toggle.on .toggle-thumb {
|
|
transform: translateX(16px);
|
|
}
|
|
|
|
/* ── See your progress button (neobrutalist) ── */
|
|
.progress-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
padding: 0.85rem 1.25rem;
|
|
background: #fff;
|
|
color: #111;
|
|
border: 2px solid #000;
|
|
border-radius: 0.75rem;
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
box-shadow: 6px 6px 0 0 #000;
|
|
transition:
|
|
transform 80ms ease,
|
|
box-shadow 80ms ease;
|
|
}
|
|
|
|
.progress-btn:hover {
|
|
transform: translate(-2px, -2px);
|
|
box-shadow: 8px 8px 0 0 #000;
|
|
}
|
|
|
|
.progress-btn:active {
|
|
transform: translate(2px, 2px);
|
|
box-shadow: 2px 2px 0 0 #000;
|
|
}
|
|
|
|
.progress-chev {
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
transition: transform 200ms ease;
|
|
}
|
|
|
|
.progress-chev.open {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.progress-btn {
|
|
background: #111827;
|
|
color: #f9fafb;
|
|
border-color: #fff;
|
|
box-shadow: 6px 6px 0 0 #fff;
|
|
}
|
|
.progress-btn:hover {
|
|
box-shadow: 8px 8px 0 0 #fff;
|
|
}
|
|
.progress-btn:active {
|
|
box-shadow: 2px 2px 0 0 #fff;
|
|
}
|
|
}
|
|
|
|
.signin-prompt {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
/*padding: 1rem 0 0.25rem;*/
|
|
}
|
|
|
|
/* ── Discord button (neobrutalist, discord purple) ── */
|
|
.discord-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
padding: 0.85rem 1.25rem;
|
|
background: #5865f2;
|
|
color: #fff;
|
|
border: 2px solid #000;
|
|
border-radius: 0.75rem;
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
box-shadow: 6px 6px 0 0 #000;
|
|
transition:
|
|
transform 80ms ease,
|
|
box-shadow 80ms ease,
|
|
background-color 120ms ease;
|
|
}
|
|
|
|
.discord-btn:hover {
|
|
transform: translate(-2px, -2px);
|
|
box-shadow: 8px 8px 0 0 #000;
|
|
background: #4752c4;
|
|
}
|
|
|
|
.discord-btn:active {
|
|
transform: translate(2px, 2px);
|
|
box-shadow: 2px 2px 0 0 #000;
|
|
background: #3c45a5;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.discord-btn {
|
|
border-color: #fff;
|
|
box-shadow: 6px 6px 0 0 #fff;
|
|
}
|
|
.discord-btn:hover {
|
|
box-shadow: 8px 8px 0 0 #fff;
|
|
}
|
|
.discord-btn:active {
|
|
box-shadow: 2px 2px 0 0 #fff;
|
|
}
|
|
}
|
|
|
|
.discord-icon {
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.signin-text {
|
|
font-size: 0.85rem;
|
|
text-align: center;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.apple-signin-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
padding: 0.6rem 1rem;
|
|
width: 100%;
|
|
margin-bottom: 0.6rem;
|
|
background: #000;
|
|
color: #fff;
|
|
border-radius: 0.5rem;
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition:
|
|
background 150ms ease,
|
|
transform 80ms ease;
|
|
}
|
|
|
|
.apple-signin-btn:hover {
|
|
background: #222;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.apple-signin-btn:active {
|
|
background: #111;
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.apple-signin-btn {
|
|
background: #fff;
|
|
color: #000;
|
|
}
|
|
.apple-signin-btn:hover {
|
|
background: #e5e5e5;
|
|
}
|
|
.apple-signin-btn:active {
|
|
background: #ccc;
|
|
}
|
|
}
|
|
|
|
.apple-icon {
|
|
width: 1.1rem;
|
|
height: 1.1rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.google-signin-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
padding: 0.6rem 1rem;
|
|
width: 100%;
|
|
background: #000;
|
|
color: #fff;
|
|
border-radius: 0.5rem;
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition:
|
|
background 150ms ease,
|
|
transform 80ms ease;
|
|
}
|
|
|
|
.google-signin-btn:hover {
|
|
background: #222;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.google-signin-btn:active {
|
|
background: #111;
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.google-signin-btn {
|
|
background: #fff;
|
|
color: #000;
|
|
}
|
|
.google-signin-btn:hover {
|
|
background: #e5e5e5;
|
|
}
|
|
.google-signin-btn:active {
|
|
background: #ccc;
|
|
}
|
|
}
|
|
|
|
.google-icon {
|
|
width: 1.1rem;
|
|
height: 1.1rem;
|
|
flex-shrink: 0;
|
|
}
|
|
</style>
|