Compare commits

..

6 Commits

6 changed files with 589 additions and 153 deletions

View File

@@ -9,5 +9,8 @@ __pycache__/
.ruff_cache/ .ruff_cache/
.uvicorn.pid .uvicorn.pid
docs/ docs/images/
docs/screenshots/
docs/*.pdf
docs/presentation.md
README.md README.md

View File

@@ -6,9 +6,11 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir --upgrade pip setuptools wheel -i https://package-mirror.liara.ir/repository/pypi/simple \
&& pip install --no-cache-dir -r requirements.txt -i https://package-mirror.liara.ir/repository/pypi/simple
COPY app ./app COPY app ./app
COPY docs/slides ./docs/slides
EXPOSE 8000 EXPOSE 8000

View File

@@ -1,7 +1,7 @@
from pathlib import Path from pathlib import Path
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import FileResponse from fastapi.responses import FileResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from app.websocket.chat import router as chat_router from app.websocket.chat import router as chat_router
@@ -9,6 +9,7 @@ from app.websocket.manager import manager
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
STATIC_DIR = BASE_DIR / "static" STATIC_DIR = BASE_DIR / "static"
SLIDES_DIR = BASE_DIR.parent / "docs" / "slides"
app = FastAPI(title="Simple WebSocket Chatroom") app = FastAPI(title="Simple WebSocket Chatroom")
@@ -20,6 +21,17 @@ app.include_router(chat_router)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
@app.get("/slides", include_in_schema=False)
async def slides_redirect():
"""Redirect to the static slides directory so relative asset paths work."""
return RedirectResponse(url="/slides/")
# Serves the presentation and its relative assets.
# This keeps docs/slides/index.html openable directly from the file system too.
app.mount("/slides", StaticFiles(directory=SLIDES_DIR, html=True), name="slides")
@app.get("/") @app.get("/")
async def home(): async def home():
"""Serve the chatroom GUI.""" """Serve the chatroom GUI."""

Binary file not shown.

View File

@@ -119,6 +119,326 @@
<aside class="notes">Explain the code structure: FastAPI serves the frontend, the WebSocket endpoint accepts connections, and the manager stores active clients by room before broadcasting messages.</aside> <aside class="notes">Explain the code structure: FastAPI serves the frontend, the WebSocket endpoint accepts connections, and the manager stores active clients by room before broadcasting messages.</aside>
</section> </section>
<section class="slide code-slide" data-title="Backend Router">
<h2>Backend Codes</h2>
<p class="subtitle">WebSocket router setup in FastAPI.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
<pre><code><span class="line"><span class="kw">import</span> json</span>
<span class="line"><span class="kw">from</span> datetime <span class="kw">import</span> datetime, timezone</span>
<span class="line"></span>
<span class="line"><span class="kw">from</span> fastapi <span class="kw">import</span> APIRouter, WebSocket, WebSocketDisconnect</span>
<span class="line"></span>
<span class="line"><span class="kw">from</span> app.websocket.manager <span class="kw">import</span> manager</span>
<span class="line"></span>
<span class="line">router = APIRouter()</span></code></pre>
</div>
<aside class="notes">You can start by explaining that this module owns the WebSocket endpoint and delegates connection storage to the shared manager.</aside>
</section>
<section class="slide code-slide" data-title="Backend Payload">
<h2>Message Payload Shape</h2>
<p class="subtitle">Every server message uses one small JSON structure.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
<pre><code><span class="line"><span class="kw">def</span> <span class="fn">now_iso</span>() -&gt; str:</span>
<span class="line"> <span class="kw">return</span> datetime.now(timezone.utc).isoformat()</span>
<span class="line"></span>
<span class="line"><span class="kw">def</span> <span class="fn">event</span>(event_type, room_id, client_id, content):</span>
<span class="line"> <span class="kw">return</span> {</span>
<span class="line"> <span class="str">"type"</span>: event_type,</span>
<span class="line"> <span class="str">"room_id"</span>: room_id,</span>
<span class="line"> <span class="str">"client_id"</span>: client_id,</span>
<span class="line"> <span class="str">"content"</span>: content,</span>
<span class="line"> <span class="str">"timestamp"</span>: now_iso(),</span>
<span class="line"> }</span></code></pre>
</div>
<aside class="notes">This slide shows the server-to-client contract: message type, room, sender, content, and timestamp.</aside>
</section>
<section class="slide code-slide" data-title="Backend Endpoint">
<h2>WebSocket Endpoint</h2>
<p class="subtitle">The route parameters select the room and the user identity.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
<pre><code><span class="line">@router.websocket(<span class="str">"/ws/{room_id}/{client_id}"</span>)</span>
<span class="line"><span class="kw">async def</span> <span class="fn">websocket_endpoint</span>(</span>
<span class="line"> websocket: WebSocket,</span>
<span class="line"> room_id: str,</span>
<span class="line"> client_id: str,</span>
<span class="line">):</span>
<span class="line"> room_id = clean_value(room_id, <span class="str">"general"</span>)</span>
<span class="line"> client_id = clean_value(client_id, <span class="str">"anonymous"</span>)</span>
<span class="line"></span>
<span class="line"> <span class="kw">await</span> manager.connect(websocket, room_id, client_id)</span></code></pre>
</div>
<aside class="notes">The browser URL maps directly to this route. FastAPI receives the HTTP Upgrade request and the manager accepts the socket.</aside>
</section>
<section class="slide code-slide" data-title="Backend Presence">
<h2>Join and Presence Events</h2>
<p class="subtitle">After connection, the server informs the room and refreshes online users.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
<pre><code><span class="line"><span class="kw">await</span> manager.broadcast_json(</span>
<span class="line"> event(<span class="str">"system"</span>, room_id, <span class="str">"system"</span>, <span class="str">f"{client_id} joined {room_id}."</span>),</span>
<span class="line"> room_id,</span>
<span class="line">)</span>
<span class="line"></span>
<span class="line"><span class="kw">await</span> manager.broadcast_json({</span>
<span class="line"> <span class="str">"type"</span>: <span class="str">"presence"</span>,</span>
<span class="line"> <span class="str">"users"</span>: manager.room_users(room_id),</span>
<span class="line"> <span class="str">"timestamp"</span>: now_iso(),</span>
<span class="line">}, room_id)</span></code></pre>
</div>
<aside class="notes">Presence is separate from normal chat messages, so the frontend can update the sidebar without adding a chat bubble.</aside>
</section>
<section class="slide code-slide" data-title="Backend Receive Loop">
<h2>Receive Loop and Broadcast</h2>
<p class="subtitle">The socket stays open and every valid message is broadcast to the room.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/websocket/chat.py</strong></div>
<pre><code><span class="line"><span class="kw">while</span> True:</span>
<span class="line"> raw_data = <span class="kw">await</span> websocket.receive_text()</span>
<span class="line"></span>
<span class="line"> <span class="kw">try</span>:</span>
<span class="line"> data = json.loads(raw_data)</span>
<span class="line"> message = str(data.get(<span class="str">"content"</span>, <span class="str">""</span>)).strip()</span>
<span class="line"> <span class="kw">except</span> json.JSONDecodeError:</span>
<span class="line"> message = raw_data.strip()</span>
<span class="line"></span>
<span class="line"> <span class="kw">if</span> message:</span>
<span class="line"> <span class="kw">await</span> manager.broadcast_json(event(<span class="str">"message"</span>, room_id, client_id, message), room_id)</span></code></pre>
</div>
<aside class="notes">This is the main real-time loop: receive text, parse JSON if possible, ignore empty messages, then broadcast.</aside>
</section>
<section class="slide code-slide" data-title="WebSocket Disconnect Handling">
<h2>Connection Teardown and Presence Update</h2>
<p class="subtitle">Handles user disconnects, cleans up the room, and updates presence for remaining clients.</p>
<div class="code-window">
<div class="code-titlebar">
<span></span><span></span><span></span>
<strong>app/websocket/chat.py</strong>
</div>
<pre><code><span class="line"><span class="kw">except</span> WebSocketDisconnect:</span>
<span class="line"> manager.disconnect(websocket, room_id)</span>
<span class="line"> <span class="kw">await</span> manager.broadcast_json(</span>
<span class="line"> event(<span class="str">"system"</span>, room_id, <span class="str">"سیستم"</span>, </span>
<span class="line"> f<span class="str">"{client_id} از اتاق {room_id} خارج شد."</span>),</span>
<span class="line"> room_id,</span>
<span class="line"> )</span>
<span class="line"> <span class="kw">await</span> manager.broadcast_json(</span>
<span class="line"> {</span>
<span class="line"> <span class="str">"type"</span>: <span class="str">"presence"</span>,</span>
<span class="line"> <span class="str">"room_id"</span>: room_id,</span>
<span class="line"> <span class="str">"client_id"</span>: <span class="str">"سیستم"</span>,</span>
<span class="line"> <span class="str">"content"</span>: <span class="str">"presence-updated"</span>,</span>
<span class="line"> <span class="str">"users"</span>: manager.room_users(room_id),</span>
<span class="line"> <span class="str">"timestamp"</span>: now_iso(),</span>
<span class="line"> },</span>
<span class="line"> room_id,</span>
<span class="line"> )</span></code></pre>
</div>
<aside class="notes">
This block handles client disconnection: it removes the websocket from the room,
broadcasts a system message announcing the departure, and then sends an updated
presence payload so all clients can refresh the active user list.
</aside>
</section>
<section class="slide code-slide" data-title="Backend Connection Manager">
<h2>ConnectionManager</h2>
<p class="subtitle">Manages WebSocket connections grouped by room and cleans up disconnected clients.</p>
<div class="code-window">
<div class="code-titlebar">
<span></span><span></span><span></span>
<strong>app/websocket/manager.py</strong>
</div>
<pre><code><span class="line"><span class="kw">class</span> <span class="fn">ConnectionManager</span>:</span>
<span class="line"> <span class="kw">def</span> <span class="fn">__init__</span>(self):</span>
<span class="line"> self.active_connections: dict[str, list[ClientConnection]] = {}</span>
<span class="line"></span>
<span class="line"> <span class="kw">async def</span> <span class="fn">connect</span>(self, websocket: WebSocket, room_id: str, client_id: str):</span>
<span class="line"> <span class="kw">await</span> websocket.accept()</span>
<span class="line"> self.active_connections.setdefault(room_id, [])</span>
<span class="line"> self.active_connections[room_id].append(ClientConnection(websocket=websocket, client_id=client_id))</span>
<span class="line"></span>
<span class="line"> <span class="kw">def</span> <span class="fn">disconnect</span>(self, websocket: WebSocket, room_id: str):</span>
<span class="line"> <span class="kw">if</span> room_id <span class="kw">not</span> <span class="kw">in</span> self.active_connections: <span class="kw">return</span></span>
<span class="line"></span>
<span class="line"> self.active_connections[room_id] = [</span>
<span class="line"> connection</span>
<span class="line"> <span class="kw">for</span> connection <span class="kw">in</span> self.active_connections[room_id] <span class="kw">if</span> connection.websocket <span class="kw">is</span> <span class="kw">not</span> websocket]</span>
<span class="line"></span>
<span class="line"> <span class="kw">if</span> <span class="kw">not</span> self.active_connections[room_id]:</span>
<span class="line"> <span class="kw">del</span> self.active_connections[room_id]</span></code></pre>
</div>
<aside class="notes">
This slide shows how the ConnectionManager tracks active users per room, handles new WebSocket
connections, and removes clients when they disconnect. Empty rooms are automatically cleaned up.
</aside>
</section>
<section class="slide code-slide" data-title="Backend Broadcast Manager">
<h2>Broadcast</h2>
<p class="subtitle">Sends messages to all clients in a room and removes failed WebSocket connections safely.</p>
<div class="code-window">
<div class="code-titlebar">
<span></span><span></span><span></span>
<strong>app/websocket/manager.py</strong>
</div>
<pre><code><span class="line"><span class="kw">class</span> <span class="fn">ConnectionManager</span>:</span>
<span class="line"></span>
<span class="line"> <span class="kw">async def</span> <span class="fn">broadcast_json</span>(self, payload: <span class="kw">dict</span>[str, Any], room_id: str):</span>
<span class="line"> <span class="kw">if</span> room_id <span class="kw">not</span> <span class="kw">in</span> self.active_connections:</span>
<span class="line"> <span class="kw">return</span></span>
<span class="line"></span>
<span class="line"> dead_connections: <span class="kw">list</span>[WebSocket] = []</span>
<span class="line"></span>
<span class="line"> <span class="com"># Iterate over a copy so disconnected clients can be removed safely later.</span></span>
<span class="line"> <span class="kw">for</span> connection <span class="kw">in</span> <span class="kw">list</span>(self.active_connections[room_id]):</span>
<span class="line"> <span class="kw">try</span>:</span>
<span class="line"> <span class="kw">await</span> connection.websocket.send_json(payload)</span>
<span class="line"> <span class="kw">except</span> Exception:</span>
<span class="line"> dead_connections.append(connection.websocket)</span>
<span class="line"></span>
<span class="line"> <span class="kw">for</span> websocket <span class="kw">in</span> dead_connections:</span>
<span class="line"> self.disconnect(websocket, room_id)</span></code></pre>
</div>
<aside class="notes">
This method broadcasts a JSON payload to all clients in a room.
It safely handles broken connections by collecting failed sockets and removing them after iteration,
preventing runtime errors while modifying the connection list.
</aside>
</section>
<section class="slide code-slide" data-title="Frontend State">
<h2>Frontend Codes</h2>
<p class="subtitle">browser state and DOM references.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
<pre><code><span class="line"><span class="kw">const</span> usernameInput = document.querySelector(<span class="str">'#username'</span>);</span>
<span class="line"><span class="kw">const</span> roomInput = document.querySelector(<span class="str">'#room'</span>);</span>
<span class="line"><span class="kw">const</span> connectBtn = document.querySelector(<span class="str">'#connectBtn'</span>);</span>
<span class="line"><span class="kw">const</span> messageForm = document.querySelector(<span class="str">'#messageForm'</span>);</span>
<span class="line"><span class="kw">const</span> messages = document.querySelector(<span class="str">'#messages'</span>);</span>
<span class="line"><span class="kw">const</span> usersList = document.querySelector(<span class="str">'#usersList'</span>);</span>
<span class="line"></span>
<span class="line"><span class="kw">let</span> socket = <span class="num">null</span>;</span>
<span class="line"><span class="kw">let</span> currentUser = <span class="str">''</span>;</span>
<span class="line"><span class="kw">let</span> currentRoom = <span class="str">''</span>;</span></code></pre>
</div>
<aside class="notes">You can introduce the frontend file by showing that the UI stores one WebSocket object and the current room/user state.</aside>
</section>
<section class="slide code-slide" data-title="Frontend URL">
<h2>Building the WebSocket URL</h2>
<p class="subtitle">The browser automatically switches between ws and wss.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
<pre><code><span class="line"><span class="kw">function</span> <span class="fn">connect</span>() {</span>
<span class="line"> currentUser = safeName(usernameInput.value, <span class="str">'anonymous'</span>);</span>
<span class="line"> currentRoom = safeName(roomInput.value, <span class="str">'general'</span>);</span>
<span class="line"></span>
<span class="line"> <span class="kw">const</span> wsProtocol = window.location.protocol === <span class="str">'https:'</span> ? <span class="str">'wss'</span> : <span class="str">'ws'</span>;</span>
<span class="line"> <span class="kw">const</span> room = encodeURIComponent(currentRoom);</span>
<span class="line"> <span class="kw">const</span> user = encodeURIComponent(currentUser);</span>
<span class="line"> <span class="kw">const</span> wsUrl = <span class="str">`${wsProtocol}://${window.location.host}/ws/${room}/${user}`</span>;</span>
<span class="line"></span>
<span class="line"> socket = <span class="kw">new</span> WebSocket(wsUrl);</span>
<span class="line">}</span></code></pre>
</div>
<aside class="notes">This is why the same frontend works locally and behind HTTPS on the server.</aside>
</section>
<section class="slide code-slide" data-title="Frontend Open Event">
<h2>Connection Open Handler</h2>
<p class="subtitle">When the socket opens, the UI moves into connected mode.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
<pre><code><span class="line">socket.addEventListener(<span class="str">'open'</span>, () =&gt; {</span>
<span class="line"> setConnectedState(<span class="num">true</span>);</span>
<span class="line"></span>
<span class="line"> roomTitle.textContent = <span class="str">`Room: ${currentRoom}`</span>;</span>
<span class="line"> roomSubtitle.textContent = <span class="str">`You are chatting as ${currentUser}.`</span>;</span>
<span class="line"></span>
<span class="line"> messageInput.focus();</span>
<span class="line">});</span></code></pre>
</div>
<aside class="notes">This slide connects WebSocket state to UI state: buttons, labels, and the message input change after the connection opens.</aside>
</section>
<section class="slide code-slide" data-title="Frontend Message Event">
<h2>Handling Incoming Messages</h2>
<p class="subtitle">Presence updates and chat messages are separated by type.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
<pre><code><span class="line">socket.addEventListener(<span class="str">'message'</span>, (event) =&gt; {</span>
<span class="line"> <span class="kw">const</span> payload = JSON.parse(event.data);</span>
<span class="line"></span>
<span class="line"> <span class="kw">if</span> (payload.type === <span class="str">'presence'</span>) {</span>
<span class="line"> updateUsers(payload.users || []);</span>
<span class="line"> <span class="kw">return</span>;</span>
<span class="line"> }</span>
<span class="line"></span>
<span class="line"> addMessage(payload);</span>
<span class="line">});</span></code></pre>
</div>
<aside class="notes">The frontend treats presence as sidebar data and normal messages as chat feed data.</aside>
</section>
<section class="slide code-slide" data-title="Frontend Render">
<h2>Rendering a Chat Message</h2>
<p class="subtitle">The browser creates message elements without reloading the page.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
<pre><code><span class="line"><span class="kw">function</span> <span class="fn">addMessage</span>(payload) {</span>
<span class="line"> clearEmptyState();</span>
<span class="line"></span>
<span class="line"> <span class="kw">const</span> card = document.createElement(<span class="str">'article'</span>);</span>
<span class="line"> card.className = <span class="str">'message'</span>;</span>
<span class="line"></span>
<span class="line"> <span class="kw">if</span> (payload.client_id === currentUser) card.classList.add(<span class="str">'mine'</span>);</span>
<span class="line"></span>
<span class="line"> content.textContent = payload.content || <span class="str">''</span>;</span>
<span class="line"> card.append(meta, content);</span>
<span class="line"> messages.appendChild(card);</span>
<span class="line"> messages.scrollTop = messages.scrollHeight;</span>
<span class="line">}</span></code></pre>
</div>
<aside class="notes">This demonstrates DOM rendering and auto-scrolling. The UI changes because WebSocket data arrives, not because the page refreshes.</aside>
</section>
<section class="slide code-slide" data-title="Frontend Send">
<h2>Sending Messages</h2>
<p class="subtitle">The form sends JSON only when the socket is open.</p>
<div class="code-window">
<div class="code-titlebar"><span></span><span></span><span></span><strong>app/static/app.js</strong></div>
<pre><code><span class="line">messageForm.addEventListener(<span class="str">'submit'</span>, (event) =&gt; {</span>
<span class="line"> event.preventDefault();</span>
<span class="line"></span>
<span class="line"> <span class="kw">const</span> content = messageInput.value.trim();</span>
<span class="line"> <span class="kw">if</span> (!content || !socket || socket.readyState !== WebSocket.OPEN) <span class="kw">return</span>;</span>
<span class="line"></span>
<span class="line"> socket.send(JSON.stringify({ content }));</span>
<span class="line"> messageInput.value = <span class="str">''</span>;</span>
<span class="line"> messageInput.focus();</span>
<span class="line">});</span></code></pre>
</div>
<aside class="notes">This is the browser-to-server direction. The server receives this JSON and broadcasts the content to the room.</aside>
</section>
<section class="slide" data-title="Takeaways"> <section class="slide" data-title="Takeaways">
<h2>Keynotes and Takeaways</h2> <h2>Keynotes and Takeaways</h2>
<p class="subtitle">The essential points to remember from the project.</p> <p class="subtitle">The essential points to remember from the project.</p>

View File

@@ -1,6 +1,6 @@
@font-face { @font-face {
font-family: "Vazirmatn"; font-family: "Vazirmatn";
src: url("../../app/static/Vazirmatn[wght].woff2") format("woff2"); src: url("Vazirmatn[wght].woff2") format("woff2");
font-weight: 100 900; font-weight: 100 900;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -93,7 +93,8 @@ body {
text-align: center; text-align: center;
} }
.slide.active.protocol-slide, .slide.active.protocol-slide,
.slide.active.image-slide { .slide.active.image-slide,
.slide.active.code-slide {
display: grid; display: grid;
grid-template-rows: auto auto minmax(0, 1fr); grid-template-rows: auto auto minmax(0, 1fr);
} }
@@ -131,7 +132,8 @@ h2 { font-size: clamp(27px, calc(46px * var(--slide-scale)), 66px); }
letter-spacing: .01em; letter-spacing: .01em;
} }
.protocol-slide .subtitle, .protocol-slide .subtitle,
.image-slide .subtitle { .image-slide .subtitle,
.code-slide .subtitle {
margin-bottom: clamp(10px, calc(14px * var(--slide-scale)), 22px); margin-bottom: clamp(10px, calc(14px * var(--slide-scale)), 22px);
} }
@@ -635,6 +637,89 @@ pre {
} }
.ltr { direction: ltr; text-align: left; } .ltr { direction: ltr; text-align: left; }
.code-window {
min-height: 0;
border-radius: clamp(10px, calc(14px * var(--slide-scale)), 18px);
background: #0d1117;
border: 1px solid rgba(255,255,255,.13);
box-shadow: 0 24px 70px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,255,255,.08);
overflow: hidden;
direction: ltr;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
}
.code-titlebar {
height: clamp(38px, calc(44px * var(--slide-scale)), 56px);
background: linear-gradient(180deg, #1f2430, #151a24);
border-bottom: 1px solid rgba(255,255,255,.09);
display: flex;
align-items: center;
gap: 8px;
padding: 0 clamp(14px, calc(18px * var(--slide-scale)), 26px);
}
.code-titlebar span {
width: clamp(10px, calc(12px * var(--slide-scale)), 15px);
height: clamp(10px, calc(12px * var(--slide-scale)), 15px);
border-radius: 50%;
display: block;
}
.code-titlebar span:nth-child(1) { background: #ff5f57; }
.code-titlebar span:nth-child(2) { background: #febc2e; }
.code-titlebar span:nth-child(3) { background: #28c840; }
.code-titlebar strong {
margin-left: 10px;
color: #c9d1d9;
font-family: "Cascadia Code", "Consolas", "SFMono-Regular", monospace;
font-size: clamp(12px, calc(14px * var(--slide-scale)), 19px);
font-weight: 600;
}
.code-window pre {
min-height: 0;
overflow: hidden;
background:
linear-gradient(90deg, rgba(125, 211, 252, .035), transparent 36%),
#0d1117;
border-radius: 0;
box-shadow: none;
padding: clamp(14px, calc(20px * var(--slide-scale)), 28px) 0;
white-space: pre;
font-size: clamp(11px, calc(14px * var(--slide-scale)), 19px);
line-height: 0;
}
.code-window code {
display: block;
background: transparent;
border-radius: 0;
color: #d6deeb;
padding: 0;
font-family: "Cascadia Code", "Consolas", "SFMono-Regular", monospace;
direction: ltr;
unicode-bidi: normal;
}
.code-window .line {
counter-increment: code-line;
display: block;
min-height: 1.55em;
line-height: 1.45;
padding: 0 clamp(18px, calc(28px * var(--slide-scale)), 42px) 0 0;
}
.code-window code {
counter-reset: code-line;
}
.code-window .line::before {
content: counter(code-line);
display: inline-block;
width: clamp(34px, calc(44px * var(--slide-scale)), 56px);
margin-right: clamp(12px, calc(18px * var(--slide-scale)), 26px);
color: #6e7681;
text-align: right;
user-select: none;
}
.code-window .kw { color: #ff7b72; }
.code-window .fn { color: #d2a8ff; }
.code-window .str { color: #a5d6ff; }
.code-window .num { color: #79c0ff; }
.version-row { .version-row {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@@ -666,7 +751,7 @@ pre {
} }
.detailed-version-row li { .detailed-version-row li {
font-size: clamp(10px, calc(13px * var(--slide-scale)), 17px); font-size: clamp(10px, calc(13px * var(--slide-scale)), 17px);
line-height: 1.42; /* line-height: 1.42; */
} }
.detailed-version-row code { .detailed-version-row code {
color: #ffe8aa; color: #ffe8aa;
@@ -1007,7 +1092,8 @@ code {
.subtitle { font-size: clamp(14px, 4vw, 18px); } .subtitle { font-size: clamp(14px, 4vw, 18px); }
.kicker { font-size: clamp(12px, 3.7vw, 16px); } .kicker { font-size: clamp(12px, 3.7vw, 16px); }
.slide.active.protocol-slide, .slide.active.protocol-slide,
.slide.active.image-slide { display: block; } .slide.active.image-slide,
.slide.active.code-slide { display: block; }
.hero-grid, .split, .demo-layout, .version-row, .resource-grid, .mechanism-grid, .takeaway-grid { grid-template-columns: 1fr; } .hero-grid, .split, .demo-layout, .version-row, .resource-grid, .mechanism-grid, .takeaway-grid { grid-template-columns: 1fr; }
.takeaway-grid { grid-template-rows: none; min-height: 0; } .takeaway-grid { grid-template-rows: none; min-height: 0; }
.hero-grid, .split, .demo-layout, .mechanism-grid { gap: 18px; } .hero-grid, .split, .demo-layout, .mechanism-grid { gap: 18px; }
@@ -1047,6 +1133,19 @@ code {
.mini-points span { font-size: 12px; padding: 7px 10px; } .mini-points span { font-size: 12px; padding: 7px 10px; }
.quote { font-size: 15px; } .quote { font-size: 15px; }
pre { font-size: clamp(10px, 2.8vw, 13px); padding: 14px; } pre { font-size: clamp(10px, 2.8vw, 13px); padding: 14px; }
.code-window { display: block; }
.code-window pre {
overflow-x: auto;
font-size: clamp(10px, 2.7vw, 13px);
padding: 12px 0;
}
.code-window .line {
padding-right: 14px;
}
.code-window .line::before {
width: 32px;
margin-right: 12px;
}
ol { font-size: 14px; line-height: 1.9; } ol { font-size: 14px; line-height: 1.9; }
.controls { left: 14px; bottom: 14px; } .controls { left: 14px; bottom: 14px; }
.controls button { width: 36px; height: 36px; font-size: 24px; } .controls button { width: 36px; height: 36px; font-size: 24px; }