2026-01-14 19:04:09 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
ArchitectureDemo.vue
|
|
|
|
|
|
终端架构演示组件
|
|
|
|
|
|
|
|
|
|
|
|
用途:
|
|
|
|
|
|
可视化展示 Terminal(终端)、Shell(壳)和 Kernel(内核)之间的交互流程。
|
|
|
|
|
|
通过模拟 "ls" 命令的执行过程,帮助用户理解输入传输、解析、系统调用、数据返回和渲染显示的完整链路。
|
|
|
|
|
|
|
|
|
|
|
|
交互功能:
|
|
|
|
|
|
- 逐步演示 (Step-by-Step):用户点击按钮一步步观察数据包流转。
|
|
|
|
|
|
- 中英双语说明:适应不同语言背景的读者。
|
|
|
|
|
|
- 状态反馈:实时显示各组件(终端/Shell/内核)的当前状态(空闲/忙碌)。
|
|
|
|
|
|
-->
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="arch-demo">
|
|
|
|
|
|
<div class="analogy-header">
|
|
|
|
|
|
<div class="analogy-item">
|
2026-01-15 12:25:48 +08:00
|
|
|
|
<div class="icon">🖥️</div>
|
2026-01-14 19:04:09 +08:00
|
|
|
|
<div class="text">
|
2026-01-15 12:25:48 +08:00
|
|
|
|
<div class="role">Terminal (终端)</div>
|
|
|
|
|
|
<div class="desc">传声筒 / 窗口</div>
|
2026-01-14 19:04:09 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="analogy-item">
|
2026-01-15 12:25:48 +08:00
|
|
|
|
<div class="icon">🗣️</div>
|
2026-01-14 19:04:09 +08:00
|
|
|
|
<div class="text">
|
2026-01-15 12:25:48 +08:00
|
|
|
|
<div class="role">Shell (壳)</div>
|
|
|
|
|
|
<div class="desc">翻译官 / 助手</div>
|
2026-01-14 19:04:09 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="analogy-item">
|
2026-01-15 12:25:48 +08:00
|
|
|
|
<div class="icon">⚙️</div>
|
2026-01-14 19:04:09 +08:00
|
|
|
|
<div class="text">
|
2026-01-15 12:25:48 +08:00
|
|
|
|
<div class="role">Kernel (内核)</div>
|
|
|
|
|
|
<div class="desc">大管家 / 芯片</div>
|
2026-01-14 19:04:09 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-15 12:25:48 +08:00
|
|
|
|
<div class="diagram-container" @click="nextStep" :class="{ 'clickable': currentStep < totalSteps }">
|
|
|
|
|
|
<!-- Click Overlay Hint -->
|
|
|
|
|
|
<div class="click-overlay" v-if="currentStep === 0">
|
|
|
|
|
|
<div class="click-hint">
|
|
|
|
|
|
<span class="icon">👆</span>
|
|
|
|
|
|
<span class="text">不断点击屏幕演示 / Keep Clicking</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Completed Overlay -->
|
|
|
|
|
|
<div class="completed-overlay" v-if="currentStep >= totalSteps">
|
|
|
|
|
|
<div class="completed-hint" @click.stop="reset">
|
|
|
|
|
|
<span class="icon">✅</span>
|
|
|
|
|
|
<span class="text">演示结束,点击重置 / Finished (Reset)</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Spaces Background -->
|
|
|
|
|
|
<div class="spaces-bg">
|
|
|
|
|
|
<div class="space user-space">
|
|
|
|
|
|
<div class="space-header">User Space (用户空间)</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="barrier">
|
|
|
|
|
|
<div class="barrier-line"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="space kernel-space">
|
|
|
|
|
|
<div class="space-header">Kernel Space (内核空间)</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-14 19:04:09 +08:00
|
|
|
|
<!-- Terminal Node -->
|
|
|
|
|
|
<div class="node terminal" :class="{ active: activeNode === 'terminal' }">
|
|
|
|
|
|
<div class="node-title">TERMINAL (终端)</div>
|
|
|
|
|
|
<div class="screen">
|
|
|
|
|
|
<div v-for="(line, i) in terminalLines" :key="i" class="line">{{ line }}</div>
|
|
|
|
|
|
<div class="line input-line">
|
|
|
|
|
|
<span class="prompt">$</span>
|
|
|
|
|
|
<span class="typing">{{ currentInput }}</span>
|
|
|
|
|
|
<span class="cursor" v-if="activeNode === 'terminal'"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="node-label">/dev/tty</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Connections -->
|
|
|
|
|
|
<div class="connection t-s" :class="{ active: packetState === 't-to-s' || packetState === 's-to-t' }">
|
|
|
|
|
|
<div class="line-path"></div>
|
|
|
|
|
|
<div class="data-label" v-if="packetState === 't-to-s'">
|
|
|
|
|
|
<span class="icon">➡️</span> Bytes
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="data-label" v-if="packetState === 's-to-t'">
|
|
|
|
|
|
<span class="icon">⬅️</span> Text
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="conn-label">stdin / stdout</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Shell Node -->
|
|
|
|
|
|
<div class="node shell" :class="{ active: activeNode === 'shell' }">
|
|
|
|
|
|
<div class="node-title">SHELL (壳)</div>
|
|
|
|
|
|
<div class="process-box">
|
|
|
|
|
|
<div class="status-icon">{{ shellIcon }}</div>
|
|
|
|
|
|
<div class="status">{{ shellStatus }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="node-label">/bin/zsh</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Connections -->
|
|
|
|
|
|
<div class="connection s-k" :class="{ active: packetState === 's-to-k' || packetState === 'k-to-s' }">
|
|
|
|
|
|
<div class="line-path"></div>
|
|
|
|
|
|
<div class="data-label" v-if="packetState === 's-to-k'">
|
|
|
|
|
|
<span class="icon">➡️</span> Syscall
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="data-label" v-if="packetState === 'k-to-s'">
|
|
|
|
|
|
<span class="icon">⬅️</span> Raw Data
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="conn-label">System Calls</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Kernel Node -->
|
|
|
|
|
|
<div class="node kernel" :class="{ active: activeNode === 'kernel' }">
|
|
|
|
|
|
<div class="node-title">KERNEL (内核)</div>
|
|
|
|
|
|
<div class="process-box">
|
|
|
|
|
|
<div class="status-icon">{{ kernelIcon }}</div>
|
|
|
|
|
|
<div class="status">{{ kernelStatus }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="node-label">macOS / Linux</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="controls">
|
|
|
|
|
|
<div class="btn-group">
|
|
|
|
|
|
<button class="btn primary" @click="nextStep" :disabled="currentStep >= totalSteps">
|
|
|
|
|
|
<span v-if="currentStep === 0">▶️ Start Simulation / 开始演示</span>
|
|
|
|
|
|
<span v-else-if="currentStep < totalSteps">Next Step / 下一步 ({{ currentStep }}/{{ totalSteps }}) ➡️</span>
|
|
|
|
|
|
<span v-else>✅ Done / 完成 (Reset)</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn secondary" @click="reset" v-if="currentStep > 0">
|
|
|
|
|
|
Reset / 重置
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="step-info" v-if="currentStep > 0">
|
|
|
|
|
|
<div class="step-title">
|
|
|
|
|
|
{{ steps[currentStep - 1].titleEn }}
|
|
|
|
|
|
<span class="divider">|</span>
|
|
|
|
|
|
{{ steps[currentStep - 1].titleZh }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step-desc">
|
|
|
|
|
|
<div class="en">{{ steps[currentStep - 1].descEn }}</div>
|
|
|
|
|
|
<div class="zh">{{ steps[currentStep - 1].descZh }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step-tech">
|
|
|
|
|
|
<span class="tech-label">Technical / 技术原理:</span>
|
|
|
|
|
|
<div class="tech-content">
|
|
|
|
|
|
<div class="en">{{ steps[currentStep - 1].techEn }}</div>
|
|
|
|
|
|
<div class="zh">{{ steps[currentStep - 1].techZh }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step-info placeholder" v-else>
|
|
|
|
|
|
<div class="step-desc">
|
|
|
|
|
|
<div class="en">Click "Start Simulation" to see how the command 'ls' travels through the system.</div>
|
|
|
|
|
|
<div class="zh">点击“开始演示”查看 'ls' 命令如何在系统中流转。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, computed } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
const currentStep = ref(0)
|
|
|
|
|
|
const activeNode = ref('terminal')
|
|
|
|
|
|
const packetState = ref(null)
|
|
|
|
|
|
const terminalLines = ref([])
|
|
|
|
|
|
const currentInput = ref('')
|
|
|
|
|
|
const shellStatus = ref('Idle')
|
|
|
|
|
|
const shellIcon = ref('💤')
|
|
|
|
|
|
const kernelStatus = ref('Idle')
|
|
|
|
|
|
const kernelIcon = ref('💤')
|
|
|
|
|
|
|
|
|
|
|
|
const steps = [
|
|
|
|
|
|
{
|
|
|
|
|
|
titleEn: "1. User Input",
|
|
|
|
|
|
titleZh: "1. 用户输入",
|
|
|
|
|
|
descEn: "You type 'ls' in the terminal window. The terminal captures your keystrokes.",
|
|
|
|
|
|
descZh: "你在终端窗口输入 'ls'。终端会捕获你的按键操作。",
|
|
|
|
|
|
techEn: "Terminal buffers input in 'Cooked Mode' until you press Enter.",
|
|
|
|
|
|
techZh: "终端在“加工模式 (Cooked Mode)”下缓冲输入,直到你按下回车键。",
|
|
|
|
|
|
action: async () => {
|
|
|
|
|
|
activeNode.value = 'terminal'
|
|
|
|
|
|
currentInput.value = 'l'
|
|
|
|
|
|
await wait(200)
|
|
|
|
|
|
currentInput.value = 'ls'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
titleEn: "2. Transmission",
|
|
|
|
|
|
titleZh: "2. 传输",
|
|
|
|
|
|
descEn: "The Terminal sends the characters 'l', 's', and 'Enter' to the Shell.",
|
|
|
|
|
|
descZh: "终端将字符 'l'、's' 和 '回车' 发送给 Shell。",
|
|
|
|
|
|
techEn: "Data travels via standard input (stdin) as a byte stream.",
|
|
|
|
|
|
techZh: "数据通过标准输入 (stdin) 以字节流的形式传输。",
|
|
|
|
|
|
action: async () => {
|
|
|
|
|
|
packetState.value = 't-to-s'
|
|
|
|
|
|
await wait(1000)
|
|
|
|
|
|
packetState.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
titleEn: "3. Shell Parsing",
|
|
|
|
|
|
titleZh: "3. Shell 解析",
|
2026-01-15 12:25:48 +08:00
|
|
|
|
descEn: "The Shell (Waiter) translates your command for the Kernel.",
|
|
|
|
|
|
descZh: "Shell(服务员)接收指令,并将其翻译成内核能听懂的请求。",
|
2026-01-14 19:04:09 +08:00
|
|
|
|
techEn: "Shell tokenizes input, finds the 'ls' executable in $PATH.",
|
|
|
|
|
|
techZh: "Shell 对输入进行分词,并在 $PATH 环境变量中查找 'ls' 可执行文件。",
|
|
|
|
|
|
action: async () => {
|
|
|
|
|
|
activeNode.value = 'shell'
|
|
|
|
|
|
shellIcon.value = '🧠'
|
|
|
|
|
|
shellStatus.value = 'Parsing "ls"...'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
titleEn: "4. System Call",
|
|
|
|
|
|
titleZh: "4. 系统调用",
|
|
|
|
|
|
descEn: "The Shell asks the Kernel to read the file list from the disk.",
|
|
|
|
|
|
descZh: "Shell 请求内核从磁盘读取文件列表。",
|
|
|
|
|
|
techEn: "Shell invokes `execve()` and `getdents()` system calls.",
|
|
|
|
|
|
techZh: "Shell 调用 `execve()` 和 `getdents()` 等系统调用。",
|
|
|
|
|
|
action: async () => {
|
|
|
|
|
|
packetState.value = 's-to-k'
|
|
|
|
|
|
await wait(1000)
|
|
|
|
|
|
packetState.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
titleEn: "5. Kernel Execution",
|
|
|
|
|
|
titleZh: "5. 内核执行",
|
2026-01-15 12:25:48 +08:00
|
|
|
|
descEn: "The Kernel (Kitchen) executes the request by accessing hardware.",
|
|
|
|
|
|
descZh: "内核(后厨)直接操作硬件(如磁盘)来执行实际任务。",
|
2026-01-14 19:04:09 +08:00
|
|
|
|
techEn: "Kernel driver accesses the file system (APFS/ext4).",
|
|
|
|
|
|
techZh: "内核驱动程序访问文件系统 (APFS/ext4)。",
|
|
|
|
|
|
action: async () => {
|
|
|
|
|
|
activeNode.value = 'kernel'
|
|
|
|
|
|
kernelIcon.value = '💾'
|
|
|
|
|
|
kernelStatus.value = 'Reading Disk...'
|
|
|
|
|
|
await wait(800)
|
|
|
|
|
|
kernelStatus.value = 'Data Found'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
titleEn: "6. Returning Data",
|
|
|
|
|
|
titleZh: "6. 返回数据",
|
|
|
|
|
|
descEn: "The Kernel gives the raw file list back to the Shell.",
|
|
|
|
|
|
descZh: "内核将原始文件列表数据返回给 Shell。",
|
|
|
|
|
|
techEn: "System call returns with file descriptors/structs.",
|
|
|
|
|
|
techZh: "系统调用返回文件描述符或结构体数据。",
|
|
|
|
|
|
action: async () => {
|
|
|
|
|
|
kernelStatus.value = 'Idle'
|
|
|
|
|
|
kernelIcon.value = '💤'
|
|
|
|
|
|
packetState.value = 'k-to-s'
|
|
|
|
|
|
await wait(1000)
|
|
|
|
|
|
packetState.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
titleEn: "7. Formatting",
|
|
|
|
|
|
titleZh: "7. 格式化",
|
|
|
|
|
|
descEn: "The Shell formats the raw list into text, adding colors if needed.",
|
|
|
|
|
|
descZh: "Shell 将原始列表格式化为文本,并根据需要添加颜色。",
|
|
|
|
|
|
techEn: "Shell formats output buffer, adding ANSI color codes.",
|
|
|
|
|
|
techZh: "Shell 格式化输出缓冲区,并添加 ANSI 颜色代码。",
|
|
|
|
|
|
action: async () => {
|
|
|
|
|
|
activeNode.value = 'shell'
|
|
|
|
|
|
shellIcon.value = '🎨'
|
|
|
|
|
|
shellStatus.value = 'Formatting...'
|
|
|
|
|
|
await wait(500)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
titleEn: "8. Display Output",
|
|
|
|
|
|
titleZh: "8. 显示输出",
|
|
|
|
|
|
descEn: "The Shell sends the final text back to the Terminal to show you.",
|
|
|
|
|
|
descZh: "Shell 将最终文本发送回终端以供显示。",
|
|
|
|
|
|
techEn: "Data travels via standard output (stdout) to the TTY.",
|
|
|
|
|
|
techZh: "数据通过标准输出 (stdout) 传输到 TTY。",
|
|
|
|
|
|
action: async () => {
|
|
|
|
|
|
shellStatus.value = 'Idle'
|
|
|
|
|
|
shellIcon.value = '💤'
|
|
|
|
|
|
packetState.value = 's-to-t'
|
|
|
|
|
|
await wait(1000)
|
|
|
|
|
|
packetState.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
titleEn: "9. Render",
|
|
|
|
|
|
titleZh: "9. 渲染",
|
|
|
|
|
|
descEn: "The Terminal draws the text on the screen grid.",
|
|
|
|
|
|
descZh: "终端在屏幕网格上绘制文本。",
|
|
|
|
|
|
techEn: "Terminal emulator renders glyphs into the frame buffer.",
|
|
|
|
|
|
techZh: "终端模拟器将字形渲染到帧缓冲区中。",
|
|
|
|
|
|
action: async () => {
|
|
|
|
|
|
activeNode.value = 'terminal'
|
|
|
|
|
|
terminalLines.value = ['file1.txt photo.jpg', 'notes.md']
|
|
|
|
|
|
currentInput.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const totalSteps = steps.length
|
|
|
|
|
|
|
|
|
|
|
|
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|
|
|
|
|
|
|
|
|
|
|
const nextStep = async () => {
|
|
|
|
|
|
if (currentStep.value >= totalSteps) {
|
|
|
|
|
|
reset()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const step = steps[currentStep.value]
|
|
|
|
|
|
currentStep.value++
|
|
|
|
|
|
await step.action()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const reset = () => {
|
|
|
|
|
|
currentStep.value = 0
|
|
|
|
|
|
activeNode.value = 'terminal'
|
|
|
|
|
|
packetState.value = null
|
|
|
|
|
|
terminalLines.value = []
|
|
|
|
|
|
currentInput.value = ''
|
|
|
|
|
|
shellStatus.value = 'Idle'
|
|
|
|
|
|
shellIcon.value = '💤'
|
|
|
|
|
|
kernelStatus.value = 'Idle'
|
|
|
|
|
|
kernelIcon.value = '💤'
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.arch-demo {
|
|
|
|
|
|
background: #09090b;
|
|
|
|
|
|
padding: 30px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
border: 1px solid #27272a;
|
|
|
|
|
|
font-family: 'JetBrains Mono', 'Menlo', monospace;
|
|
|
|
|
|
color: #e4e4e7;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 30px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.analogy-header {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
border-bottom: 1px solid #27272a;
|
|
|
|
|
|
padding-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.analogy-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.analogy-item .icon {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
background: #18181b;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
border: 1px solid #27272a;
|
|
|
|
|
|
width: 48px;
|
|
|
|
|
|
height: 48px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.analogy-item .role {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #22d3ee;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.analogy-item .desc {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #a1a1aa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.diagram-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
position: relative;
|
2026-01-15 12:25:48 +08:00
|
|
|
|
/* Increase padding to accommodate labels */
|
|
|
|
|
|
padding: 40px 10px 20px 10px;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
cursor: default;
|
|
|
|
|
|
transition: background 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.diagram-container.clickable {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.diagram-container.clickable:hover {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.02);
|
2026-01-14 19:04:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 12:25:48 +08:00
|
|
|
|
.click-overlay {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
z-index: 50; /* Topmost */
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.4);
|
|
|
|
|
|
backdrop-filter: blur(2px);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
animation: pulse-bg 2s infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.click-hint {
|
|
|
|
|
|
background: #22c55e;
|
|
|
|
|
|
color: #000;
|
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
|
border-radius: 30px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
box-shadow: 0 4px 15px rgba(34, 197, 94, 0.4);
|
|
|
|
|
|
transform: scale(1);
|
|
|
|
|
|
transition: transform 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.diagram-container:hover .click-hint {
|
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes pulse-bg {
|
|
|
|
|
|
0% { background: rgba(0, 0, 0, 0.4); }
|
|
|
|
|
|
50% { background: rgba(0, 0, 0, 0.2); }
|
|
|
|
|
|
100% { background: rgba(0, 0, 0, 0.4); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.completed-overlay {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
z-index: 50; /* Same as click overlay */
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.6);
|
|
|
|
|
|
backdrop-filter: blur(2px);
|
|
|
|
|
|
animation: fade-in 0.5s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.completed-hint {
|
|
|
|
|
|
background: #10b981;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
|
border-radius: 30px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: transform 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.completed-hint:hover {
|
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
|
background: #059669;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.spaces-bg {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
z-index: 0;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.space {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.space-header {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-space {
|
|
|
|
|
|
flex: 2;
|
|
|
|
|
|
background: rgba(34, 211, 238, 0.03);
|
|
|
|
|
|
border-right: 1px dashed #3f3f46;
|
|
|
|
|
|
border-radius: 8px 0 0 8px;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
/* Ensure User Space (containing Shell) is below the Barrier Label */
|
|
|
|
|
|
z-index: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.user-space .space-header { color: #22d3ee; }
|
|
|
|
|
|
|
|
|
|
|
|
.kernel-space {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
background: rgba(239, 68, 68, 0.03);
|
|
|
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
z-index: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.kernel-space .space-header { color: #ef4444; }
|
|
|
|
|
|
|
|
|
|
|
|
.barrier {
|
|
|
|
|
|
width: 2px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 10; /* Bring Barrier to front */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.barrier-line {
|
|
|
|
|
|
width: 2px;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: repeating-linear-gradient(
|
|
|
|
|
|
to bottom,
|
|
|
|
|
|
#facc15 0,
|
|
|
|
|
|
#facc15 10px,
|
|
|
|
|
|
transparent 10px,
|
|
|
|
|
|
transparent 20px
|
|
|
|
|
|
);
|
|
|
|
|
|
opacity: 0.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-14 19:04:09 +08:00
|
|
|
|
.node {
|
|
|
|
|
|
background: #18181b;
|
|
|
|
|
|
border: 2px solid #27272a;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
width: 140px;
|
|
|
|
|
|
height: 130px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
transition: all 0.3s;
|
2026-01-15 12:25:48 +08:00
|
|
|
|
z-index: 5; /* Nodes should be above spaces but below barrier label if overlapping */
|
2026-01-14 19:04:09 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 12:25:48 +08:00
|
|
|
|
/* Specific z-index for Shell to prevent it from covering barrier label */
|
|
|
|
|
|
.node.shell {
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 19:04:09 +08:00
|
|
|
|
.node.active {
|
|
|
|
|
|
border-color: #22c55e;
|
|
|
|
|
|
box-shadow: 0 0 15px rgba(34, 197, 94, 0.2);
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.node-title {
|
|
|
|
|
|
background: #27272a;
|
|
|
|
|
|
color: #a1a1aa;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
padding: 6px 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
|
border-radius: 6px 6px 0 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.node-label {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: -20px;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
color: #71717a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.screen, .process-box {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.process-box {
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-icon {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.screen {
|
|
|
|
|
|
background: #000;
|
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.line {
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.input-line {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.prompt {
|
|
|
|
|
|
color: #22c55e;
|
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cursor {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
height: 12px;
|
|
|
|
|
|
background: #e4e4e7;
|
|
|
|
|
|
animation: blink 1s step-end infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #facc15;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.connection {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 2px;
|
|
|
|
|
|
background: #27272a;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
margin: 0 15px;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.connection.active {
|
|
|
|
|
|
background: #22c55e;
|
|
|
|
|
|
box-shadow: 0 0 10px rgba(34, 197, 94, 0.4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.conn-label {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
color: #52525b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.data-label {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: -25px;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
background: #22c55e;
|
|
|
|
|
|
color: #000;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
animation: pop-in 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes pop-in {
|
|
|
|
|
|
from { opacity: 0; transform: translate(-50%, 5px); }
|
|
|
|
|
|
to { opacity: 1; transform: translate(-50%, 0); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes blink {
|
|
|
|
|
|
50% { opacity: 0; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.controls {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
background: #18181b;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
border: 1px solid #27272a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-group {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn {
|
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn.primary {
|
|
|
|
|
|
background: #22c55e;
|
|
|
|
|
|
color: #000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn.primary:hover:not(:disabled) {
|
|
|
|
|
|
background: #16a34a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn.primary:disabled {
|
|
|
|
|
|
background: #27272a;
|
|
|
|
|
|
color: #71717a;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn.secondary {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border-color: #3f3f46;
|
|
|
|
|
|
color: #a1a1aa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn.secondary:hover {
|
|
|
|
|
|
border-color: #71717a;
|
|
|
|
|
|
color: #e4e4e7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
animation: fade-in 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #22d3ee;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-desc {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #e4e4e7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-tech {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #71717a;
|
|
|
|
|
|
background: #09090b;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tech-label {
|
|
|
|
|
|
color: #facc15;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes fade-in {
|
|
|
|
|
|
from { opacity: 0; transform: translateY(5px); }
|
|
|
|
|
|
to { opacity: 1; transform: translateY(0); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 640px) {
|
|
|
|
|
|
.analogy-header {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.diagram-container {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 50px;
|
|
|
|
|
|
padding: 20px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.connection {
|
|
|
|
|
|
width: 2px;
|
|
|
|
|
|
height: 50px;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.conn-label {
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 10px;
|
|
|
|
|
|
right: auto;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.packet {
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 10px;
|
|
|
|
|
|
animation: travel-vertical 1s linear forwards;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.packet.reverse {
|
|
|
|
|
|
animation: travel-vertical-back 1s linear forwards;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes travel-vertical {
|
|
|
|
|
|
0% { top: 0; transform: translateY(0); }
|
|
|
|
|
|
100% { top: 100%; transform: translateY(-100%); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes travel-vertical-back {
|
|
|
|
|
|
0% { top: 100%; transform: translateY(-100%); }
|
|
|
|
|
|
100% { top: 0; transform: translateY(0); }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|