feat: website release v2 (#616)

* feat: upgrade to svelte 5 & tailwind v4 + add navigation menu to ui package

* feat: website navigation with nav menu from shadcn

* chore: refactor navigation menu storybook

* fix: responsiveness bug

* chore: remove tailwind config file

* chore:
- remove carbon icons from repo
- add Geist font family

* refactor website

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
Adeyemi Adetunji
2025-12-20 12:44:59 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 690cf147ff
commit e6a3cf9332
108 changed files with 2425 additions and 2169 deletions
@@ -0,0 +1,82 @@
<script lang="ts">
import { openModal, htmlBody } from './store';
import * as Dialog from '@cio/ui/base/dialog';
interface Avatar {
src: string;
}
let isAddIcon = false;
let hoveredIndex: number | null = null;
let avatars: Avatar[] = [
{ src: 'avatar_a' },
{ src: 'avatar_b' },
{ src: 'avatar_c' },
{ src: 'avatar_d' },
{ src: 'avatar_e' },
{ src: 'avatar_f' },
{ src: 'avatar_g' },
{ src: 'avatar_h' },
{ src: 'avatar_i' },
{ src: 'avatar_j' },
{ src: 'avatar_k' },
{ src: 'avatar_l' },
{ src: 'avatar_m' },
{ src: 'avatar_n' },
{ src: 'avatar_o' },
{ src: 'avatar_p' },
{ src: 'avatar_q' },
{ src: 'avatar_r' },
{ src: 'avatar_s' },
{ src: 'avatar_t' }
];
function selectAvatar(avatar: Avatar) {
$htmlBody.avatar = avatar.src;
$openModal.avatar = false;
}
</script>
<Dialog.Root bind:open={$openModal.avatar}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Choose your Avatars</Dialog.Title>
</Dialog.Header>
<!-- map for each avatar -->
<div class="flex flex-wrap justify-evenly gap-y-3 mt-3">
{#each avatars as avatar, index}
<button
on:click={() => {
selectAvatar(avatar);
}}
on:mouseenter={() => {
hoveredIndex = index;
isAddIcon = true;
}}
on:mouseleave={() => {
hoveredIndex = null;
isAddIcon = false;
}}
class="relative w-[17%] flex justify-center items-center shadow-sm rounded-full hover:scale-110 transition-all duration-300"
>
<!-- + icon overlay -->
{#if isAddIcon && hoveredIndex === index}
<div
class="w-full h-full flex justify-center items-center bg-white border absolute rounded-full opacity-[0.7] blur-md"
></div>
<img src="/free-tools/progress-report/hover-plus-icon.svg" alt="" class="absolute" />
{/if}
<!-- avatar -->
<img
src="https://assets.cdn.clsrio.com/progress-report/avatar/{avatar.src}.svg"
alt=""
class="w-full"
/>
</button>
{/each}
</div>
<!-- -->
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,81 @@
<script lang="ts">
import * as Dialog from '@cio/ui/base/dialog';
import { htmlBody, openModal } from './store';
interface Background {
src: string;
}
let backgrounds: Background[] = [
{
src: 'blue_tetiary_background'
},
{
src: 'blue_secondary_background'
},
{
src: 'blue_primary_background'
},
{
src: 'purple_tetiary_background'
},
{
src: 'purple_secondary_background'
},
{
src: 'purple_primary_background'
},
{
src: 'black_tetiary_background'
},
{
src: 'black_secondary_background'
},
{
src: 'black_primary_background'
},
{
src: 'yellow_tetiary_background'
},
{
src: 'yellow_secondary_background'
},
{
src: 'yellow_primary_background'
}
];
function selectBackground(background: Background) {
$htmlBody.background = background.src;
$openModal.background = false;
}
</script>
<Dialog.Root bind:open={$openModal.background}>
<Dialog.Content class="h-full max-h-[60vh]">
<Dialog.Header>
<Dialog.Title>Choose your Background</Dialog.Title>
</Dialog.Header>
<!-- -->
<div
class="flex flex-wrap justify-between gap-y-3 mt-3 overflow-y-auto overflow-x-hidden h-[85%]"
>
{#each backgrounds as background}
<button
on:click={() => {
selectBackground(background);
}}
class="flex w-[30%] items-center shadow-sm hover:scale-110 transition-all duration-300"
>
<img
src="https://assets.cdn.clsrio.com/progress-report/backgrounds/{background.src}.webp"
alt=""
class=""
/>
</button>
{/each}
</div>
<!-- -->
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,51 @@
<script lang="ts">
import { toPng } from 'html-to-image';
import { nodeStore, htmlBody, openModal } from './store';
import { Button } from '@cio/ui/base/button';
export let isDisabled: boolean;
export let isDownloading: boolean;
export let text: string = 'Generate Progress Report';
function convertToPng() {
isDownloading = true;
$openModal.showFullscreenButton = false;
const fileName = `${$htmlBody.name} - ${$htmlBody.mood.text} (ClassroomIO_com)`;
if ($nodeStore) {
setTimeout(() => {
toPng($nodeStore, {
width: 500,
height: 500,
quality: 1
})
.then((dataUrl) => {
const link = document.createElement('a');
link.download = fileName;
link.href = dataUrl;
link.click();
})
.catch((error) => {
console.error('Oops, something went wrong!', error);
})
.finally(() => {
isDownloading = false;
$openModal.showFullscreenButton = false;
});
}, 300);
} else {
console.error('Node is not defined');
}
}
</script>
<Button
type="button"
disabled={isDownloading || !isDisabled}
onclick={convertToPng}
loading={isDownloading}
class="{text === 'Generate Progress Report' ? 'block md:hidden' : ''} mt-5 w-full"
>
{isDownloading ? 'Loading...' : text}
</Button>
@@ -0,0 +1,13 @@
<script lang="ts">
import * as Dialog from '@cio/ui/base/dialog';
import Report from './report.svelte';
import { openModal } from './store';
</script>
<Dialog.Root bind:open={$openModal.fullscreen}>
<Dialog.Content showCloseButton={false}>
<!-- -->
<Report />
<!-- -->
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,94 @@
<script lang="ts">
import * as Dialog from '@cio/ui/base/dialog';
import { htmlBody, openModal } from './store';
interface Mood {
text: string;
iconSrc: string;
}
let moods: Mood[] = [
{
text: 'Unmotivated',
iconSrc: 'unmotivated'
},
{
text: 'Feeling Enlightened',
iconSrc: 'feeling-enlightened'
},
{
text: 'Enthusiastic',
iconSrc: 'enthusiastic'
},
{
text: 'Confused',
iconSrc: 'confused'
},
{
text: 'Feeling stuck',
iconSrc: 'feeling-stuck'
},
// {
// text: 'Believing in the process',
// iconSrc: 'believing-in-the-process'
// },
{
text: 'Curious',
iconSrc: 'curious'
},
{
text: 'Achieving Mastery',
iconSrc: 'achieving-mastery'
},
{
text: 'Feeling Positive',
iconSrc: 'feeling-positive'
},
{
text: 'Reflecting',
iconSrc: 'reflecting'
},
{
text: 'Making progress',
iconSrc: 'progress'
},
{
text: 'Feeling Sad',
iconSrc: 'sad'
}
];
function selectMood(mood: Mood) {
$htmlBody.mood.text = mood.text;
$htmlBody.mood.iconSrc = mood.iconSrc;
$openModal.mood = false;
}
</script>
<Dialog.Root bind:open={$openModal.mood}>
<Dialog.Content class="w-full md:w-[30%] md:min-w-[500px]">
<Dialog.Header>
<Dialog.Title>Choose your Mood</Dialog.Title>
</Dialog.Header>
<!-- mapping -->
<div class="flex flex-wrap justify-between gap-y-3 mt-3">
{#each moods as mood}
<button
on:click={() => {
selectMood(mood);
}}
class="flex gap-3 items-center shadow-sm rounded-full px-4 py-1 bg-[#F1F6FF] hover:bg-[#0233BD] hover:text-white transition-all duration-700"
>
<p class="text-xs md:text-sm font-semibold">{mood.text}</p>
<img
src="https://assets.cdn.clsrio.com/progress-report/emojis/{mood.iconSrc}.png"
alt={mood.text}
class="w-4"
/>
</button>
{/each}
</div>
<!-- -->
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,142 @@
<script lang="ts">
import { htmlBody, nodeStore, openModal } from './store';
import { derived } from 'svelte/store';
const colorMap = {
blue_tetiary_background: '#0542CC',
blue_secondary_background: '#0542CC',
blue_primary_background: '#0542CC',
purple_tetiary_background: '#9D4EDD',
purple_secondary_background: '#9D4EDD',
purple_primary_background: '#9D4EDD',
black_tetiary_background: '#252525',
black_secondary_background: '#252525',
black_primary_background: '#252525',
yellow_tetiary_background: '#FFBC42',
yellow_secondary_background: '#FFBC42',
yellow_primary_background: '#FFBC42'
};
type ColorMapKey = keyof typeof colorMap;
const backgroundColor = derived(htmlBody, ($htmlBody) => {
return colorMap[$htmlBody.background as ColorMapKey] || '#1e5ed2';
});
let node: any;
$: nodeStore.set(node);
$: rootBgImage = `https://assets.cdn.clsrio.com/progress-report/backgrounds/${
$htmlBody.background || 'blue_tetiary_background'
}.webp`;
</script>
<div
class="h-full min-h-[500px] max-h-[500px] md:h-[60vh] w-full border relative flex items-center justify-center"
style={`font-family: "Space Grotesk", sans-serif; background-image: url('${rootBgImage}'); background-size: cover; background-repeat: no-repeat`}
bind:this={node}
>
<!-- fullscreen button -->
{#if $openModal.showFullscreenButton}
{#if $openModal.fullscreen}
<button
type="button"
on:click={() => ($openModal.fullscreen = !$openModal.fullscreen)}
class="absolute top-4 right-4 p-1.5 rounded-full hover:scale-90 transition-all duration-300 bg-white"
>
<img src="/free-tools/progress-report/close-icon.svg" alt="" class="" />
</button>
{:else}
<button
type="button"
on:click={() => ($openModal.fullscreen = !$openModal.fullscreen)}
class="absolute top-4 right-4 w-7 hover:scale-90 transition-all duration-300"
>
<img src="/free-tools/progress-report/full-screen-icon.svg" alt="" class="" />
</button>
{/if}
{/if}
<div class="mt-20">
<!-- using both md & lg for the width here because the lib doesn't consider md to be medium screens and above, using md alone will make the output break on desktop so i had to also specify for large screens -->
<div
class="rounded-lg shadow-md bg-white border-4 border-gray-100 z-20 mx-auto flex justify-center items-center w-[16rem] h-[16rem]"
>
<div class="relative w-full h-full flex flex-col justify-center gap-3 items-center px-2">
<!-- avatar -->
<img
src={`https://assets.cdn.clsrio.com/progress-report/avatar/${
$htmlBody.avatar || 'avatar_a'
}.svg`}
alt=""
class="absolute top-0 w-[25%] h-[25%] ml-2 -mt-[2.2rem]"
/>
<!-- mood -->
{#if $htmlBody.name || $htmlBody.mood.text}
<div
style="background-color: {$backgroundColor}; color: white; display: flex;"
class="px-3 py-0.5 border border-[#EDEDED] rounded-2xl mt-6 md:mt-3 font-semibold text-[10px] md:text-[0.7rem] flex items-center justify-between"
>
<span class="mr-0.5">{$htmlBody.name}</span>
{#if $htmlBody.mood.text}
<span class="flex gap-1 items-center">
<p class="font-semibold text-[10px] md:text-[0.7rem]">
is {$htmlBody.mood.text}
</p>
<img
src="https://assets.cdn.clsrio.com/progress-report/emojis/{$htmlBody.mood
.iconSrc}.png"
alt={$htmlBody.mood.text}
class="w-3"
/>
</span>
{/if}
</div>
{:else}
<p
style="background-color: {$backgroundColor}"
class="px-3 py-0.5 border border-[#EDEDED] text-white rounded-2xl mt-3 font-medium text-[9px]"
>
Add your name
</p>
{/if}
<!-- learning -->
<span
class="w-full md:my-2 p-1 overflow-hidden text-[0.7rem] md:text-[0.8rem] font-normal text-center outline-none h-[10vh] md:h-[12vh]"
>
{$htmlBody.learning}
</span>
<!-- progress -->
<div class="flex flex-col justify-center items-center mb-2 md:mb-0">
<div class="border border-[#d9e0f5] w-full h-4 rounded-full p-0.5 bg-[#f1f6ff]">
<div
class="progress-range rounded-full w-full h-full"
style="background: {$backgroundColor}; width: {$htmlBody.progress}%"
/>
</div>
<p class="text-[12px] font-semibold mt-1">Progress Achieved: {$htmlBody.progress}%</p>
</div>
</div>
</div>
<div
style="display: flex;"
class="border mt-20 bottom-5 w-[50%] mx-auto flex gap-1 items-center justify-center bg-white rounded-md px-2 py-1"
>
<img src="/logo-192.png" alt="classroomio logo" class="w-4" />
<p class="text-[10px] font-bold">ClassroomIO.com</p>
</div>
<!-- -->
</div>
</div>
<!-- importing font family -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap');
</style>
@@ -0,0 +1,57 @@
import { derived, writable } from 'svelte/store';
import type { Writable } from 'svelte/store';
interface OpenModal {
avatar: boolean;
background: boolean;
fullscreen: boolean;
showFullscreenButton: boolean;
mood: boolean;
}
export interface HtmlBody {
name: string;
learning: string;
progress: number;
avatar: string;
background: string;
mood: {
text: string;
iconSrc: string;
};
}
// store for the node binded to the report component for the component to image conversion
export const nodeStore = writable(null);
export const openModal: Writable<OpenModal> = writable({
avatar: false,
background: false,
fullscreen: false,
showFullscreenButton: false,
mood: false
});
// report html
export const htmlBody: Writable<HtmlBody> = writable({
name: '',
learning: '',
progress: 10,
avatar: '',
background: '',
mood: {
text: '',
iconSrc: ''
}
});
// a mini validation for the htmlBody store that ensures all fields are filled
export const isFormComplete = derived(htmlBody, ($htmlBody) => {
return !!(
$htmlBody.name.trim() &&
$htmlBody.learning.trim() &&
$htmlBody.progress > 0 &&
$htmlBody.mood.text.trim() &&
$htmlBody.mood.iconSrc.trim()
);
});
@@ -0,0 +1,70 @@
<script lang="ts">
import { sineInOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
export let activityName: string;
export let formattedTime: string;
export let displayTime: string;
export let resetTimer: () => void;
export let toggleSound: () => void;
export let toggleTimer: () => void;
export let restartTimer: () => void;
export let isPaused: boolean;
export let soundOn: boolean;
</script>
<div transition:fly={{ y: 100, delay: 0, easing: sineInOut }}>
<div class="px-6 pt-8 text-center">
<button
type="button"
on:click={resetTimer}
class="text-[10px] text-[#656565] font-semibold uppercase underline"
>set timer for another activity</button
>
<button
class="border mx-auto mt-7 mb-3 px-2 py-0.5 rounded-full flex items-center gap-1 text-sm text-[#656565] font-semibold border-[#C2D2FF] bg-[#DCE5FF]"
>
{activityName}:
<span class="text-[#0233BD]">{formattedTime}</span>
</button>
<!-- countdown -->
<h1 class="text-[#040F2D] mb-14 text-6xl font-bold">{displayTime}</h1>
</div>
<div class="flex">
<button
type="button"
on:click={toggleSound}
class="bg-[#F7F7F7] w-[34%] flex flex-col gap-1 items-center uppercase text-[10px] py-5 md:py-10 rounded-bl-md hover:bg-[#F1F6FF] transition-all duration-500"
>
{#if soundOn}
<img src="/free-tools/stopwatch/speaker-icon.svg" alt="" class="w-7 md:w-12" />
{:else}
<img src="/free-tools/stopwatch/no-speaker-icon.svg" alt="" class="w-7 md:w-12" />
{/if}
buzzer</button
>
<button
type="button"
on:click={toggleTimer}
class="bg-[#0F62FE] text-white w-[34%] flex flex-col gap-1 items-center uppercase text-[10px] py-5 md:py-10"
>
{#if isPaused}
<img src="/free-tools/stopwatch/play-icon.svg" alt="" class="w-7 md:w-12" />
{:else}
<img src="/free-tools/stopwatch/pause-icon.svg" alt="" class="w-7 md:w-12" />
{/if}
{isPaused ? 'play' : 'pause'}</button
>
<button
type="button"
on:click={restartTimer}
class="bg-[#F7F7F7] w-[34%] flex flex-col gap-1 items-center uppercase text-[10px] py-5 md:py-10 rounded-br-md hover:bg-[#F1F6FF] transition-all duration-500"
>
<img src="/free-tools/stopwatch/reset-icon.svg" alt="" class="w-7 md:w-12" />
restart</button
>
</div>
</div>
@@ -0,0 +1,99 @@
<script lang="ts">
import { sineInOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
import WinningLine from './winning-line.svelte';
import { changeToComputer } from './store';
interface Game {
board: Array<Array<string>>;
click: (rowIndex: number, columnIndex: number) => Game;
}
export let game: Game;
export let winner = null;
let customDelay: number;
$: {
if ($changeToComputer.change) {
customDelay = 400;
} else {
customDelay = 0;
}
}
</script>
<div class="relative">
<table>
<tbody>
{#each game.board as row, rowIndex}
<tr>
{#each row as column, columnIndex}
<td
class="cursor-pointer text-center h-25 w-[1.5em] border-r-10 border-b-10 border-white last:border-r-0"
on:click={() => {
game = game.click(rowIndex, columnIndex);
}}
>
{#if column === 'X'}
<img
transition:fly={{ y: 100, delay: 0, easing: sineInOut }}
src="/free-tools/tic-tac/tic-tac-x.svg"
alt="X icon"
/>
{:else if column === 'O'}
<img
transition:fly={{ y: 100, delay: customDelay, easing: sineInOut }}
src="/free-tools/tic-tac/tic-tac-o.svg"
alt="O icon"
/>
{:else}
<!-- empty cell -->
{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
<WinningLine {winner} />
</div>
<style>
table {
overflow: hidden;
border-collapse: collapse;
font-size: 4em;
color: white;
border: 0px;
z-index: 10;
position: relative;
}
td {
overflow: hidden;
text-align: center;
height: 100px;
width: 100px;
border-right: 10px solid white;
border-bottom: 10px solid white;
position: relative;
z-index: 10;
}
td:last-child {
border-right: none;
}
tr:last-child td {
border-bottom: none;
}
@media (max-width: 600px) {
td {
width: 16vw;
height: 16vw;
}
}
</style>
@@ -0,0 +1,178 @@
import { scores, changeToComputer } from './store';
let activateComp = false;
changeToComputer.subscribe((value) => {
if (value && value.change === true) {
activateComp = true;
} else {
activateComp = false;
}
});
export function TicTacToe({ onWin, onTie }: { onWin: (winner: any) => void, onTie: () => void }) {
const board = [
['', '', ''],
['', '', ''],
['', '', '']
];
let turn = 'X';
let done = false;
return {
board,
click(rowIndex:number, columnIndex:number) {
if (!done && !board[rowIndex][columnIndex]) {
board[rowIndex][columnIndex] = turn;
turn = turn === 'X' ? 'O' : 'X';
const winner = getWinner(board);
if (winner) {
onWin(winner);
done = true;
} else if (isBoardFull(board)) {
onTie();
scores.update((currentScores) => {
return { ...currentScores, draw: currentScores.draw + 1 };
});
done = true;
} else {
if (activateComp) {
const aiMove = getBestMove(board);
if (aiMove) {
board[aiMove.row][aiMove.column] = 'O';
turn = 'X';
const aiWinner = getWinner(board);
if (aiWinner) {
onWin(aiWinner);
done = true;
} else if (isBoardFull(board)) {
onTie();
scores.update((currentScores) => {
return { ...currentScores, draw: currentScores.draw + 1 };
});
done = true;
}
} else {
console.error("AI could not find a valid move.");
}
}
}
}
return this;
}
};
}
function getWinner(board: string[][]) {
// check rows
for (let row = 0; row < 3; row++) {
if (board[row][0] === board[row][1] && board[row][0] === board[row][2] && board[row][0]) {
return { symbol: board[row][0], direction: 'horizontal', row };
}
}
// check columns
for (let column = 0; column < 3; column++) {
if (
board[0][column] === board[1][column] &&
board[0][column] === board[2][column] &&
board[0][column]
) {
return { symbol: board[0][column], direction: 'vertical', column };
}
}
// check diagonals
if (board[0][0] === board[1][1] && board[0][0] == board[2][2] && board[0][0]) {
return { symbol: board[0][0], direction: 'diagonal', diagonal: 'top-left-to-bottom-right' };
}
if (board[0][2] === board[1][1] && board[0][2] == board[2][0] && board[0][2]) {
return { symbol: board[0][2], direction: 'diagonal', diagonal: 'top-right-to-bottom-left' };
}
// no winner
return null;
}
function isBoardFull(board: string[][]) {
for (let row = 0; row < 3; row++) {
for (let column = 0; column < 3; column++) {
if (!board[row][column]) {
return false;
}
}
}
return true;
}
function getBestMove(board: string[][]) {
let bestScore = -Infinity;
let move;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
// check if cell is empty
if (board[i][j] === '') {
board[i][j] = 'O'; // make the move
let score = minimax(board, 0, false); // call minimax on the new board
board[i][j] = ''; // undo the move
if (score > bestScore) {
bestScore = score;
move = { row: i, column: j };
}
}
}
}
return move;
}
function minimax(board: string[][], depth: number, isMaximizing: boolean) {
const result = getWinner(board);
if (result !== null) {
if (result.symbol === 'O') {
return 10 - depth; // ai wins
} else if (result.symbol === 'X') {
return depth - 10; // player wins
} else {
return 0; // draw
}
}
if (isMaximizing) {
let bestScore = -Infinity;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (board[i][j] === '') {
board[i][j] = 'O';
let score = minimax(board, depth + 1, false);
board[i][j] = '';
bestScore = Math.max(score, bestScore);
}
}
}
return bestScore;
} else {
let bestScore = Infinity;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (board[i][j] === '') {
board[i][j] = 'X';
let score = minimax(board, depth + 1, true);
board[i][j] = '';
bestScore = Math.min(score, bestScore);
}
}
}
return bestScore;
}
}
@@ -0,0 +1,22 @@
import { writable } from 'svelte/store';
import type {Writable} from 'svelte/store'
interface Scores {
X: number;
O: number;
draw: number;
}
interface ChangeToComputer {
change: boolean;
}
export const scores: Writable<Scores> = writable({
X: 0,
O: 0,
draw: 0
});
export const changeToComputer: Writable<ChangeToComputer> = writable({
change: false
});
@@ -0,0 +1,80 @@
<script lang="ts">
import { fade } from 'svelte/transition';
export let winner: Winner | null = null;
interface Winner {
direction: 'horizontal' | 'vertical' | 'diagonal';
row?: number;
column?: number;
diagonal?: 'top-left-to-bottom-right' | 'top-right-to-bottom-left';
}
$: if (winner) {
if (winner.direction === 'vertical' && winner.column !== undefined) {
document.documentElement.style.setProperty('--col-index', winner.column.toString());
} else if (winner.direction === 'horizontal' && winner.row !== undefined) {
document.documentElement.style.setProperty('--row-index', winner.row.toString());
}
}
</script>
{#if winner}
<div
transition:fade={{ delay: 500, duration: 2 }}
class="winning-line absolute bg-[#ffc300] z-[100]"
class:horizontal-line={winner.direction === 'horizontal'}
class:vertical-line={winner.direction === 'vertical'}
class:diagonal-line-1={winner.direction === 'diagonal' &&
winner.diagonal === 'top-left-to-bottom-right'}
class:diagonal-line-2={winner.direction === 'diagonal' &&
winner.diagonal === 'top-right-to-bottom-left'}
></div>
{/if}
<style>
.horizontal-line {
top: calc(var(--row-index) * 100px + 50px - 5px);
left: 0;
width: 100%;
height: 10px;
}
.vertical-line {
top: 0;
left: calc(var(--col-index) * 100px + 50px - 5px);
width: 10px;
height: 100%;
}
.diagonal-line-1 {
top: 0;
left: 0;
width: 140%;
height: 100%;
background: none;
border-top: 10px solid #ffc300;
transform: rotate(45deg);
transform-origin: top left;
}
.diagonal-line-2 {
top: 0;
right: 0;
width: 140%;
height: 100%;
background: none;
border-top: 10px solid #ffc300;
transform: rotate(-45deg);
transform-origin: top right;
}
@media (max-width: 600px) {
.horizontal-line {
top: calc(var(--row-index) * 16vw + 8vw - 5px);
}
.vertical-line {
left: calc(var(--col-index) * 16vw + 8vw - 5px);
}
}
</style>